Compare commits

...

18 Commits

Author SHA1 Message Date
administrator a79a1a7b0e Initial commit 2025-03-21 10:59:11 +05:30
administrator 2033d5c227 Initial commit 2025-03-21 10:59:11 +05:30
raman cabb3c85a1 fix issues 2025-03-21 10:56:43 +05:30
Pranay a64fdf9a43 Recruitment Changes 2025-03-20 19:03:52 +05:30
shankar a9650eb637 Target change to Number of Positions 2025-03-13 12:00:17 +05:30
shankar dec0db69f2 Change to Expeted Skills TO Primary Skills 2025-03-12 18:26:43 +05:30
Pranay a421881fda fix dependencies HR Recruitment extended 2025-03-12 17:58:42 +05:30
Pranay 56179c5d18 Employees family, education, and other fields added 2025-03-12 17:44:06 +05:30
Pranay 106171103a HR RECRUITMENT MODULE(NEW) 2025-03-12 17:24:52 +05:30
administrator 9631ef27b3 Merge pull request 'test' (#1) from test into feature/odoo18
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/1
2025-03-11 14:31:53 +05:30
raman 6667746ba8 Creation of Attendance Report 2025-03-04 12:37:12 +05:30
raman af9fe42e8a bio fix 2025-02-21 15:10:26 +05:30
raman 1fa260bb26 FIX: web icon 2025-02-21 14:44:12 +05:30
raman 3dbf149bd5 web icon 2025-02-21 12:46:24 +05:30
raman 74f41caf00 BIO metric 2025-02-13 18:47:36 +05:30
raman e8358b050b dashboard fix 2025-02-05 18:46:53 +05:30
raman 4ab920555c srcoller error 2025-01-31 14:47:48 +05:30
raman 1fb8f3b552 {FIX} :time Zone fix 2025-01-30 09:59:01 +05:30
311 changed files with 82140 additions and 1060 deletions

View File

@ -47,13 +47,13 @@ class WebManifest(http.Controller):
'scope': '/odoo',
'start_url': '/odoo',
'display': 'standalone',
'background_color': '#714B67',
'theme_color': '#714B67',
'background_color': '#017e84',
'theme_color': '#017e84',
'prefer_related_applications': False,
}
icon_sizes = ['192x192', '512x512']
manifest['icons'] = [{
'src': '/web/static/img/odoo-icon-%s.png' % size,
'src': '/web/static/img/ftp.png',
'sizes': size,
'type': 'image/png',
} for size in icon_sizes]

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

View File

@ -0,0 +1,2 @@
from . import models
from . import controllers

View File

@ -0,0 +1,50 @@
{
'name': 'FTP Custom Dashboards',
'summary': 'Advanced Dynamic Dashboards with 3D Views, Charts, and Real-time Data for Odoo 18 Community',
'category': 'Reporting',
'version': '1.0.0',
'author': 'Your Name',
'website': 'https://yourwebsite.com',
'license': 'LGPL-3',
'maintainers': ['yourgithubusername'],
'sequence': -100,
'depends': [
'base', 'web', 'hr', 'hr_recruitment_extended',
'website_hr_recruitment_extended', 'website_mail'
],
'data': [
'views/dashboard_menu.xml',
],
'assets': {
'web.assets_backend': [
# Ensure jQuery and Odoo dependencies are loaded first
'web/static/lib/jquery/jquery.js',
'web/static/src/legacy/js/public/public_widget.js',
'web/static/src/legacy/js/public/minimal_dom.js',
'web/static/src/legacy/js/core/*',
'ftp_custom_dashboards/static/src/lib/Chart/Chart.js',
'ftp_custom_dashboards/static/src/lib/d3/d3.min.js',
'ftp_custom_dashboards/static/src/lib/three/three.min.js',
# 'ftp_custom_dashboards/static/src/lib/Chart/chartjs-plugin-datalabels.js',
# Local assets (Sortable.js and Select2)
'website_hr_recruitment_extended/static/src/lib/select2/selecttwo.css',
'website_hr_recruitment_extended/static/src/js/select2_init.js',
'ftp_custom_dashboards/static/src/lib/sortable/Sortable.min.js',
'ftp_custom_dashboards/static/src/lib/interact/interact.min.js',
# Custom module assets
'ftp_custom_dashboards/static/src/js/customRecruitmentDashboard.js',
'ftp_custom_dashboards/static/src/xml/customRecruitmentDashboard.xml',
'ftp_custom_dashboards/static/src/css/customRecruitmentDashboard.css',
'ftp_custom_dashboards/static/src/dashboard_item/dashboard_item.js',
'ftp_custom_dashboards/static/src/dashboard_item/dashboard_item.xml',
],
},
'external_dependencies': {
'python': ['numpy', 'pandas'],
},
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -0,0 +1 @@
from . import controllers

View File

@ -0,0 +1,62 @@
import warnings
from datetime import datetime
from dateutil.relativedelta import relativedelta
from operator import itemgetter
from werkzeug.urls import url_encode
from odoo import http, _, fields
from odoo.addons.website_hr_recruitment.controllers.main import WebsiteHrRecruitment
from odoo.osv.expression import AND
from odoo.http import request
from datetime import timedelta
from odoo.tools import email_normalize
from odoo.tools.misc import groupby
import base64
from odoo.exceptions import UserError
from PIL import Image
from io import BytesIO
import re
import json
class website_hr_recruitment_applications(http.Controller):
@http.route('/recruitment/get_assignees', type='json', auth='user', methods=['POST'])
def get_assignees(self):
"""Fetch users who are in the 'Recruitment Interview Group'."""
group_id = request.env.ref("hr_recruitment.group_hr_recruitment_interviewer").id
assignees = request.env["res.users"].sudo().search([
("groups_id", "in", [group_id])
])
return [{"id": int(user.id), "name": user.name} for user in assignees]
@http.route('/custom_dashboard/recruitment_data', type='json', auth='user')
def recruitment_data(self, params=None):
domain = []
if params:
if params.get('date_range'):
date_range = params['date_range']
# Apply the date range filter in the domain
if date_range == "7":
domain.append(('create_date', '>=', fields.Date.today() - timedelta(days=7)))
elif date_range == "30":
domain.append(('create_date', '>=', fields.Date.today() - timedelta(days=30)))
# Add more conditions for different date ranges
if params.get('assignees'):
domain.append(('user_id', 'in', params['assignees']))
# Fetch data based on filtered domain
total_open_positions = request.env['hr.job.recruitment'].search_count(domain)
total_applications = request.env['hr.applicant'].search_count(domain)
# active_jobs = request.env['hr.job'].search_count(domain + [('state', '=', 'open')])
return {
'total_open_positions': total_open_positions,
'total_applications': total_applications,
'active_jobs': 52,
}

View File

@ -0,0 +1 @@
from . import dashboard_menu

View File

@ -0,0 +1,8 @@
# from odoo import models, fields
#
# class FTPDashboardMenu(models.Model):
# _name = 'ftp.dashboard.menu'
# _description = 'FTP Dashboard Menus'
#
# name = fields.Char(string="Menu Name", required=True)
# active = fields.Boolean(string="Active", default=True)

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ftp_dashboard_menu_manager,FTP Dashboard Menu Manager,model_ftp_dashboard_menu,base.group_user,1,1,1,1
access_ftp_dashboard_menu_user,FTP Dashboard Menu User,model_ftp_dashboard_menu,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ftp_dashboard_menu_manager FTP Dashboard Menu Manager model_ftp_dashboard_menu base.group_user 1 1 1 1
3 access_ftp_dashboard_menu_user FTP Dashboard Menu User model_ftp_dashboard_menu 1 0 0 0

View File

@ -0,0 +1,241 @@
/* General Styling */
.custom_recruitment_dashboard {
display: flex;
flex-direction: column;
height: 100vh;
font-family: 'Poppins', sans-serif; /* Modern font */
background: #f4f7fc;
width: 100%;
overflow: hidden !important;
}
.custom_recruitment_dashboard .o_dashboard_title {
font-size: 40px;
color: #3b82f6;
font-weight: bold;
background: white;
}
.custom_recruitment_dashboard .custom_recruitment_layout {
overflow: hidden !important;
}
/* Filters */
.custom_recruitment_dashboard .o_dashboard_filters {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
border-bottom: 2px solid #ddd;
background: white;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
overflow : hidden !important;
}
/* Dashboard Content */
.custom_recruitment_dashboard .o_dashboard_content {
flex-grow: 1;
display: flex;
flex-direction: column;
background-color: #FAFAFA;
padding: 10px;
height: 90%;
width: 100%;
overflow: hidden !important;
}
/* Dashboard Items Container - Horizontal Scrolling */
.custom_recruitment_dashboard #dashboard_items_container {
display: flex;
flex-wrap: nowrap; /* Prevents items from wrapping */
gap: 20px; /* Space between items */
padding: 10px;
overflow-x: auto; /* Enables horizontal scrolling */
overflow-y: hidden;
white-space: nowrap;
width: 100%;
height: 250px;
scroll-behavior: smooth; /* Smooth scrolling */
}
/* Dashboard Items */
.custom_recruitment_dashboard .dashboard-item {
background: linear-gradient(145deg, #ffffff, #e3e6f0);
border-radius: 12px;
padding: 10px;
box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.15);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 250px; /* Fixed width */
height: 150px; /* Fixed height */
transition: transform 0.2s ease-in-out, box-shadow 0.3s ease;
flex-shrink: 0; /* Prevents items from shrinking */
}
/* Add shadow when moving */
.custom_recruitment_dashboard .dashboard-item:active {
transform: scale(1.05);
box-shadow: 6px 6px 20px rgba(0, 0, 0, 0.2);
cursor: grabbing;
}
/* Inner content */
.custom_recruitment_dashboard .dashboard-content {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
/* Icons */
.custom_recruitment_dashboard .dashboard-item i {
font-size: 40px;
color: #3b82f6;
margin-bottom: 10px;
}
/* Titles */
.custom_recruitment_dashboard .dashboard-item h3 {
font-size: 1.2rem;
color: #333;
font-weight: 600;
margin: 5px 0;
}
/* Descriptions */
.custom_recruitment_dashboard .dashboard-item p {
font-size: 0.9rem;
color: #777;
user-select: none; /* Prevents text selection while dragging */
}
/* Disable text selection */
.custom_recruitment_dashboard .dashboard-item,
.dashboard-item * {
user-select: none;
}
/* Resizing Effect */
.custom_recruitment_dashboard .dashboard-item.resizing {
transition: none; /* Disable animation while resizing */
}
/* Scrollbar Styling */
.custom_recruitment_dashboard #dashboard_items_container::-webkit-scrollbar {
height: 8px;
}
.custom_recruitment_dashboard #dashboard_items_container::-webkit-scrollbar-track {
background: #ddd;
border-radius: 4px;
}
.custom_recruitment_dashboard #dashboard_items_container::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.custom_recruitment_dashboard #dashboard_items_container::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Dashboard Graph Container */
.custom_recruitment_dashboard #dashboard_graph_container {
height: calc(100vh - 180px); /* Adjust based on the space taken by header and filters */
overflow-y: auto; /* Enable scrolling only on the graph container */
overflow-x: hidden !important;
padding: 10px;
display: flex;
justify-content: space-between; /* Ensure space between charts */
align-items: stretch; /* Make sure both charts stretch to fit container height */
gap: 10px; /* Reduced space between charts */
margin-bottom: 5%; /* Add margin to separate the charts vertically when stacked */
}
/* General Styling for Chart Section */
.custom_recruitment_dashboard .submission_status_pie {
width: 39%; /* Slightly reduce width to ensure both charts fit properly side by side */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 20px; /* Add margin to separate the charts vertically when stacked */
flex-grow: 1; /* Allow sections to grow and take up remaining space */
}
.custom_recruitment_dashboard .candidate_pipeline_dashboard {
width: 59%; /* Slightly reduce width to ensure both charts fit properly side by side */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 20px; /* Add margin to separate the charts vertically when stacked */
flex-grow: 1; /* Allow sections to grow and take up remaining space */
}
.custom_recruitment_dashboard .job_applications_bar {
width: 89%; /* Slightly reduce width to ensure both charts fit properly side by side */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 20px; /* Add margin to separate the charts vertically when stacked */
flex-grow: 1;
}
/* Ensure the charts fit well within the section */
.custom_recruitment_dashboard .chart-container {
width: 100%; /* Take full width of the chart section */
padding: 10px;
box-sizing: border-box; /* Include padding in the width calculation */
display: flex;
justify-content: center;
}
/* Ensure charts fit properly in the container */
.custom_recruitment_dashboard canvas#submissionStatusPieChart,
.custom_recruitment_dashboard canvas#applicationsPerJobBarChart,
.custom_recruitment_dashboard canvas#candidatePipelineBarChart {
width: 100% !important;
height: auto !important;
max-width: 100%; /* Prevent charts from overflowing */
max-height: 300px; /* Optional: set max height to avoid too large charts */
}
/* Ensure proper responsiveness for smaller screens */
@media (max-width: 1024px) {
.custom_recruitment_dashboard #dashboard_graph_container {
padding: 10px;
flex-direction: column; /* Stack charts vertically on smaller screens */
align-items: center;
overflow-y: auto;
}
.custom_recruitment_dashboard .chart-section {
width: 100%; /* Full width on smaller screens */
margin-bottom: 20px; /* Space between charts */
}
.custom_recruitment_dashboard .chart-container {
width: 90%; /* Reduce width to prevent overflow on smaller screens */
}
/* Ensure charts don't overflow horizontally */
.custom_recruitment_dashboard canvas {
width: 100% !important;
height: auto !important;
}
}

View File

@ -0,0 +1,17 @@
import { Component } from "@odoo/owl";
export class DashboardItem extends Component {
static template = "DashboardItemTemplate";
static props = {
icon: String,
name: String,
count: Number,
action: Function, // Ensure action is a function
};
onClick() {
if (this.props.action) {
this.props.action(); // Call the function when clicked
}
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="DashboardItemTemplate">
<div class="stat-card" t-on-click="onClick">
<div class="stat-icon">
<i t-att-class="props.icon"/>
</div>
<div class="stat-content">
<p>
<t t-esc="props.name"/>
</p>
<h2>
<t t-esc="props.count"/>
</h2>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,954 @@
/** @odoo-module **/
/* global ChartDataLabels */
import { Component, useState, onWillStart, onMounted, useRef } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { rpc } from "@web/core/network/rpc";
import { Layout } from "@web/search/layout";
import { DashboardItem } from "../dashboard_item/dashboard_item";
//import Sortable from "../lib/sortable/Sortable.min.js";
//import interact from "../lib/interact/interact.min.js";
import Chart from "../lib/Chart/Chart.js";
//import ChartDataLabels from "../lib/Chart/chartjs-plugin-datalabels.js";
import d3 from "../lib/d3/d3.min.js";
import THREE from "../lib/three/three.min.js";
export class CreateRecruitmentDashboard extends Component {
static template = "Custom_Recruitment_Dashboard_Template";
static components = { Layout, DashboardItem };
static props = {
date_from: { type: String, optional: true },
date_to: { type: String, optional: true },
assignees: { type: Array, optional: true },
selectedRecruitmentType: { type: String, optional: true },
selectedAssignees: { type: Array, optional: true },
jobRequests: { type: Array, optional: true },
filteredJobRequests: { type: Array, optional: true },
applicantsData: { type: Array, optional: true },
filteredApplicantsData: { type: Array, optional: true },
dashboardItems: { type: Array, optional: true },
jobPositions: { type: Array, optional: true},
selectedPositions: { type: Array, optional: true },
action: { type: Object, optional: true },
actionId: { type: Number, optional: true },
updateActionState: { type: Function, optional: true },
className: { type: String, optional: true },
globalState: { type: Object, optional: true },
isPublished: { type: Boolean, optional: true },
recruitmentStages: { type: Object, optional: true },
};
setup() {
super.setup();
this.orm = useService("orm");
this.action = useService("action");
this.canvasRef = useRef("chartCanvas");
this.submissionChart = null;
this.jobApplicationsBarChart = null;
this.pipelineCandidateBarChart = null;
const storedFilters = JSON.parse(localStorage.getItem("recruitmentDashboardFilters")) || {};
this.state = useState({
date_from: storedFilters.date_from || this.props.date_from || null,
date_to: storedFilters.date_to || this.props.date_to || null,
assignees: this.props.assignees || [],
selectedRecruitmentType: storedFilters.selectedRecruitmentType || this.props.selectedRecruitmentType || 'external',
selectedAssignees: storedFilters.selectedAssignees || this.props.selectedAssignees || [],
jobRequests: this.props.jobRequests || [],
jobPositions: this.props.jobPositions || [],
selectedPositions: storedFilters.selectedPositions || this.props.selectedPositions || [],
filteredJobRequests: this.props.filteredJobRequests || [],
applicantsData: this.props.applicantsData || [],
filteredApplicantsData: this.props.filteredApplicantsData || [],
recruitmentStages: this.props.recruitmentStages || [],
isPublished: storedFilters.isPublished || this.props.isPublished || false,
dashboardItems: [],
});
this.display = { controlPanel: {} };
onWillStart(async () => {
await this.loadInitialData();
// this.applyStoredFilters();
});
onMounted(async () => {
await this.initializeUI();
this.renderAllDashboards();
this.onDataFilterApply();
this.render();
console.log(this.state);
});
}
renderAllDashboards() {
this.renderSubmissionStatusPieChart();
this.renderSubmissionStatusBarChart();
this.renderCandidatePipeLineBarChart();
}
destroyCharts() {
if (this.submissionChart) {
this.submissionChart.destroy();
this.submissionChart = null;
}
if (this.jobApplicationsBarChart) {
this.jobApplicationsBarChart.destroy();
this.jobApplicationsBarChart = null;
}
if (this.pipelineCandidateBarChart) {
this.pipelineCandidateBarChart.destroy();
this.pipelineCandidateBarChart = null;
}
}
renderSubmissionStatusPieChart() {
const ctx = document.getElementById("submissionStatusPieChart");
if (!ctx) return;
// Ensure the old chart is destroyed before creating a new one
if (this.submissionChart) {
this.submissionChart.destroy();
this.submissionChart = null;
}
const jobRequests = this.state.filteredJobRequests;
// Categorize job requests
let filledSubmissions = [];
let partialSubmissions = [];
let zeroSubmissions = [];
jobRequests.forEach(jr => {
if (jr.no_of_submissions >= jr.no_of_recruitment) {
filledSubmissions.push(jr);
} else if (jr.no_of_submissions > 0 && jr.no_of_submissions < jr.no_of_recruitment) {
partialSubmissions.push(jr);
} else {
zeroSubmissions.push(jr);
}
});
// Define chart data
const labels = ["Zero Submissions", "Partial Submissions", "Filled Submissions"];
const data = [zeroSubmissions.length, partialSubmissions.length, filledSubmissions.length];
const colors = ["#E74C3C", "#F4C542", "#2ECC71"]; // Red, Yellow, Green
this.submissionChart = new window.Chart(ctx, {
type: "pie",
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors
}]
},
options: {
responsive: true,
plugins: {
tooltip: {
callbacks: {
label: (tooltipItem) => {
let category;
if (tooltipItem.dataIndex === 0) {
category = zeroSubmissions;
} else if (tooltipItem.dataIndex === 1) {
category = partialSubmissions;
} else {
category = filledSubmissions;
}
if (category.length === 0) return "No Data";
return category.map(jr =>
`Seq: ${jr.recruitment_sequence}, Job: ${jr.job_id[1]}, Req: ${jr.no_of_recruitment}, Sub: ${jr.no_of_submissions}`
);
}
}
},
},
elements: {
arc: {
borderWidth: 0, // Remove borders to enhance 3D effect
backgroundColor: function (context) {
var index = context.dataIndex;
return colors[index];
},
// Add shadow effect to give depth to the slices
shadowOffsetX: 4,
shadowOffsetY: 4,
shadowBlur: 10,
shadowColor: "rgba(0, 0, 0, 0.3)",
}
},
animation: {
animateScale: true, // Enable scaling for the 3D-like effect
animateRotate: true, // Enable rotation to make it more dynamic
},
cutoutPercentage: 0, // Create a fully filled pie chart without a hole in the middle (to look more like a globe)
rotation: 0.5 * Math.PI, // Rotate the pie chart slightly
responsive: true,
maintainAspectRatio: false,
onClick: (event, elements) => {
if (elements.length) {
const index = elements[0].index;
let name = null;
let categories = index === 0 ? zeroSubmissions : index === 1 ? partialSubmissions : filledSubmissions;
const categoryIds = categories.map(category => category.id);
if (index === 0 && categoryIds.length > 0) {
name = 'Zero Submissions';
}
else if (index === 1 && categoryIds.length > 0) {
name = 'Partial Submissions';
}
else {
name = 'Filled Submissions';
}
this.viewPieChartJobRequests(categoryIds, name);
}
}
}
});
}
viewPieChartJobRequests(jobIds, ActionName) {
this.destroyCharts();
this.action.doAction({
type: "ir.actions.act_window",
name: ActionName,
res_model: "hr.job.recruitment",
domain: [["id", "in", jobIds]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
renderCandidatePipeLineBarChart() {
const ctx = document.getElementById("candidatePipelineBarChart");
if (!ctx) return;
if (this.pipelineCandidateBarChart) {
this.pipelineCandidateBarChart.destroy();
this.pipelineCandidateBarChart = null;
}
const applicantsData = this.state.filteredApplicantsData;
const recruitmentStages = this.state.recruitmentStages || [];
const jobRequests = this.state.filteredJobRequests || [];
const stageDetails = {};
const stageCandidateCount = {};
// ➡️ Filtered data based on application_status
const filteredApplicants = applicantsData.filter(
applicant => applicant.application_status !== 'refused' &&
applicant.application_status !== 'archived'
);
// ➡️ Separate refused applicants for a new stage
const refusedApplicants = applicantsData.filter(
applicant => applicant.application_status === 'refused'
);
// Process accepted applicants
filteredApplicants.forEach(applicant => {
const stages = applicant.recruitment_stage_id;
if (Array.isArray(stages) && stages[1]) {
const stage = stages[1];
if (!stageDetails[stage]) {
stageDetails[stage] = {};
stageCandidateCount[stage] = 0;
}
stageCandidateCount[stage] += 1;
const jobId = applicant.hr_job_recruitment?.[0];
if (jobId) {
const jobRequest = jobRequests.find(jr => jr.id === jobId);
if (jobRequest) {
const jobKey = `${jobRequest.recruitment_sequence}_${jobRequest.job_id?.[1] || 'Unknown Job'}`;
if (!stageDetails[stage][jobKey]) {
stageDetails[stage][jobKey] = {
seq: jobRequest.recruitment_sequence,
job: jobRequest.job_id?.[1] || 'Unknown Job',
req: jobRequest.no_of_recruitment || 0,
sub: jobRequest.no_of_submissions || 0,
count: 0
};
}
stageDetails[stage][jobKey].count += 1;
}
}
}
});
// ➡️ Add Refused applicants as a separate stage
if (refusedApplicants.length > 0) {
stageCandidateCount['Refused'] = refusedApplicants.length;
stageDetails['Refused'] = refusedApplicants.map(applicant => ({
applicant: applicant.candidate_id?.[1] || 'N/A',
seq: applicant.hr_job_recruitment?.[1] ||'N/A',
job: applicant.job_id?.[1] || 'Unknown Job',
stage: applicant.recruitment_stage_id?.[1] || 'N/A' // ➡️ New Field to Indicate Refused Stage
}));
}
// Sorting stages
const sortedStages = recruitmentStages.sort((a, b) => a.sequence - b.sequence);
const datasets = [
...sortedStages.map(stage => ({
label: stage.name,
stage_id: stage.id,
data: [stageCandidateCount[stage.name] || 0],
backgroundColor: stage.stage_color,
borderColor: stage.stage_color,
borderWidth: 1
})),
// ➡️ Add Refused stage dataset
{
label: 'Refused',
stage_id: null,
data: [stageCandidateCount['Refused'] || 0],
backgroundColor: '#E74C3C', // Red color for refused
borderColor: '#C0392B',
borderWidth: 1
}
];
this.pipelineCandidateBarChart = new window.Chart(ctx, {
type: 'bar',
data: {
labels: ['Candidates'],
datasets: datasets
},
options: {
plugins: {
legend: { display: true, position: 'top' },
tooltip: {
callbacks: {
title: function (tooltipItems) {
const stageName = datasets[tooltipItems[0].datasetIndex].label;
const totalCount = stageCandidateCount[stageName] || 0;
return `Total Candidates: ${totalCount}`;
},
label: function (tooltipItem) {
const stageName = datasets[tooltipItem.datasetIndex].label;
const details = Object.values(stageDetails[stageName] || {});
if (details.length === 0) return "No Data";
let tooltipText;
if (stageName === 'Refused') {
tooltipText = details.map(detail =>
`Seq: ${detail.applicant}, Job: ${detail.job}, Stage: ${detail.stage}`
);
} else {
tooltipText = details.map(detail =>
`Seq: ${detail.seq}, Job: ${detail.job}, Req: ${detail.req}, Sub: ${detail.sub}, Count: ${detail.count}`
);
}
return tooltipText;
}
}
}
},
onClick: (event, elements) => {
if (elements.length > 0) {
const datasetIndex = elements[0].datasetIndex;
const stageName = datasets[datasetIndex].label;
const selectedStageDetails = Object.values(stageDetails[stageName] || {});
if (selectedStageDetails.length) {
const jobSeqs = selectedStageDetails.map(detail => detail.seq);
const jobIds = jobRequests
.filter(job => jobSeqs.includes(job.recruitment_sequence))
.map(details => details.id);
// const refusedJobIds = jobRequests.filter(job=> job.id === details.seq).map(details => details.id);
debugger;
this.destroyCharts();
this.action.doAction({
type: "ir.actions.act_window",
name: 'Applications',
res_model: "hr.applicant",
domain: stageName === 'Refused'
? [["hr_job_recruitment", "in", jobIds],["application_status", "=", "refused"],'|', ['active','=',false],['active','=',true]]
: [["hr_job_recruitment", "in", jobIds], ['recruitment_stage_id', '=', datasets[datasetIndex].stage_id]],
view_mode: "kanban,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
}
},
responsive: true,
maintainAspectRatio: false
}
});
}
renderSubmissionStatusBarChart() {
const ctx = document.getElementById("applicationsPerJobBarChart");
if (!ctx) return;
if (this.jobApplicationsBarChart) {
this.jobApplicationsBarChart.destroy();
this.jobApplicationsBarChart = null;
}
const jobRequests = this.state.filteredJobRequests;
// Group by user_id
const groupedByUser = {};
jobRequests.forEach(jr => {
const userId = jr.user_id[0];
const userName = jr.user_id[1];
if (!groupedByUser[userId]) {
groupedByUser[userId] = {
userName,
jobRequests: []
};
}
groupedByUser[userId].jobRequests.push(jr);
});
// Labels - Users
const userLabels = Object.values(groupedByUser).map(user => user.userName);
// Data arrays for each category
const requirementsData = [];
const applicationsData = [];
const submissionsData = [];
const rejectionsData = [];
const hiredData = [];
const tooltipsData = {};
userLabels.forEach(userName => {
const user = Object.values(groupedByUser).find(user => user.userName === userName);
let totalRequirements = 0, totalApplications = 0, totalSubmissions = 0, totalRejections = 0, totalHired = 0;
let jobDetails = [];
user.jobRequests.forEach(jr => {
totalRequirements += jr.no_of_recruitment;
totalApplications += jr.application_count;
totalSubmissions += jr.no_of_submissions;
totalRejections += jr.no_of_refused_submissions;
totalHired += jr.no_of_hired_employee;
jobDetails.push(`Seq: ${jr.recruitment_sequence}, Job: ${jr.job_id[1]}, Requirements: ${jr.no_of_recruitment}, Applications: ${jr.application_count}, submissions: ${jr.no_of_submissions}, Rejections: ${jr.no_of_refused_submissions}, Hired: ${jr.no_of_hired_employee}`);
});
requirementsData.push(totalRequirements);
applicationsData.push(totalApplications);
submissionsData.push(totalSubmissions);
rejectionsData.push(totalRejections);
hiredData.push(totalHired);
tooltipsData[userName] = jobDetails;
});
// Create Chart
this.jobApplicationsBarChart = new window.Chart(ctx, {
type: "bar",
data: {
labels: userLabels,
datasets: [
{
label: "Requirements",
data: requirementsData,
backgroundColor: "#9B59B6", // Purple
borderColor: "#8E44AD",
borderWidth: 1
},
{
label: "Applications",
data: applicationsData,
backgroundColor: "#3498DB", // Blue
borderColor: "#2980B9",
borderWidth: 1
},
{
label: "Submissions",
data: submissionsData,
backgroundColor: "#F4C542", // Yellow
borderColor: "#D4AC0D",
borderWidth: 1
},
{
label: "Rejections",
data: rejectionsData,
backgroundColor: "#E74C3C", // Red
borderColor: "#C0392B",
borderWidth: 1
},
{
label: "Hired",
data: hiredData,
backgroundColor: "#2ECC71", // Green
borderColor: "#27AE60",
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: false,
title: { display: true, text: "Recruiters (User ID)" }
},
y: {
stacked: false,
title: { display: true, text: "Count" },
beginAtZero: true
}
},
plugins: {
tooltip: {
callbacks: {
label: (tooltipItem) => {
const datasetLabel = tooltipItem.dataset.label;
const value = tooltipItem.raw;
const userName = tooltipItem.label;
const jobDetails = tooltipsData[userName] || [];
if (jobDetails.length === 0) return "No Data";
let tooltipText = jobDetails.map(jr =>
`${jr}`
);
tooltipText.unshift(`${datasetLabel}: ${value}`);
return tooltipText;
}
}
}
}
}
});
}
async loadInitialData() {
try {
this.state.recruitmentStages = await this.fetchRecruitmentStages();
await this.fetchAssignees();
const jobRequestsData = await this.jobRequestsData();
this.state.jobRequests = jobRequestsData || [];
this.state.jobPositions = Object.values(jobRequestsData.reduce((acc, job) => {
const jobId = job.job_id[0]; // Extracting the job ID
const jobName = job.job_id[1]; // Extracting the job name
if (!acc[jobId]) {
acc[jobId] = {
id: jobId,
name: jobName,
jobs: [],
};
}
acc[jobId].jobs.push(job);
return acc;
}, {}));
this.state.filteredJobRequests = this.state.jobRequests.filter(job => job.recruitment_type === this.state.selectedRecruitmentType);
const jobApplicants = await this.jobApplicantsData();
this.state.applicantsData = jobApplicants || [];
debugger;
this.state.filteredApplicantsData = jobApplicants || [];
let dashboardData = await this.fetchDashboardData() || [];
this.state.dashboardItems = dashboardData;
this.onDataFilterApply();
} catch (error) {
console.error("Error loading initial data:", error);
}
}
async initializeUI() {
await this._renderAssigneesSelect2();
this._bindSelect2Event();
this._getSelectedData();
}
async jobApplicantsData() {
try {
const applicantsData = await this.orm.searchRead(
"hr.applicant",
["|", ["active", "=", true], ["active", "=", false]],
["id","recruitment_stage_id","candidate_id","hr_job_recruitment","job_id","refused_state","employee_id","submitted_to_client","stage_color","active","application_status"]
);
return applicantsData;
} catch (error) {
console.error("Error fetching Applicants data:", error);
return [];
}
}
async jobRequestsData() {
try {
const recruitmentData = await this.orm.searchRead(
"hr.job.recruitment",
[],
["id","job_id","department_id","no_of_recruitment","application_count","no_of_hired_employee","no_of_submissions","no_of_refused_submissions","recruitment_sequence","recruitment_type","requested_by","user_id","is_published","target_from","target_to","application_ids"]
);
return recruitmentData;
} catch (error) {
console.error("Error fetching job Requests data:", error);
return [];
}
}
async fetchRecruitmentStages() {
try {
const recruitmentData = await this.orm.searchRead(
"hr.recruitment.stage",
[],
["id","sequence","name","stage_color"]
);
return recruitmentData;
} catch (error) {
console.error("Error fetching job Requests data:", error);
return [];
}
}
async computedTotalPublishedRequirements() {
return this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_recruitment, 0); // Sum up no_of_recruitment
}
async computedTotalApplicants() {
return this.state.filteredJobRequests
.reduce((sum, job) => sum + job.application_count, 0); // Sum up no_of_recruitment
}
async computeActiveJobRequests() {
return this.state.filteredJobRequests
.reduce((sum, job) => sum + (job.is_published ? 1 : 0), 0); // Sum up no_of_recruitment
}
async computeTotalFilledRequests() {
return this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_hired_employee, 0);
}
async computeTotalSubmissions() {
return this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_submissions, 0);
}
async computeTotalRefusedSubmissions() {
return this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_refused_submissions, 0);
}
async computeOfferAcceptanceRate() {
const totalSubmissions = this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_submissions, 0);
const totalHired = this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_hired_employee, 0);
// Avoid division by zero
if (totalSubmissions === 0) {
return 0; // If there are no submissions, return 0% acceptance rate
}
return (totalHired / totalSubmissions) * 100;
}
async computeDropOutRate() {
const totalSubmissions = this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_submissions, 0);
const totalRefusals = this.state.filteredJobRequests
.reduce((sum, job) => sum + job.no_of_refused_submissions, 0);
// Avoid division by zero
if (totalSubmissions === 0) {
return 0; // If there are no submissions, return 0% acceptance rate
}
return (totalRefusals / totalSubmissions) * 100;
}
async fetchDashboardData() {
try {
return [
{ icon: "fa fa fa-clock-o", name: "Total Positions", count: await this.computedTotalPublishedRequirements() || 0, action: () => this.viewOpenPositions() }, // Clipboard list for positions
{ icon: "fa fa-users", name: "Total Applications", count: await this.computedTotalApplicants() || 0, action: () => this.viewApplications() }, // File icon for applications
{ icon: "fa fa-briefcase", name: "Active Job Requests", count: await this.computeActiveJobRequests() || 0, action: () => this.viewActiveJobRequests() }, // Briefcase for active jobs
{ icon: "fa fa-check-circle", name: "Positions Filled", count: await this.computeTotalFilledRequests() || 0, action: () => this.viewHiredApplications() }, // Users cog for filled positions
{ icon: "fa fa-paper-plane", name: "Submitted Applications", count: await this.computeTotalSubmissions() || 0, action: () => this.viewSubmittedApplications() }, // User check for submitted applications
{ icon: "fa fa-user-times", name: "Refused Submissions", count: await this.computeTotalRefusedSubmissions() || 0, action: () => this.viewRefusedSubmittedApplications() }, // User times for refused submissions
{ icon: "fa fa-check-circle", name: "Offer Acceptance Rate", count: `${(await this.computeOfferAcceptanceRate()).toFixed(2)}%` || "0%", action: () => this.viewHiredApplications() }, // Check circle for offer acceptance
{ icon: "fa fa-times-circle", name: "Refusal Rate", count: `${(await this.computeDropOutRate()).toFixed(2)}%` || "0%", action: () => this.viewRefusedSubmittedApplications() }, // Times circle for refusal rate
];
} catch (error) {
console.error("Error fetching dashboard data:", error);
return []; // Return an empty array in case of failure
}
}
viewOpenPositions() {
const jobRequestIds = this.state.filteredJobRequests.map(job => job.id);
this.action.doAction({
type: "ir.actions.act_window",
name: "Open Positions",
res_model: "hr.job.recruitment",
domain: [['id','in',jobRequestIds]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
viewHiredApplications() {
const jobRequestIds = this.state.filteredJobRequests.map(job => job.id);
this.action.doAction({
type: "ir.actions.act_window",
name: "Hired Applications",
res_model: "hr.applicant",
domain: ["|", ["active", "=", true], ["active", "=", false], ["hr_job_recruitment.id", "in", jobRequestIds], ["recruitment_stage_id.hired_stage", "=", true]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
viewApplications() {
const jobRequestIds = this.state.filteredJobRequests.map(job => job.id);
this.action.doAction({
type: "ir.actions.act_window",
name: "Applications",
res_model: "hr.applicant",
domain: ["|", ["active", "=", true], ["active", "=", false], ["hr_job_recruitment.id", "in", jobRequestIds]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
viewSubmittedApplications() {
const jobRequestIds = this.state.filteredJobRequests.map(job => job.id);
this.action.doAction({
type: "ir.actions.act_window",
name: "Submitted Applications",
res_model: "hr.applicant",
domain: ["|", ["active", "=", true], ["active", "=", false], ["hr_job_recruitment.id", "in", jobRequestIds], ["submitted_to_client", "=", true]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
viewRefusedSubmittedApplications() {
const jobRequestIds = this.state.filteredJobRequests.map(job => job.id);
this.action.doAction({
type: "ir.actions.act_window",
name: "Refused Submissions",
res_model: "hr.applicant",
domain: ["|", ["active", "=", true], ["active", "=", false], ["hr_job_recruitment.id", "in", jobRequestIds], ["submitted_to_client", "=", true] ,["application_status", "=", "refused"]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
viewActiveJobRequests() {
const jobRequestIds = this.state.filteredJobRequests.map(job => job.id);
this.action.doAction({
type: "ir.actions.act_window",
name: "Active Jobs",
res_model: "hr.job.recruitment",
domain: [["is_published", "=", true],["id", "in", jobRequestIds]],
view_mode: "kanban,list,form",
views: [[false, "list"], [false, "form"]],
target: "current",
});
}
openJobRequisitionsPage() {
this.action.doAction("hr_recruitment_extended.action_hr_job_recruitment");
}
async _renderAssigneesSelect2() {
// Load Select2 manually if not already loaded
if (!$.fn.select2) {
console.warn("Select2 not found! Loading manually...");
$.getScript("/website_hr_recruitment_extended/static/src/lib/select2/select2.main.js", function() {
$("#assignees_select").select2();
$('#positions_select').select2();
});
} else {
$("#assignees_select").select2();
$("#positions_select").select2();
}
}
async fetchAssignees() {
try {
const assignees = await rpc("/recruitment/get_assignees");
this.state.assignees = assignees;
} catch (error) {
console.error("Error fetching assignees:", error);
}
}
async fetchApplicantData() {
try {
const assignees = await rpc("/recruitment/get_assignees");
this.state.assignees = assignees;
} catch (error) {
console.error("Error fetching assignees:", error);
}
}
onDateRangeChange(ev) {
this.state.selectedDateRange = ev.target.value;
this.fetchDashboardData(); // Refresh data
}
_bindSelect2Event() {
$("#assignees_select").on("change", (event) => {
const selectedValues = $("#assignees_select").val(); // Get selected values
this.state.selectedAssignees = selectedValues ? selectedValues.map(id => parseInt(id)) : [];
});
$("#positions_select").on("change", (event) => {
const selectedValues = $("#positions_select").val(); // Get selected values
this.state.selectedPositions = selectedValues ? selectedValues.map(id => parseInt(id)) : [];
});
}
_getSelectedData() {
$("#recruitment_type").on("change", (event) => {
this.state.selectedRecruitmentType = $("#recruitment_type").val();
})
}
async onDataFilterApply() {
if (this.state.date_from || this.state.date_to || this.state.selectedAssignees || this.state.selectedPositions) {
let filteredJobRequests = [...this.state.jobRequests];
const dateFrom = this.state.date_from ? new Date(this.state.date_from) : null;
const dateTo = this.state.date_to ? new Date(this.state.date_to) : null;
const assignees = this.state.selectedAssignees ? this.state.selectedAssignees : null;
const recruitmentType = this.state.selectedRecruitmentType ? this.state.selectedRecruitmentType : null;
const positions = this.state.selectedPositions ? this.state.selectedPositions: null;
const is_published = this.state.isPublished ? this.state.isPublished: false;
// Filter by date_from
if (is_published) {
filteredJobRequests = filteredJobRequests.filter(job => {
return job.is_published === true
});
}
if (dateFrom) {
filteredJobRequests = filteredJobRequests.filter(job => {
const targetFrom = job.target_from ? new Date(job.target_from) : null;
return targetFrom && targetFrom >= dateFrom; // Return boolean value for filtering
});
}
// Filter by date_to
if (dateTo) {
filteredJobRequests = filteredJobRequests.filter(job => {
const targetTo = job.target_to ? new Date(job.target_to) : null;
return targetTo && targetTo <= dateTo; // Return boolean value for filtering
});
}
// Filter by assignees
if (assignees && assignees.length > 0) {
filteredJobRequests = filteredJobRequests.filter(job => {
const assignedUser = job.user_id; // Assuming `job.user_id` is the user assigned to the job
return assignedUser && assignees.includes(assignedUser[0]); // Check if user is in the assignees list
});
}
if (recruitmentType) {
filteredJobRequests = filteredJobRequests.filter(job => {
return job.recruitment_type === recruitmentType
})
}
if (positions && positions.length > 0) {
filteredJobRequests = filteredJobRequests.filter(job => {
const jobPosition = job.job_id; // Assuming `job.user_id` is the user assigned to the job
return jobPosition && positions.includes(jobPosition[0]); // Check if user is in the assignees list
});
}
const filteredApplicantsData = this.state.applicantsData.filter(applicant => {
// Check if hr_job_recruitment[0] (the first element of the hr_job_recruitment array) exists in any job's ids
return filteredJobRequests.some(job => filteredJobRequests.map(job => job.id) && filteredJobRequests.map(job => job.id).includes(applicant.hr_job_recruitment[0]));
});
// Save filters to localStorage
localStorage.setItem("recruitmentDashboardFilters", JSON.stringify({
date_from: this.state.date_from || null,
date_to: this.state.date_to || null,
selectedAssignees: this.state.selectedAssignees || [],
selectedRecruitmentType: this.state.selectedRecruitmentType || null,
selectedPositions: this.state.selectedPositions || [],
isPublished: this.state.isPublished || false,
}));
// Update the state with the filtered job requests
Object.assign(this.state, {
filteredApplicantsData: filteredApplicantsData,
filteredJobRequests: filteredJobRequests,
dashboardItems: await this.fetchDashboardData() || [],
});
this.renderAllDashboards();
// ✅ Use `setState` to update state properly
this.render();
}
}
reorderDashboardItems(evt) {
const newOrder = Array.from(evt.from.children).map(el => el.getAttribute("data-item-name"));
// Update the state
this.state.dashboardItems.sort((a, b) => newOrder.indexOf(a.name) - newOrder.indexOf(b.name));
}
onTogglePublished() {
this.state.isPublished = !this.state.isPublished;
this.render()
}
}
registry.category("actions").add("createRecruitmentDashboard", CreateRecruitmentDashboard);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { _t } from "@web/core/l10n/translation";
publicWidget.registry.Select2Widget = publicWidget.Widget.extend({
selector: "select.o_select2", // Apply to select elements with this class
start: function () {
this._super.apply(this, arguments);
// Ensure jQuery and Select2 are loaded
if (typeof jQuery !== "undefined" && $.fn.select2) {
this.$el.select2({
width: "100%",
placeholder: _t("Select an option"),
allowClear: true,
});
} else {
console.error("Error: jQuery or Select2 is not loaded properly.");
}
},
});
export default publicWidget.registry.Select2Widget;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,129 @@
<templates xml:space="preserve">
<t t-name="Custom_Recruitment_Dashboard_Template">
<div class="custom_recruitment_dashboard">
<div class="custom_recruitment_layout">
<link rel="stylesheet" type="text/css"
href="/ftp_custom_dashboards/static/src/lib/select2/select2.css"/>
<script type="text/javascript"
src="/website_hr_recruitment_extended/static/src/lib/select2/select2.main.js"/>
<script type="text/javascript"
src="/ftp_custom_dashboards/static/src/lib/sortable/Sortable.min.js"/>
<script type="text/javascript"
src="/ftp_custom_dashboards/static/src/lib/interact/interact.min.js"/>
<script type="text/javascript"
src="/ftp_custom_dashboards/static/src/lib/three/three.min.js"/>
<script type="text/javascript"
src="/ftp_custom_dashboards/static/src/lib/d3/d3.min.js"/>
<!-- Filters Container -->
<div class="o_dashboard_title">
<span>Analysis</span>
</div>
<div class="o_dashboard_filters">
<!-- Date Range Selection -->
<div class="d-flex align-items-center">
<label for="date_from" class="me-2">From:</label>
<input type="date" class="o_input" id="date_from" t-model="state.date_from"/>
<label for="date_to" class="ms-3 me-2">To:</label>
<input type="date" class="o_input" id="date_to" t-model="state.date_to"/>
</div>
<!-- Assignee Selection -->
<select class="select2" id="assignees_select" multiple="multiple" t-on-change="onAssigneeChange">
<t t-foreach="state.assignees" t-as="user" t-key="user.id">
<t t-if="state.selectedAssignees.includes(user.id)">
<option t-att-value="user.id" selected="selected">
<t t-esc="user.name"/>
</option>
</t>
<t t-else="">
<option t-att-value="user.id">
<t t-esc="user.name"/>
</option>
</t>
</t>
</select>
<select class="select2" id="positions_select" multiple="multiple">
<t t-foreach="state.jobPositions" t-as="position" t-key="position.id">
<t t-if="state.selectedPositions.includes(position.id)">
<option t-att-value="position.id" selected="selected">
<t t-esc="position.name"/>
</option>
</t>
<t t-else="">
<option t-att-value="position.id">
<t t-esc="position.name"/>
</option>
</t>
</t>
</select>
<select class="form-control" name="recruitment_type" id="recruitment_type" required="1">
<option value="external" selected="'selected' if state.selectedRecruitmentType === 'external' else None">External</option>
<option value="internal" selected="'selected' if state.selectedRecruitmentType === 'internal' else None">Internal</option>
</select>
<div class="ms-3 d-flex align-items-center">
<label class="me-2">Show Only Published:</label>
<button class="btn toggle-btn"
t-att-class="state.isPublished ? 'btn-success' : 'btn-light'"
t-on-click="onTogglePublished">
<t t-esc="state.isPublished ? 'OFF' : 'ON'"/>
</button>
</div>
<button class="btn btn-primary ms-3" t-on-click="onDataFilterApply">Filter</button>
</div>
<!-- Dashboard Content -->
<div class="o_dashboard_content">
<div id="dashboard_items_container" class="dashboard_items_container">
<t t-if="state.dashboardItems">
<t t-foreach="state.dashboardItems" t-as="item" t-key="item.name">
<div class="dashboard-item resizable draggable"
t-att-data-item-name="item.name"
t-on-click="() => item.action()">
<div class="dashboard-content">
<i t-att-class="item.icon"></i>
<h3><t t-esc="item.name"/></h3>
<p><t t-esc="item.count"/> Applications</p>
</div>
</div>
</t>
</t>
</div>
<div id="dashboard_graph_container" class="dashboard_graph_container">
<div class="chart-section mt-4 submission_status_pie">
<h3 class="text-center">Job Submission Status Overview</h3>
<div class="chart-container">
<canvas id="submissionStatusPieChart" width="600" height="600"></canvas> <!-- Increased size -->
</div>
</div>
<div class="chart-section mt-4 candidate_pipeline_dashboard">
<h3 class="text-center">Candidate Pipeline Dashboard</h3>
<!-- Chart Container -->
<div class="chart-container">
<canvas id="candidatePipelineBarChart"></canvas>
</div>
</div>
<div class="chart-section mt-4 job_applications_bar">
<h3 class="text-center">Recruiter Analysis</h3>
<!-- Chart Container -->
<div class="chart-container">
<canvas id="applicationsPerJobBarChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,44 @@
<odoo>
<!-- <record id="ftp_dashboard_menu_view_list" model="ir.ui.view">-->
<!-- <field name="name">ftp.dashboard.menu.list</field>-->
<!-- <field name="model">ftp.dashboard.menu</field>-->
<!-- <field name="arch" type="xml">-->
<!-- <list editable="top">-->
<!-- <field name="name"/>-->
<!-- <field name="active"/>-->
<!-- </list>-->
<!-- </field>-->
<!-- </record>-->
<!-- <record id="ftp_dashboard_menu_view_form" model="ir.ui.view">-->
<!-- <field name="name">ftp.dashboard.menu.form</field>-->
<!-- <field name="model">ftp.dashboard.menu</field>-->
<!-- <field name="arch" type="xml">-->
<!-- <form>-->
<!-- <sheet>-->
<!-- <group>-->
<!-- <field name="name"/>-->
<!-- <field name="active"/>-->
<!-- </group>-->
<!-- </sheet>-->
<!-- </form>-->
<!-- </field>-->
<!-- </record>-->
<!-- <record id="ftp_dashboard_menu_action" model="ir.actions.act_window">-->
<!-- <field name="name">Dashboard Menus</field>-->
<!-- <field name="res_model">ftp.dashboard.menu</field>-->
<!-- <field name="view_mode">list,form</field>-->
<!-- </record>-->
<record id="custom_recruitment_ftp_dashboard_action" model="ir.actions.client">
<field name="name">FTP Recruitment Dashboards</field>
<field name="tag">createRecruitmentDashboard</field>
</record>
<menuitem id="ftp_dashboards_menu" name="FTP Dashboards" action='custom_recruitment_ftp_dashboard_action' parent="hr_recruitment.menu_hr_recruitment_root" sequence="12" />
</odoo>

View File

@ -12,7 +12,6 @@
'website': "https://www.ftprotech.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Human Resources/Attendances',
'version': '0.1',
@ -25,7 +24,20 @@
'data': [
'security/ir.model.access.csv',
'data/cron.xml',
'views/hr_attendance.xml'
'views/hr_attendance.xml',
'views/day_attendance_report.xml',
],
'assets': {
'web.assets_backend': [
'hr_attendance_extended/static/src/xml/attendance_report.xml',
'hr_attendance_extended/static/src/js/attendance_report.js',
],
'web.assets_frontend': [
'web/static/lib/jquery/jquery.js',
'hr_attendance_extended/static/src/js/jquery-ui.min.js',
'hr_attendance_extended/static/src/js/jquery-ui.min.css',
]
}
}

View File

@ -1 +1,2 @@
from . import hr_attendance
from . import hr_attendance
from . import hr_attendance_report

View File

@ -0,0 +1,147 @@
from odoo import models, fields, api
from datetime import datetime, timedelta
import xlwt
from io import BytesIO
import base64
from odoo.exceptions import UserError
def convert_to_date(date_string):
# Use strptime to parse the date string in 'dd/mm/yyyy' format
return datetime.strptime(date_string, '%Y/%m/%d')
class AttendanceReport(models.Model):
_name = 'attendance.report'
_description = 'Attendance Report'
@api.model
def get_attendance_report(self, employee_id, start_date, end_date):
# Ensure start_date and end_date are in the correct format (datetime)
if employee_id == '-':
employee_id = False
else:
employee_id = int(employee_id)
if isinstance(start_date, str):
start_date = datetime.strptime(start_date, '%d/%m/%Y')
if isinstance(end_date, str):
end_date = datetime.strptime(end_date, '%d/%m/%Y')
# Convert the dates to 'YYYY-MM-DD' format for PostgreSQL
start_date_str = start_date.strftime('%Y-%m-%d')
end_date_str = end_date.strftime('%Y-%m-%d')
# Define the base where condition
if employee_id:
case = """WHERE emp.id = %s AND at.check_in >= %s AND at.check_out <= %s"""
params = (employee_id, start_date_str, end_date_str)
else:
case = """WHERE at.check_in >= %s AND at.check_out <= %s"""
params = (start_date_str, end_date_str)
# Define the query with improved date handling
query = """
WITH daily_checkins AS (
SELECT
emp.id,
emp.name,
DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date,
at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in,
at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out,
at.worked_hours,
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row,
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row
FROM
hr_attendance at
LEFT JOIN
hr_employee emp ON at.employee_id = emp.id
""" + case + """
)
SELECT
id,
name,
date,
MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in,
MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out,
SUM(worked_hours) AS total_worked_hours
FROM
daily_checkins
GROUP BY
id, name, date
ORDER BY
id, date;
"""
# Execute the query with parameters
self.env.cr.execute(query, params)
rows = self.env.cr.dictfetchall()
data = []
a = 0
for r in rows:
a += 1
# Calculate worked hours in Python, but here it's better done in the query itself.
worked_hours = r['last_check_out'] - r['first_check_in'] if r['first_check_in'] and r[
'last_check_out'] else 0
data.append({
'id': a,
'employee_id': r['id'],
'employee_name': r['name'],
'date': r['date'],
'check_in': r['first_check_in'],
'check_out': r['last_check_out'],
'worked_hours': worked_hours,
})
return data
@api.model
def export_to_excel(self, employee_id, start_date, end_date):
# Fetch the attendance data (replace with your logic to fetch attendance data)
attendance_data = self.get_attendance_report(employee_id, start_date, end_date)
if not attendance_data:
raise UserError("No data to export!")
# Create an Excel workbook and a sheet
workbook = xlwt.Workbook()
sheet = workbook.add_sheet('Attendance Report')
# Define the column headers
headers = ['Employee Name', 'Check-in', 'Check-out', 'Worked Hours']
# Write headers to the first row
for col_num, header in enumerate(headers):
sheet.write(0, col_num, header)
# Write the attendance data to the sheet
for row_num, record in enumerate(attendance_data, start=1):
sheet.write(row_num, 0, record['employee_name'])
sheet.write(row_num, 1, record['check_in'].strftime("%Y-%m-%d %H:%M:%S"))
sheet.write(row_num, 2, record['check_out'].strftime("%Y-%m-%d %H:%M:%S"))
if isinstance(record['worked_hours'], timedelta):
hours = record['worked_hours'].seconds // 3600
minutes = (record['worked_hours'].seconds % 3600) // 60
# Format as "X hours Y minutes"
worked_hours_str = f"{record['worked_hours'].days * 24 + hours} hours {minutes} minutes"
sheet.write(row_num, 3, worked_hours_str)
else:
sheet.write(row_num, 3, record['worked_hours'])
# Save the workbook to a BytesIO buffer
output = BytesIO()
workbook.save(output)
# Convert the output to base64 for saving in Odoo
file_data = base64.b64encode(output.getvalue())
# Create an attachment record to save the Excel file in Odoo
attachment = self.env['ir.attachment'].create({
'name': 'attendance_report.xls',
'type': 'binary',
'datas': file_data,
'mimetype': 'application/vnd.ms-excel',
})
# Return the attachment's URL to allow downloading in the Odoo UI
return '/web/content/%d/%s' % (attachment.id, attachment.name),

View File

@ -0,0 +1,185 @@
import { useService } from "@web/core/utils/hooks";
import { Component, xml, useState, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { getOrigin } from "@web/core/utils/urls";
export default class AttendanceReport extends Component {
static props = ['*'];
static template = 'attendance_report_template';
setup() {
super.setup(...arguments);
this.orm = useService("orm");
this.state = useState({
startDate: "", // To store the start date parameter
endDate: "",
attendanceData: [], // Initialized as an empty array
groupedData: [], // To store the grouped attendance data by employee_id
employeeIDS: [] // List of employee IDs to bind with select dropdown
});
onMounted(() => {
this.loademployeeIDS();
});
}
async loademployeeIDS() {
try {
const employee = await this.orm.searchRead('hr.employee', [], ['id', 'display_name']);
this.state.employeeIDS = employee;
this.initializeSelect2();
this.render();// Initialize Select2 after data is loaded
this.reload();
} catch (error) {
console.error("Error loading employeeIDS:", error);
}
}
// Initialize Select2 with error handling and ensuring it's initialized only once
initializeSelect2() {
const employeeIDS = this.state.employeeIDS;
// Ensure the <select> element is initialized only once
const $empSelect = $('#emp');
const from_date = $("#from_date").datepicker({
dateFormat: "dd/mm/yy", // Date format
showAnim: "slideDown", // Animation
changeMonth: true, // Allow month selection
changeYear: true, // Allow year selection
yearRange: "2010:2030", // Year range
// ... other options
});
const to_date = $("#to_date").datepicker({
dateFormat: "dd/mm/yy", // Date format
showAnim: "slideDown", // Animation
changeMonth: true, // Allow month selection
changeYear: true, // Allow year selection
yearRange: "2010:2030", // Year range
// ... other options
});
// Debugging the employeeIDS array to verify its structure
console.log("employeeIDS:", employeeIDS);
// Check if employeeIDS is an array and has the necessary properties
if (Array.isArray(employeeIDS) && employeeIDS.length > 0) {
// Clear the current options (if any)
$empSelect.empty();
// Add options for each employee
employeeIDS.forEach(emp => {
$empSelect.append(
`<option value="${emp.id}">${emp.display_name}</option>`
);
});
$empSelect.append(
`<option value="-">All</option>`
);
// Initialize the select with the 'multiple' attribute for multi-select
// $empSelect.attr('multiple', 'multiple');
// Enable tagging (you can manually add tags as well)
$empSelect.on('change', (ev) => {
const selectedEmployeeIds = $(ev.target).val();
console.log('Selected Employee IDs: ', selectedEmployeeIds);
const selectedEmployees = employeeIDS.filter(emp => selectedEmployeeIds.includes(emp.id.toString()));
console.log('Selected Employees: ', selectedEmployees);
});
} else {
console.error("Invalid employee data format:", employeeIDS);
}
}
// Method called when a date is selected in the input fields
async ExportToExcel() {
var startdate = $('#from_date').val()
var enddate = $('#to_date').val()
let domain = [
['check_in', '>=', startdate],
['check_in', '<=', enddate],
];
// If employee(s) are selected, filter the data by employee_id
if ($('#emp').val() && $('#emp').val().length > 0 && $('#emp').val() !== '-') {
domain.push(['employee_id', '=', parseInt($('#emp').val())]);
}
try {
debugger;
// Fetch the attendance data based on the date range and selected employees
const URL = await this.orm.call('attendance.report', 'export_to_excel', [$('#emp').val(), startdate, enddate]);
window.open(getOrigin()+URL, '_blank');
} catch (error) {
console.error("Error generating report:", error);
}
}
async generateReport() {
let { startDate, endDate, selectedEmployeeIds } = this.state;
startDate = $('#from_date').val()
endDate = $('#to_date').val()
if (!startDate || !endDate) {
alert("Please specify both start and end dates!");
return;
}
// Build the domain for the search query
let domain = [
['check_in', '>=', startDate],
['check_in', '<=', endDate],
];
// If employee(s) are selected, filter the data by employee_id
if ($('#emp').val() && $('#emp').val().length > 0 && $('#emp').val() !== '-') {
domain.push(['employee_id', '=', parseInt($('#emp').val())]);
}
try {
// Fetch the attendance data based on the date range and selected employees
// const attendanceData = await this.orm.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']);
const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#emp').val(),startDate,endDate]);
// Group data by employee_id
const groupedData = this.groupDataByEmployee(attendanceData);
// Update state with the fetched and grouped data
this.state.attendanceData = attendanceData;
this.state.groupedData = groupedData;
} catch (error) {
console.error("Error generating report:", error);
}
}
// Helper function to group data by employee_id
groupDataByEmployee(data) {
const grouped = {};
data.forEach(record => {
const employeeId = record.employee_id; // employee_id[1] is the name
if (employeeId) {
if (!grouped[employeeId]) {
grouped[employeeId] = [];
}
grouped[employeeId].push(record);
}
});
// Convert the grouped data into an array to be used in t-foreach
return Object.values(grouped);
}
}
// Register the action in the actions registry
registry.category("actions").add("AttendanceReport", AttendanceReport);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="attendance_report_template">
<script t-att-src="'/web/static/lib/jquery/jquery.js'"></script>
<script t-att-src="'/hr_attendance_extended/static/src/js/jquery-ui.min.js'"></script>
<link rel="stylesheet" type="text/css" href="/hr_attendance_extended/static/src/js/jquery-ui.min.css"/>
<style>
.ui-datepicker {
background-color: #f0f0f0;
}
.ui-state-highlight {
background-color: #ffcc00;
}
</style>
<div class="header pt-5" style="text-align:center">
<h1>Attendance Report</h1>
<div class="navbar navbar-expand-lg container">
<h4 class="p-3 text-nowrap">Employee </h4>
<div class="input-group input-group-lg">
<select type="text" id="emp" class="form-control" />
</div>
<h4 class="p-3 text-nowrap"> From Date</h4>
<div class="input-group input-group-lg">
<input type="text" id="from_date" class="form-control" value="DD/MM/YYYY"/>
</div>
<h4 class="p-3 text-nowrap"> To Date</h4>
<div class="input-group input-group-lg">
<input type="text" id="to_date" class="form-control" value="DD/MM/YYYY"/>
</div>
</div>
<button class="btn btn-outline-success" t-on-click="generateReport" >Generate Report</button>
<button class="btn btn-outline-success" t-on-click="ExportToExcel">Export Report</button>
<div t-if="this.state.groupedData.length > 0" style="max-height: 800px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
<div t-foreach="this.state.groupedData" t-as="group" t-key="group[0].employee_id">
<div class="employee-group">
<h3 style="text-align:left" class="p-2">
Employee:
<t t-if="group[0].employee_id">
<t t-esc="group[0].employee_name"/>
</t>
<t t-else="">
<span>Unknown Employee</span>
</t>
</h3>
<!-- Scrollable Container for the Table -->
<div class="scrollable-table-container">
<table class="table table-hover">
<thead>
<tr>
<th>Date</th>
<th>Employee</th>
<th>Check In</th>
<th>Check Out</th>
<th>Worked Hours</th>
</tr>
</thead>
<tbody>
<tr t-foreach="group" t-as="data" t-key="data.id">
<td><t t-esc="data.date"/></td>
<td>
<t t-if="data.employee_id">
<t t-esc="data.employee_name"/>
</t>
<t t-else="">
<span>Unknown Employee</span>
</t>
</td>
<td><t t-esc="data.check_in"/></td>
<td><t t-esc="data.check_out"/></td>
<td><t t-esc="data.worked_hours"/></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div t-else="">
<p>No data available for the selected date range.</p>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,7 @@
<odoo>
<record id="action_attendance_report_s" model="ir.actions.client">
<field name="name">Attendance Report</field>
<field name="tag">AttendanceReport</field>
</record>
<menuitem action="action_attendance_report_s" id="menu_hr_attendance_day" groups="hr_attendance.group_hr_attendance_officer" parent="hr_attendance_extended.menu_attendance_attendance"/>
</odoo>

View File

@ -7,8 +7,8 @@
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-top: 20px;
max-height: 900px; /* Adjust this value as needed */
overflow-y: auto; /* Make the container scrollable */
max-height: 100%; /* Adjust the height for scrolling */
overflow-y: auto; /* Enable vertical scrolling */
}
/* General Container */
@ -123,8 +123,8 @@
.card-content {
padding: 20px;
overflow-y: auto;
max-height: 350px;
overflow-y: auto; /* Enable scroll if content overflows */
max-height: 350px; /* Limit height to enable scroll */
font-size: 1em;
color: #555;
}
@ -165,10 +165,13 @@ body {
font-family: 'Arial', sans-serif;
color: #333;
background-color: #f0f0f5;
display: flex;
margin: 0;
padding: 0;
overflow-y: auto;
height: 100%;
overflow-x: hidden; /* Prevent horizontal scrolling */
overflow-y: auto; /* Enable vertical scrolling */
height: 100%; /* Ensure the body takes full height */
scroll-behavior: smooth; /* Smooth scroll */
}
.profile-header {
@ -319,3 +322,20 @@ body {
}
/* Optional: Scrollbar styling for Webkit browsers */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@ -44,6 +44,20 @@ export class NetflixProfileContainer extends Component {
this.fetchEmployeeData();
});
}
hr_timesheets() {
this.action.doAction({
name: "Timesheets",
type: 'ir.actions.act_window',
res_model: 'account.analytic.line',
view_mode: 'list,form',
views: [[false, 'list'], [false, 'form']],
context: {
'search_default_month': true,
},
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
target: 'current'
})
}
attendance_sign_in_out() {
if (this.state.login_employee.attendance_state == 'checked_out') {
this.state.login_employee.attendance_state = 'checked_in'
@ -145,11 +159,11 @@ export class NetflixProfileContainer extends Component {
const employee = employeeData[0];
attendanceLines.forEach(line => {
let createDate = new Date(line.create_date);
line.create_date = createDate.toISOString().split('T')[0]; // Format as 'YYYY-MM-DD'
let checkIn = new Date(line.check_in);
line.check_in = checkIn.toTimeString().slice(0, 5); // Format as 'HH:MM'
let checkOut = new Date(line.check_out);
line.check_out = checkOut.toTimeString().slice(0, 5); // Format as 'HH:MM'
line.create_date = createDate.toLocaleDateString('en-IN', { timeZone: 'Asia/Kolkata' }); // Format as 'YYYY-MM-DD'
let checkIn = new Date(line.check_in + 'Z');
line.check_in = checkIn.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM'
let checkOut = new Date(line.check_out + 'Z');
line.check_out = checkOut.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM'
line.worked_hours = line.worked_hours.toFixed(2);
});
this.state.attendance_lines = attendanceLines,

View File

@ -1 +1,2 @@
from . import models,wizards
from . import models
from . import wizards

View File

@ -16,15 +16,16 @@
# for the full list
'category': 'Human Resources/Employees',
'version': '0.1',
'license': 'LGPL-3',
# any module necessary for this one to work correctly
'depends': ['base','hr'],
'depends': ['base','hr','account','mail','hr_skills', 'hr_contract'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'views/hr_employee.xml',
'views/bank_details.xml',
'wizards/work_location_wizard.xml'
],
}

View File

@ -1,2 +1,7 @@
from . import hr_employee
from . import work_location_history
from . import work_location_history
from . import education_history
from . import employer_history
# from . import hr_employee_attachments
from . import family_details
from . import bank_details

View File

@ -0,0 +1,12 @@
from odoo import api, fields, models
class Bank(models.Model):
_inherit = 'res.bank'
branch = fields.Char(string='Branch')
class PartnerBank(models.Model):
_inherit='res.partner.bank'
full_name = fields.Char(string="Full Name(as per bank)")

View File

@ -0,0 +1,24 @@
from odoo import models, fields
class EducationHistory(models.Model):
_name = "education.history"
_description = "Education Details"
_rec_name = "name"
name = fields.Char(string="Specialization", required=True)
university = fields.Char(string="University/Institute", required=True)
start_year = fields.Integer(string="Start Year", required=True)
end_year = fields.Integer(string="End Year", required=True)
marks_or_grade = fields.Char(string="Marks/Grade", required=True)
education_type = fields.Selection([
('10', '10th'),
('inter', 'Inter'),
('graduation', 'Graduation'),
('post_graduation', 'Post Graduation'),
('additional', 'Additional Qualification'),
], string="Education Type", required=True)
employee_id = fields.Many2one('hr.employee')

View File

@ -0,0 +1,12 @@
from odoo import models, fields
class EmployerHistory(models.Model):
_name = 'employer.history'
_description = 'Employee Work History'
company_name = fields.Char(string='Company Name', required=True)
designation = fields.Char(string='Designation', required=True)
date_of_joining = fields.Date(string='Date of Joining', required=True)
last_working_day = fields.Date(string='Last Working Day')
ctc = fields.Char(string='CTC')
employee_id = fields.Many2one('hr.employee')

View File

@ -0,0 +1,23 @@
from odoo import models, fields
class FamilyDetails(models.Model):
_name = "family.details"
_description = "Family Details"
_rec_name = "name"
name = fields.Char(string="Full Name", required=True)
contact_no = fields.Char(string="Contact No")
dob = fields.Date(string="Date of Birth")
location = fields.Char(string="Location")
relation_type = fields.Selection([
('father', 'Father'),
('mother', 'Mother'),
('spouse', 'Spouse'),
('kid1', 'Kid 1'),
('kid2', 'Kid 2'),
], string="Relation Type", required=True)
employee_id = fields.Many2one('hr.employee')

View File

@ -3,6 +3,8 @@
from odoo import _, api, fields, models
from dateutil.relativedelta import relativedelta
from odoo.exceptions import ValidationError
from datetime import datetime, timedelta
from calendar import monthrange
class HrEmployeeBase(models.AbstractModel):
@ -20,6 +22,18 @@ class HrEmployeeBase(models.AbstractModel):
emp_type = fields.Many2one('hr.contract.type', "Employee Type", 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")
@api.constrains('identification_id')
def _check_identification_id(self):
@ -35,10 +49,41 @@ class HrEmployeeBase(models.AbstractModel):
current_date = fields.Date.today()
# Calculate the difference between current date and doj
delta = relativedelta(current_date, record.doj)
def calculate_experience(joined_date, current_date):
# Start by calculating the difference in years and months
delta_years = current_date.year - joined_date.year
delta_months = current_date.month - joined_date.month
delta_days = current_date.day - joined_date.day
# Adjust months and years if necessary
if delta_months < 0:
delta_years -= 1
delta_months += 12
# Handle day adjustment if necessary (i.e., current day is less than the joined day)
if delta_days < 0:
# Subtract one month to adjust
delta_months -= 1
# Get the number of days in the previous month to add to the days
if current_date.month == 1:
days_in_last_month = monthrange(current_date.year - 1, 12)[1]
else:
days_in_last_month = monthrange(current_date.year, current_date.month - 1)[1]
delta_days += days_in_last_month
# Final adjustment: if months become negative after adjusting days, fix that
if delta_months < 0:
delta_years -= 1
delta_months += 12
return delta_years, delta_months, delta_days
# Calculate the experience
years, months, days = calculate_experience(record.doj, current_date)
# Format the experience as 'X years Y months Z days'
experience_str = f"{delta.years} years {delta.months} months {delta.days} days"
experience_str = f"{years} years {months} months {days} days"
record.current_company_exp = experience_str
else:
record.current_company_exp = '0 years 0 months 0 days'
@ -84,3 +129,31 @@ class HrEmployeeBase(models.AbstractModel):
total_months = record.previous_exp % 12
record.total_exp = f"{total_years} years {total_months} months 0 days"
class HrEmployee(models.Model):
_inherit = 'hr.employee'
education_history = fields.One2many('education.history','employee_id', string='Education Details')
employer_history = fields.One2many('employer.history','employee_id', string='Education Details')
family_details = fields.One2many('family.details','employee_id',string='Family Details')
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")
marriage_anniversary_date = fields.Date(string='Anniversary Date', tracking=True)
passport_start_date = fields.Date(string='Passport Issued Date')
passport_end_date = fields.Date(string='Passport End Date')
passport_issued_location = fields.Char(string='Passport Issued Location')
previous_company_pf_no = fields.Char(string='Previous Company PF No')
previous_company_uan_no = fields.Char(string='Previous Company UAN No')

View File

@ -0,0 +1,10 @@
from odoo import models, fields
class HrEmployeeAttachment(models.Model):
_name = 'hr.employee.attachment'
_description = 'Attachments for Work History and Education Details'
name = fields.Char(string='Attachment Name', required=True)
file = fields.Binary(string='File', required=True)
employer_history_id = fields.Many2one('employer.history', string='Employer History')
education_history_id = fields.Many2one('education.history', string='Education History')

View File

@ -1,3 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_emp_work_location_history,emp.work.location.history,model_emp_work_location_history,base.group_user,1,1,1,1
access_work_location_wizard,work.location.wizard,model_work_location_wizard,base.group_user,1,1,1,1
access_work_location_wizard,work.location.wizard,model_work_location_wizard,base.group_user,1,1,1,1
access_education_history,education.history,model_education_history,base.group_user,1,1,1,1
access_employer_history,employer.history,model_employer_history,base.group_user,1,1,1,1
access_family_details,family.details,model_family_details,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_emp_work_location_history emp.work.location.history model_emp_work_location_history base.group_user 1 1 1 1
3 access_work_location_wizard work.location.wizard model_work_location_wizard base.group_user 1 1 1 1
4 access_education_history education.history model_education_history base.group_user 1 1 1 1
5 access_employer_history employer.history model_employer_history base.group_user 1 1 1 1
6 access_family_details family.details model_family_details base.group_user 1 1 1 1
7

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="view_res_bank_form_inherit" model="ir.ui.view">
<field name="name">base_view_res_bank_form_inherit</field>
<field name="model">res.bank</field>
<field name="inherit_id" ref="base.view_res_bank_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='bic']" position="after">
<field name="branch"/>
</xpath>
</field>
</record>
<record id="view_partner_bank_form_inherit_account_inherit" model="ir.ui.view">
<field name="name">view_partner_bank_form_inherit_account_inherit</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="account.view_partner_bank_form_inherit_account"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='acc_number']" position="after">
<field name="full_name"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Form View -->
<record id="view_education_details_form" model="ir.ui.view">
<field name="name">education.details.form</field>
<field name="model">education.details</field>
<field name="arch" type="xml">
<form string="Education Details">
<sheet>
<group>
<group>
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
</group>
<group>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- list/List View -->
<record id="view_education_details_list" model="ir.ui.view">
<field name="name">education.details.list</field>
<field name="model">education.details</field>
<field name="arch" type="xml">
<list string="Education Details">
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
</list>
</field>
</record>
<!-- Action -->
<record id="action_education_details" model="ir.actions.act_window">
<field name="name">Education Details</field>
<field name="res_model">education.details</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu Items -->
<!-- <menuitem-->
<!-- id="hr_employee_education_details"-->
<!-- name="Education Details"-->
<!-- action="hr_resume_type_action"-->
<!-- parent="hr_skills.menu_human_resources_configuration_resume"-->
<!-- sequence="3"-->
<!-- groups="base.group_no_one"/>-->
</odoo>

View File

@ -1,11 +1,138 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="hr_skills_hr_employee_view_form_inherit" model="ir.ui.view">
<field name="name">hr.employee.skills.form.inherit</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr_skills.hr_employee_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='skills_resume']" position="inside">
<div>
<div>
<separator string="Employer History"/>
<!-- This field uses a custom list view rendered by the 'resume_one2many' widget.
Adding fields in the list arch below makes them accessible to the widget
-->
<field mode="list" nolabel="1" name="employer_history">
<list string="Employer Details">
<field name="company_name"/>
<field name="designation"/>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" column_invisible="1"/>
</list>
<form string="Employer Details">
<sheet>
<group>
<group>
<field name="company_name"/>
<field name="designation"/>
</group>
<group>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</div>
<div>
<separator string="Education History"/>
<field mode="list" nolabel="1" name="education_history"
class="mt-2">
<list string="Education Details">
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<field name="employee_id" column_invisible="1"/>
</list>
<form string="Education Details">
<sheet>
<group>
<group>
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
</group>
<group>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<field name="employee_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</div>
</div>
</xpath>
</field>
</record>
<record id="view_employee_form_inherit" model="ir.ui.view">
<field name="name">hr.employee.form.inherit</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='passport_id']" position="after">
<field name="passport_start_date"/>
<field name="passport_end_date"/>
<field name="passport_issued_location"/>
</xpath>
<xpath expr="//field[@name='marital']" position="after">
<field name="marriage_anniversary_date" invisible="marital != 'married'"/>
</xpath>
<xpath expr="//page[@name='personal_information']/group" position="inside">
<h5>Family Details</h5>
<field name="family_details">
<list editable="bottom">
<field name="relation_type"/>
<field name="name"/>
<field name="contact_no"/>
<field name="dob"/>
<field name="location"/>
</list>
</field>
<br/>
<br/>
<h5>Education Details</h5>
<field name="education_history">
<list string="Education Details">
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<field name="employee_id" column_invisible="1"/>
</list>
</field>
<br/>
<br/>
<h5>Employer History</h5>
<field name="employer_history">
<list string="Employer Details">
<field name="company_name"/>
<field name="designation"/>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" column_invisible="1"/>
</list>
</field>
</xpath>
<xpath expr="//field[@name='employee_type']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
@ -26,14 +153,16 @@
<field name="pan_no"/>
</xpath>
<xpath expr="//header" position="inside">
<button name="open_work_location_wizard" type="object" string="Update Work Location" class="btn-primary" groups="hr.group_hr_manager"/>
<button name="open_work_location_wizard" type="object" string="Update Work Location"
class="btn-primary" groups="hr.group_hr_manager"/>
</xpath>
<xpath expr="//notebook" position="inside">
<page name="work_location_history_page" string="Work Location History" groups="hr.group_hr_manager">
<field name="work_loc_history">
<list editable="bottom">
<field name="employee_id" column_invisible="1"/>
<field name="work_location_type" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
<field name="work_location_type"
options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="note"/>
@ -41,6 +170,31 @@
</field>
</page>
</xpath>
<xpath expr="//field[@name='private_car_plate']" position="after">
<field name="blood_group"/>
</xpath>
<xpath expr="//field[@name='private_email']" position="before">
<label for="permanent_street" string="Permanent Address"/>
<div class="o_address_format">
<field name="permanent_street" placeholder="Street..." class="o_address_street"/>
<field name="permanent_street2" placeholder="Street 2..." class="o_address_street"/>
<field name="permanent_city" placeholder="City" class="o_address_city"/>
<field name="permanent_state_id" class="o_address_state" placeholder="State"
options="{'no_open': True, 'no_quick_create': True}"
context="{'default_country_id': private_country_id}"/>
<field name="permanent_zip" placeholder="ZIP" class="o_address_zip"/>
<field name="permanent_country_id" placeholder="Country" class="o_address_country"
options="{&quot;no_open&quot;: True, &quot;no_create&quot;: True}"/>
</div>
</xpath>
<xpath expr="//page[@name='hr_settings']" position="inside">
<group>
<group string="PF Details" name="pf_details">
<field name="previous_company_pf_no"/>
<field name="previous_company_uan_no"/>
</group>
</group>
</xpath>
</field>
</record>
@ -55,22 +209,24 @@
<xpath expr="//field[@name='work_location_id']" position="after">
<field name="doj"/>
</xpath>
<!-- <xpath expr="//field[@name='identification_id']" position="attributes">-->
<!-- <attribute name="string">AADHAR No</attribute>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='identification_id']" position="after">-->
<!-- <field name="pan_no"/>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='identification_id']" position="attributes">-->
<!-- <attribute name="string">AADHAR No</attribute>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='identification_id']" position="after">-->
<!-- <field name="pan_no"/>-->
<!-- </xpath>-->
</field>
</record>
<record id="mail.menu_root_discuss" model="ir.ui.menu">
<field name="groups_id" eval="[(3,ref('base.group_user')),(4, ref('hr_employee_extended.group_internal_user'))]"/>
<field name="groups_id"
eval="[(3,ref('base.group_user')),(4, ref('hr_employee_extended.group_internal_user'))]"/>
</record>
<record id="hr.menu_hr_root" model="ir.ui.menu">
<field name="groups_id" eval="[(3,ref('hr.group_hr_manager')),(3,ref('hr.group_hr_user')),(3,ref('base.group_user')),(3,ref('hr_employee_extended.group_external_user')),(4, ref('hr_employee_extended.group_internal_user'))]"/>
<field name="groups_id"
eval="[(3,ref('hr.group_hr_manager')),(3,ref('hr.group_hr_user')),(3,ref('base.group_user')),(3,ref('hr_employee_extended.group_external_user')),(4, ref('hr_employee_extended.group_internal_user'))]"/>
</record>
</data>
</odoo>

View File

@ -119,11 +119,11 @@
parent="menu_hr_payroll_payslips"/>
<!-- **** Reporting **** -->
<menuitem id="menu_report_payroll"
name="Payroll"
action="ir_actions_server_action_open_reporting"
sequence="10"
parent="menu_hr_payroll_report"/>
<!-- <menuitem id="menu_report_payroll"-->
<!-- name="Payroll"-->
<!-- action="ir_actions_server_action_open_reporting"-->
<!-- sequence="10"-->
<!-- parent="menu_hr_payroll_report"/>-->
<menuitem
id="menu_hr_payroll_headcount_action"
@ -132,12 +132,12 @@
sequence="11"
parent="menu_hr_payroll_report"/>
<menuitem
id="menu_hr_work_entry_report"
name="Work Entry Analysis"
action="hr_work_entry_report_action"
sequence="20"
parent="menu_hr_payroll_report"/>
<!-- <menuitem-->
<!-- id="menu_hr_work_entry_report"-->
<!-- name="Work Entry Analysis"-->
<!-- action="hr_work_entry_report_action"-->
<!-- sequence="20"-->
<!-- parent="menu_hr_payroll_report"/>-->
<!-- **** Configuration **** -->

View File

@ -2,3 +2,4 @@
from . import controllers
from . import models
from . import wizards

View File

@ -18,21 +18,38 @@
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['hr_recruitment','hr','hr_recruitment_skills','website_hr_recruitment','requisitions'],
'depends': ['base','hr_recruitment','hr','hr_recruitment_skills','website_hr_recruitment','requisitions'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/cron.xml',
'data/sequence.xml',
'data/mail_template.xml',
'views/hr_recruitment.xml',
'views/hr_location.xml',
'views/website_hr_recruitment_application_templates.xml',
'views/stages.xml',
'views/hr_applicant_views.xml',
'views/hr_job_recruitment.xml',
'views/hr_recruitment.xml',
'views/res_partner.xml',
'views/hr_recruitment_application_templates.xml',
'views/candidate_experience.xml',
'views/recruitment_attachments.xml',
'views/hr_employee_education_employer_family.xml',
'views/hr_recruitment_source.xml',
'views/requisitions.xml',
'views/skills.xml',
'wizards/post_onboarding_attachment_wizard.xml',
# 'views/resume_pearser.xml',
],
'assets': {
'web.assets_backend': [
'hr_recruitment_extended/static/src/img/pdf_icon.png',
],
'web.assets_frontend': [
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
],
}
}

View File

@ -11,157 +11,225 @@ from odoo.osv.expression import AND
from odoo.http import request
from odoo.tools import email_normalize
from odoo.tools.misc import groupby
import base64
from odoo.exceptions import UserError
from PIL import Image
from io import BytesIO
import re
import json
class WebsiteRecruitmentApplication(WebsiteHrRecruitment):
class website_hr_recruitment_applications(http.Controller):
@http.route('/hr_recruitment_extended/fetch_hr_recruitment_degree', type='json', auth="public", website=True)
def fetch_recruitment_degrees(self):
degrees = {}
all_degrees = http.request.env['hr.recruitment.degree'].sudo().search([])
if all_degrees:
for degree in all_degrees:
degrees[degree.id] = degree.name
return degrees
@http.route(['/hr_recruitment/second_application_form/<int:applicant_id>'], type='http', auth="public", website=True)
def second_application_form(self, applicant_id, **kwargs):
"""Renders the website form for applicants to submit additional details."""
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found()
if applicant and applicant.send_second_application_form:
if applicant.second_application_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template")
else:
return request.render("hr_recruitment_extended.applicant_form_template", {
'applicant': applicant
})
else:
return request.not_found()
@http.route('/hr_recruitment_extended/fetch_preferred_locations', type='json', auth="public", website=True)
def fetch_preferred_locations(self, loc_ids):
locations = {}
for id in loc_ids:
location = http.request.env['hr.location'].sudo().browse(id)
if location:
locations[location.id] = location.location_name
return locations
@http.route('/website_hr_recruitment/check_recent_application', type='json', auth="public", website=True)
def check_recent_application(self, field, value, job_id):
def refused_applicants_condition(applicant):
return not applicant.active \
and applicant.job_id.id == int(job_id) \
and applicant.create_date >= (datetime.now() - relativedelta(months=6))
field_domain = {
'name': [('partner_name', '=ilike', value)],
'email': [('email_normalized', '=', email_normalize(value))],
'phone': [('partner_phone', '=', value)],
'linkedin': [('linkedin_profile', '=ilike', value)],
}.get(field, [])
@http.route(['/hr_recruitment/submit_second_application/<int:applicant_id>/submit'], type='http', auth="public",
methods=['POST'], website=True, csrf=False)
def process_application_form(self, applicant_id, **kwargs):
# Get the applicant
candidate_image_base64 = kwargs.pop('candidate_image_base64')
candidate_image = kwargs.pop('candidate_image')
experience_years = kwargs.pop('experience_years', 0)
experience_months = kwargs.pop('experience_months', 0)
if not len(str(experience_months)) > 0:
experience_months = 0
if not len(str(experience_years)) > 0:
experience_years = 0
# If there are months, convert everything to months
if int(experience_months) > 0:
kwargs['total_exp'] = (int(experience_years) * 12) + int(experience_months)
kwargs['total_exp_type'] = 'month'
else:
kwargs['total_exp'] = int(experience_years)
kwargs['total_exp_type'] = 'year'
applications_by_status = http.request.env['hr.applicant'].sudo().search(AND([
field_domain,
[
('job_id.website_id', 'in', [http.request.website.id, False]),
'|',
('application_status', '=', 'ongoing'),
'&',
('application_status', '=', 'refused'),
('active', '=', False),
]
]), order='create_date DESC').grouped('application_status')
refused_applicants = applications_by_status.get('refused', http.request.env['hr.applicant'])
if any(applicant for applicant in refused_applicants if refused_applicants_condition(applicant)):
return {
'message': _(
'We\'ve found a previous closed application in our system within the last 6 months.'
' Please consider before applying in order not to duplicate efforts.'
)
}
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found() # Return 404 if applicant doesn't exist
if 'ongoing' not in applications_by_status:
return {'message': None}
if applicant.second_application_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template")
ongoing_application = applications_by_status.get('ongoing')[0]
if ongoing_application.job_id.id == int(job_id):
recruiter_contact = "" if not ongoing_application.user_id else _(
' In case of issue, contact %(contact_infos)s',
contact_infos=", ".join(
[value for value in itemgetter('name', 'email', 'phone')(ongoing_application.user_id) if value]
))
return {
'message': _(
'An application already exists for %(value)s.'
' Duplicates might be rejected. %(recruiter_contact)s',
value=value,
recruiter_contact=recruiter_contact
)
}
return {
'message': _(
'We found a recent application with a similar name, email, phone number.'
' You can continue if it\'s not a mistake.'
)
kwargs['candidate_image'] = candidate_image_base64
applicant.write(kwargs)
applicant.candidate_id.candidate_image = candidate_image_base64
template = request.env.ref('hr_recruitment_extended.email_template_second_application_submitted',
raise_if_not_found=False)
if template and applicant.user_id.email:
template.sudo().send_mail(applicant.id, force_send=True)
applicant.second_application_form_status = 'done'
# Redirect to a Thank You page
return request.render("hr_recruitment_extended.thank_you_template")
@http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public",
website=True)
def post_onboarding_form(self, applicant_id, **kwargs):
"""Renders the website form for applicants to submit additional details."""
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found()
if applicant and applicant.send_post_onboarding_form:
if applicant.post_onboarding_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template")
else:
return request.render("hr_recruitment_extended.post_onboarding_form_template", {
'applicant': applicant
})
else:
return request.not_found()
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
methods=['POST'], website=True, csrf=False)
def process_employee_joining_form(self,applicant_id,**post):
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found() # Return 404 if applicant doesn't exist
if applicant.post_onboarding_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template")
private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0)))
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
applicant_data = {
'applicant_id': int(post.get('applicant_id', 0)),
'employee_id': int(post.get('employee_id', 0)),
'candidate_image': post.get('candidate_image_base64', ''),
'doj': datetime.strptime(post.get('doj'), '%Y-%m-%d').date() if post.get('doj', None) else '',
'email_from': post.get('email_from', ''),
'gender': post.get('gender', ''),
'partner_phone': post.get('partner_phone', ''),
'alternate_phone': post.get('alternate_phone', ''),
'birthday': post.get('birthday', ''),
'blood_group': post.get('blood_group', ''),
'private_street': post.get('present_street', ''),
'private_street2': post.get('present_street2', ''),
'private_city': post.get('present_city', ''),
'private_state_id': private_state_id.id if private_state_id else '',
'private_country_id': private_state_id.country_id.id if private_state_id else '',
'private_zip': post.get('present_zip', ''),
'permanent_street': post.get('permanent_street', ''),
'permanent_street2': post.get('permanent_street2', ''),
'permanent_city': post.get('permanent_city', ''),
'permanent_state_id': permanent_state_id.id if permanent_state_id else '',
'permanent_country_id': permanent_state_id.country_id.id if permanent_state_id else '',
'permanent_zip': post.get('permanent_zip', ''),
'marital': post.get('marital', ''),
'marriage_anniversary_date': post.get('marriage_anniversary_date', ''),
'full_name_as_in_bank': post.get('full_name_as_in_bank', ''),
'bank_name': post.get('bank_name', ''),
'bank_branch': post.get('bank_branch', ''),
'bank_account_no': post.get('bank_account_no', ''),
'bank_ifsc_code': post.get('bank_ifsc_code', ''),
'passport_no': post.get('passport_no', ''),
'passport_start_date': datetime.strptime(post.get('passport_start_date'), '%Y-%m-%d').date() if post.get('passport_start_date', None) else '',
'passport_end_date': datetime.strptime(post.get('passport_end_date'), '%Y-%m-%d').date() if post.get('passport_end_date', None) else '',
'passport_issued_location': post.get('passport_issued_location', ''),
'pan_no': post.get('pan_no', ''),
'identification_id': post.get('identification_id', ''),
'previous_company_pf_no': post.get('previous_company_pf_no', ''),
'previous_company_uan_no': post.get('previous_company_uan_no', ''),
'post_onboarding_form_status': 'done'
}
def _should_log_authenticate_message(self, record):
if record._name == "hr.applicant" and not request.session.uid:
return False
return super()._should_log_authenticate_message(record)
applicant_data = {k: v for k, v in applicant_data.items() if v != '' and v != 0}
def extract_data(self, model, values):
candidate = False
current_ctc = values.pop('current_ctc', None)
expected_ctc = values.pop('expected_ctc', None)
exp_type = values.pop('exp_type', None)
current_location = values.pop('current_location', None)
preferred_locations_str = values.pop('preferred_locations', '')
preferred_locations = [int(x) for x in preferred_locations_str.split(',')] if len(preferred_locations_str) > 0 else []
current_organization = values.pop('current_organization', None)
notice_period = values.pop('notice_period',0)
notice_period_type = values.pop('notice_period_type', 'day')
if model.model == 'hr.applicant':
# pop the fields since there are only useful to generate a candidate record
# partner_name = values.pop('partner_name')
first_name = values.pop('first_name', None)
middle_name = values.pop('middle_name', None)
last_name = values.pop('last_name', None)
partner_phone = values.pop('partner_phone', None)
alternate_phone = values.pop('alternate_phone', None)
partner_email = values.pop('email_from', None)
degree = values.pop('degree',None)
if partner_phone and partner_email:
candidate = request.env['hr.candidate'].sudo().search([
('email_from', '=', partner_email),
('partner_phone', '=', partner_phone),
], limit=1)
if candidate:
candidate.sudo().write({
'partner_name': f"{first_name + ' '+ ((middle_name + ' ') if middle_name else '') + last_name}",
'first_name': first_name,
'middle_name': middle_name,
'last_name': last_name,
'alternate_phone': alternate_phone,
'type_id': int(degree) if degree.isdigit() else False
})
if not candidate:
candidate = request.env['hr.candidate'].sudo().create({
'partner_name': f"{first_name + ' '+ ((middle_name + ' ') if middle_name else '') + last_name}",
'email_from': partner_email,
'partner_phone': partner_phone,
'first_name': first_name,
'middle_name': middle_name,
'last_name': last_name,
'alternate_phone': alternate_phone,
'type_id': int(degree) if degree.isdigit() else False
})
values['partner_name'] = f"{first_name + ' '+ ((middle_name + ' ') if middle_name else '') + last_name}"
if partner_phone:
values['partner_phone'] = partner_phone
if partner_email:
values['email_from'] = partner_email
data = super().extract_data(model, values)
data['record']['current_ctc'] = float(current_ctc if current_ctc else 0)
data['record']['salary_expected'] = float(expected_ctc if expected_ctc else 0)
data['record']['exp_type'] = exp_type if exp_type else 'fresher'
data['record']['current_location'] =current_location if current_location else ''
data['record']['current_organization'] = current_organization if current_organization else ''
data['record']['notice_period'] = notice_period if notice_period else 0
data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day'
if len(preferred_locations_str) > 0:
data['record']['preferred_location'] = preferred_locations
if candidate:
data['record']['candidate_id'] = candidate.id
data['record']['type_id'] = candidate.type_id.id
return data
# Get family details data from JSON
family_data_json = post.get('family_data_json', '[]')
family_data = json.loads(family_data_json) if family_data_json else []
if family_data:
applicant_data['family_details'] = [
(0, 0, {
'relation_type': member.get('relation', ''),
'name': str(member.get('name', '')),
'contact_no': member.get('contact', ''),
'dob': datetime.strptime(member.get('dob'), '%Y-%m-%d').date() if member.get('dob') else None,
'location': member.get('location', ''),
}) for member in family_data if member.get('name') and member.get('relation') # Optional filter to avoid empty members
]
# education details
education_data_json = post.get('education_data_json', '[]')
education_data = json.loads(education_data_json) if education_data_json else []
if education_data:
applicant_data['education_history'] = [
(0,0,{
'education_type': education.get('education_type',''),
'name': education.get('specialization', ''),
'university': education.get('university', ''),
'start_year': education.get('start_year', ''),
'end_year': education.get('end_year', ''),
'marks_or_grade': education.get('marks_or_grade','')
}) for education in education_data
]
# employer details
employer_history_data_json = post.get('employer_history_data_json', '[]')
employer_data = json.loads(employer_history_data_json) if employer_history_data_json else []
if employer_data:
applicant_data['employer_history'] = [
(0,0,{
'company_name': company.get('company_name',''),
'designation': company.get('designation', ''),
'date_of_joining': datetime.strptime(company.get('date_of_joining'), '%Y-%m-%d').date() if company.get('date_of_joining') else '',
'last_working_day': datetime.strptime(company.get('last_working_day'), '%Y-%m-%d').date() if company.get('last_working_day') else '',
'ctc': company.get('ctc', ''),
}) for company in employer_data
]
#attachments
attachments_data_json = post.get('attachments_data_json', '[]')
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
if attachments_data:
applicant_data['joining_attachment_ids'] = [
(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)
return request.render("hr_recruitment_extended.thank_you_template")
@http.route('/hr_recruitment_extended/fetch_related_state_ids', type='json', auth="public", website=True)
def fetch_preferred_state_ids(self, country_id=None):
state_ids = {}
states = http.request.env['res.country.state'].sudo()
if country_id:
for state_id in states.search([('country_id', '=?', country_id)]):
state_ids[state_id.id] = state_id.name
return state_ids

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record model="ir.cron" id="hr_job_end_date_update">
<field name="name">JOB: End date</field>
<field name="model_id" ref="hr.model_hr_job"/>
<record model="ir.cron" id="hr_job_recruitment_end_date_update">
<field name="name">JOB Recruitment: End date</field>
<field name="model_id" ref="model_hr_job_recruitment"/>
<field name="state">code</field>
<field name="code">model.hr_job_end_date_update()</field>
<field name="code">model.hr_job_recruitment_end_date_update()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>

View File

@ -2,41 +2,424 @@
<odoo>
<data>
<record id="template_recruitment_deadline_alert" model="mail.template">
<field name="name">Recruitment Deadline Alert</field>
<field name="model_id" ref="hr.model_hr_job"/>
<field name="email_from">{{ object.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.user_id.email }}
</field>
<field name="subject">Reminder: Recruitment Process Ending Soon - {{ object.name }}</field>
<field name="description">Notification sent to recruiters when a job's recruitment deadline is approaching.</field>
<field name="body_html" type="html">
<t t-set="user_names" t-value="', '.join(object.user_id.mapped('name')) if object.user_id else 'Recruiter'"/>
<t t-set="location_names" t-value="', '.join(object.locations.mapped('name')) if object.locations else 'N/A'"/>
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="user_names">Recruiter</t>,
<br /><br />
This is a friendly reminder that the recruitment process for the position
<strong><t t-esc="object.name or ''">Job Title</t></strong> is approaching its end:
<ul>
<li><strong>Job Position:</strong> <t t-esc="object.name or ''">Job Name</t></li>
<li><strong>Target End Date:</strong> <t t-esc="object.target_to or ''">End Date</t></li>
<li><strong>Location(s):</strong> <t t-esc="location_names">Locations</t></li>
</ul>
<br />
Please ensure all recruitment activities are completed before the deadline.
<br /><br />
<a t-att-href="'%s/web#id=%d&amp;model=hr.job&amp;view_type=form' % (object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)" target="_blank">
Click here to view the job details.
</a>
<br /><br />
Regards,<br />
<t t-esc="user.name or 'System Admin'">System Admin</t>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<field name="name">Recruitment Deadline Alert</field>
<field name="model_id" ref="model_hr_job_recruitment"/>
<field name="email_from">{{ object.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.user_id.email }}</field>
<field name="subject">Reminder: Recruitment Process Ending Soon - {{ object.job_id.name }}</field>
<field name="description">
Notification sent to recruiters when a job's recruitment deadline is approaching.
</field>
<field name="body_html" type="html">
<t t-set="user_names"
t-value="', '.join(object.user_id.mapped('name')) if object.user_id else 'Recruiter'"/>
<t t-set="location_names"
t-value="', '.join(object.locations.mapped('name')) if object.locations else 'N/A'"/>
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; line-height: 1.5; padding: 20px;">
<p>Dear <t t-esc="user_names">Recruiter</t>,
</p>
<p>This is a friendly reminder that the recruitment process for the position
<strong>
<t t-esc="object.job_id.name or ''">Job Title</t>
</strong>
is approaching its end. Please find the details below:
</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #f9f9f9;">
<strong>Job Position:</strong>
</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<t t-esc="object.job_id.name or ''">Job Name</t>
</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #f9f9f9;">
<strong>Target End Date:</strong>
</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<t t-esc="object.target_to or ''">End Date</t>
</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #f9f9f9;">
<strong>Location(s):</strong>
</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<t t-esc="location_names">Locations</t>
</td>
</tr>
</table>
<p style="margin-top: 15px;">Please ensure all recruitment activities are completed before the
deadline.
</p>
<p style="margin-top: 15px;">
<a t-att-href="'%s/web#id=%d&amp;model=hr.job.recruitment&amp;view_type=form' % (object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)"
style="background-color: #007bff; color: #ffffff; padding: 10px 15px; text-decoration: none; border-radius: 5px; display: inline-block;"
target="_blank">
View Job Details
</a>
</p>
<p style="margin-top: 20px;">Best Regards,</p>
<p>
<t t-esc="user.name or 'System Admin'">System Admin</t>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_second_application_form" model="mail.template">
<field name="name">Employee Salary &amp; Experience Form Request</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.email_from }}</field>
<field name="subject">Action Required: Please Fill Out the Form</field>
<field name="description">
Request to employees to provide salary expectations, experience, and current offers.
</field>
<field name="body_html" type="html">
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="form_url" t-value="base_url + '/hr_recruitment/second_application_form/%s' % object.id"/>
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
<p>Dear
<strong>
<t t-esc="applicant_name">Applicant</t>
</strong>
,
</p>
<p>We hope you're doing well. Kindly take a few minutes to fill out the following form regarding:
</p>
<ul style="margin-left: 15px;">
<li>
<strong>Salary Expectations</strong>
</li>
<li>
<strong>Previous Work Experience</strong>
</li>
<li>
<strong>Current Job Offers (if any)</strong>
</li>
</ul>
<p>Click the button below to access the form:</p>
<p style="text-align: center; margin-top: 20px;">
<a t-att-href="form_url" target="_blank"
style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none;
font-weight: bold; border-radius: 5px; display: inline-block;">
Fill Out the Form
</a>
</p>
<p>If you have any questions, feel free to reach out.</p>
<p>Best Regards,
<br/>
<strong>
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
</strong>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_second_application_submitted" model="mail.template">
<field name="name">Applicant Form Submission Notification</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.user_id.email }}</field> <!-- Recruiter's Email -->
<field name="subject">New Submission: Applicant Salary &amp; Experience Form</field>
<field name="description">
Notification sent to recruiter when an applicant submits the form.
</field>
<field name="body_html" type="html">
<t t-set="recruiter_name" t-value="object.user_id.name or 'Recruiter'"/>
<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="applicant_url"
t-value="base_url + '/web#id=%s&amp;model=hr.applicant&amp;view_type=form' % object.id"/>
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
<p>Dear
<strong>
<t t-esc="recruiter_name">Recruiter</t>
</strong>
,
</p>
<p>The applicant
<strong>
<t t-esc="applicant_name">Applicant</t>
</strong>
has submitted their additional details.
</p>
<p style="text-align: center; margin-top: 20px;">
<a t-att-href="applicant_url" target="_blank"
style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none;
font-weight: bold; border-radius: 5px; display: inline-block;">
Review Application
</a>
</p>
<p>If you have any questions, please reach out.</p>
<p>Best Regards,
<br/>
<strong>
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
</strong>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_post_onboarding_form" model="mail.template">
<field name="name">Joining Formalities Notification</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.email_from }}</field>
<field name="subject">Welcome Onboard | Joining Formalities | FTPROTECH</field>
<field name="description">
Notification sent to applicants with joining formalities details.
</field>
<field name="body_html" type="html">
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
<t t-if="object.employee_code">
<t t-set="employee_code" t-value="object.employee_code"/>
</t>
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
<p>Dear
<strong>
<t t-esc="applicant_name">Applicant</t>
</strong>
,
</p>
<p>Welcome to the <strong>FTPROTECH</strong> family! 🎉
</p>
<p>We are excited to have you on board. Please take some time to complete the joining formalities on
your first day.
</p>
<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 + '/FTPROTECH/JoiningForm/%s' % object.id"/>
<p style="text-align: center; margin-top: 20px;">
<a t-att-href="form_url" target="_blank"
style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none;
font-weight: bold; border-radius: 5px; display: inline-block;">
Fill Out the Joining Form
</a>
</p>
<t t-if="object.employee_code">
<p>
<strong>Note Your Employee Code:</strong>
<t t-esc="employee_code"/>
(Mentioned in your offer letter)
</p>
</t>
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
<p>For HR records, please provide soft copies of the following documents:</p>
<!-- Personal Documents -->
<t t-if="ctx.get('personal_docs')">
<strong>Personal Documents:</strong>
<ul>
<t t-foreach="ctx.get('personal_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
<!-- Education Documents -->
<t t-if="ctx.get('education_docs')">
<strong>Education Documents:</strong>
<ul>
<t t-foreach="ctx.get('education_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
<!-- Previous Employer Documents -->
<t t-if="ctx.get('previous_employer_docs')">
<strong>Previous Employer Documents:</strong>
<ul>
<t t-foreach="ctx.get('previous_employer_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
<!-- Additional Documents -->
<t t-if="ctx.get('other_docs')">
<strong>Additional Documents:</strong>
<ul>
<t t-foreach="ctx.get('other_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
</t>
<p>If you have any questions while filling out the form, feel free to reach out to us at
<a href="mailto:hr@ftprotech.com" style="color: #007bff; text-decoration: none;">
hr@ftprotech.com</a>.
</p>
<p>Looking forward to welcoming you!</p>
<p>Best Regards,
<br/>
<strong>
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
</strong>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_candidate_approval" model="mail.template">
<field name="name">Candidate Approval Request</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ ctx['recruitment_manager'].email }}</field>
<field name="subject">Approval Required: Candidate {{ object.candidate_id.partner_name }}</field>
<field name="description">Request for approval from the Recruitment Manager for candidate progression.
</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['recruitment_manager'].name">Recruitment Manager</t>,
<br/>
<br/>
The following candidate requires your approval to proceed to the next stage of the hiring
process:
<ul>
<li>
<strong>Candidate Name:</strong>
<t t-esc="object.candidate_id.partner_name">Candidate</t>
</li>
<li>
<strong>Current Stage:</strong>
<t t-esc="object.recruitment_stage_id.name">Stage</t>
</li>
<li>
<strong>Applied Job Position:</strong>
<t t-esc="object.job_id.name">Job Position</t>
</li>
</ul>
<br/>
Please review the candidates details and provide your approval.
<br/>
<t t-set="base_url"
t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="approval_url"
t-value="base_url + '/web?#id=%s&amp;model=hr.applicant&amp;view_type=form' % object.id"/>
<strong>Application:</strong>
<a t-att-href="approval_url" target="_blank" style="color: #007bff; text-decoration: none;">
Click here to view details
</a>
<br/>
<br/>
Regards,
<br/>
<t t-esc="object.user_id.name or 'Recruitment Team'">Recruitment Team</t>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_stage_approved" model="mail.template">
<field name="name">Candidate Stage Approved</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ ctx['recruitment_manager'].email }}</field>
<field name="email_to">{{ object.user_id.email }}</field>
<field name="subject">Candidate {{ object.candidate_id.partner_name }} Approved for Next Stage</field>
<field name="description">Notification that the candidate has been approved to proceed to the next stage.
</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="object.user_id.name">Recruiter</t>,
<br/>
<br/>
The candidate
<strong>
<t t-esc="object.candidate_id.partner_name">Candidate</t>
</strong>
has been approved by the Recruitment Manager
and has progressed to the next stage of the hiring process.
<br/>
<br/>
<ul>
<li>
<strong>Candidate Name:</strong>
<t t-esc="object.candidate_id.partner_name"/>
</li>
<li>
<strong>Approved Stage:</strong>
<t t-esc="object.recruitment_stage_id.name"/>
</li>
<li>
<strong>Applied Job Position:</strong>
<t t-esc="object.job_id.name"/>
</li>
</ul>
<br/>
Please proceed with the next steps in the hiring process.
<br/>
<t t-set="base_url"
t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="applicant_url"
t-value="base_url + '/web?#id=%s&amp;model=hr.applicant&amp;view_type=form' % object.id"/>
<br/>
<br/>
<strong>Application:</strong>
<a t-att-href="applicant_url" target="_blank" style="color: #007bff; text-decoration: none;">
Click here to view details
</a>
<br/>
<br/>
Regards,
<br/>
<t t-esc="ctx['recruitment_manager'].name or 'Recruitment Manager'">Recruitment Manager</t>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,12 @@
<odoo>
<data noupdate="1">
<!-- Define a sequence for HRJobRecruitment -->
<record id="seq_hr_job_recruitment" model="ir.sequence">
<field name="name">HR Job Recruitment Sequence</field>
<field name="code">hr.job.recruitment.sequence</field>
<field name="prefix">HR/JR/</field>
<field name="padding">5</field>
<field name="number_next_actual">1</field>
</record>
</data>
</odoo>

View File

@ -1,5 +1,15 @@
# -*- coding: utf-8 -*-
from . import hr_recruitment
from . import hr_job_recruitment
from . import stages
from . import hr_applicant
from . import hr_job
from . import res_partner
from . import candidate_experience
from . import hr_employee_education_employer_family
# from . import resume_pearser
from . import recruitment_attachments
from . import hr_recruitment_source
from . import requisitions
from . import skills

View File

@ -0,0 +1,26 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import date
from datetime import timedelta
import datetime
class CandidateExperience(models.Model):
_name = "candidate.experience"
_description = "Candidate Experience"
_rec_name = 'experience_code'
experience_code = fields.Char('Experience Code')
experience_from = fields.Integer(string="Experience From (Years)")
experience_to = fields.Integer(string="Experience To (Years)")
# active = fields.Boolean()
def name_get(self):
""" Override name_get to display a custom name based on recruitment_sequence and job_id """
result = []
for record in self:
# Combine recruitment_sequence and job_id name for the display name
name = f"{record.experience_code} - {record.experience_from} - {record.experience_To} years"
result.append((record.id, name))
return result

View File

@ -1,19 +1,294 @@
from email.policy import default
from odoo import models, fields, api, _
from dateutil.relativedelta import relativedelta
from datetime import datetime
from odoo.exceptions import ValidationError
class HRApplicant(models.Model):
_inherit = 'hr.applicant'
_track_duration_field = 'recruitment_stage_id'
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
submitted_to_client = fields.Boolean(string="Submitted_to_client", default=False, readonly=True, tracking=True)
client_submission_date = fields.Datetime(string="Submission Date")
@api.model
def _read_group_recruitment_stage_ids(self, stages, domain):
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
job_recruitment_id = self._context.get('default_hr_job_recruitment')
search_domain = []
if job_recruitment_id:
search_domain = [('job_recruitment_ids', '=', job_recruitment_id)] + search_domain
# if stages:
# search_domain = [('id', 'in', stages.ids)] + search_domain
stage_ids = stages.sudo()._search(search_domain, order=stages._order)
return stages.browse(stage_ids)
def write(self, vals):
# user_id change: update date_open
res = super().write(vals)
if vals.get('user_id'):
vals['date_open'] = fields.Datetime.now()
old_interviewers = self.interviewer_ids
# stage_id: track last stage before update
if 'recruitment_stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
if 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
for applicant in self:
vals['last_stage_id'] = applicant.recruitment_stage_id.id
return res
@api.depends('hr_job_recruitment')
def _compute_department(self):
for applicant in self:
applicant.department_id = applicant.hr_job_recruitment.department_id.id
@api.depends('hr_job_recruitment')
def _compute_stage(self):
for applicant in self:
if applicant.hr_job_recruitment:
if not applicant.recruitment_stage_id:
stage_ids = self.env['hr.recruitment.stage'].search([
'|',
('job_recruitment_ids', '=', False),
('job_recruitment_ids', '=', applicant.hr_job_recruitment.id),
('fold', '=', False)
], order='sequence asc', limit=1).ids
applicant.recruitment_stage_id = stage_ids[0] if stage_ids else False
else:
applicant.recruitment_stage_id = False
@api.depends('job_id')
def _compute_user(self):
for applicant in self:
applicant.user_id = applicant.hr_job_recruitment.user_id.id
def init(self):
super().init()
self.env.cr.execute("""
CREATE INDEX IF NOT EXISTS hr_applicant_job_id_recruitment_stage_id_idx
ON hr_applicant(job_id, recruitment_stage_id)
WHERE active IS TRUE
""")
refused_state = fields.Many2one('hr.recruitment.stage', readonly=True, force_save=True)
hr_job_recruitment = fields.Many2one('hr.job.recruitment')
job_id = fields.Many2one('hr.job', related='hr_job_recruitment.job_id', store=True)
recruitment_stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', ondelete='restrict', tracking=True,
compute='_compute_recruitment_stage', store=True, readonly=False,
domain="[('job_recruitment_ids', '=', hr_job_recruitment)]",
copy=False, index=True,
group_expand='_read_group_recruitment_stage_ids')
stage_color = fields.Char(related="recruitment_stage_id.stage_color")
send_second_application_form = fields.Boolean(related='recruitment_stage_id.second_application_form')
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
# holding_offer = fields.HTML()
employee_code = fields.Char(related="employee_id.employee_id")
recruitment_attachments = fields.Many2many(
'recruitment.attachments',
string='Attachments Request')
joining_attachment_ids = fields.One2many('employee.recruitment.attachments','applicant_id',string="Attachments")
attachments_validation_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
approval_required = fields.Boolean(related='recruitment_stage_id.require_approval')
application_submitted = fields.Boolean(string="Application Submitted")
resume = fields.Binary(related='candidate_id.resume', readonly=False, compute_sudo=True)
def submit_to_client(self):
for rec in self:
submitted_count = len(self.sudo().search([('id','!=',rec.id),('submitted_to_client','=',True)]).ids)
if submitted_count >= rec.hr_job_recruitment.no_of_eligible_submissions:
raise ValidationError(_("Max no of submissions for this JD has been reached"))
rec.submitted_to_client = True
rec.client_submission_date = fields.Datetime.now()
def submit_for_approval(self):
for rec in self:
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
if not manager_id:
raise ValidationError(_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
mail_template = self.env.ref('hr_recruitment_extended.email_template_candidate_approval')
# menu_id = self.env.ref('hr_recruitment.menu_crm_case_categ0_act_job').id
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
render_ctx = dict(recruitment_manager=manager_id)
mail_template.with_context(render_ctx).send_mail(
self.id,
force_send=True,
email_layout_xmlid='mail.mail_notification_light')
rec.application_submitted = True
def approve_applicant(self):
for rec in self:
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
if not manager_id:
raise ValidationError(
_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
mail_template = self.env.ref('hr_recruitment_extended.email_template_stage_approved')
# menu_id = self.env.ref('hr_recruitment.menu_crm_case_categ0_act_job').id
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
render_ctx = dict(recruitment_manager=manager_id)
mail_template.with_context(render_ctx).send_mail(
self.id,
force_send=True,
email_layout_xmlid='mail.mail_notification_light')
rec.application_submitted = False
recruitment_stage_ids = rec.hr_job_recruitment.recruitment_stage_ids.ids
current_stage = self.env['hr.recruitment.stage'].browse(rec.recruitment_stage_id.id)
next_stage = self.env['hr.recruitment.stage'].search([
('id', 'in', recruitment_stage_ids),
('sequence', '>', current_stage.sequence)
], order='sequence asc', limit=1)
if next_stage:
rec.recruitment_stage_id = next_stage.id
def action_validate_attachments(self):
for rec in self:
if rec.employee_id and rec.joining_attachment_ids:
rec.joining_attachment_ids.write({'employee_id': rec.employee_id.id})
rec.attachments_validation_status = 'validated'
else:
raise ValidationError(_("No Data to Validate"))
def send_second_application_form_to_candidate(self):
"""Send the salary expectation and experience form to the candidate."""
template = self.env.ref('hr_recruitment_extended.email_template_second_application_form', raise_if_not_found=False)
for applicant in self:
if template and applicant.email_from:
template.send_mail(applicant.id, force_send=True)
applicant.second_application_form_status = 'email_sent_to_candidate'
def send_post_onboarding_form_to_candidate(self):
for rec in self:
if not rec.employee_id:
raise ValidationError(_('You must first create the employee before before Sending the Post Onboarding Form'))
elif not rec.employee_id.employee_id:
raise ValidationError(_('Employee Code for the Employee (%s) is missing')%(rec.employee_id.name))
return {
'type': 'ir.actions.act_window',
'name': 'Select Attachments',
'res_model': 'post.onboarding.attachment.wizard',
'view_mode': 'form',
'view_type': 'form',
'target': 'new',
'context': {'default_attachment_ids': []}
}
def _track_template(self, changes):
res = super(HRApplicant, self)._track_template(changes)
applicant = self[0]
# When applcant is unarchived, they are put back to the default stage automatically. In this case,
# don't post automated message related to the stage change.
if 'recruitment_stage_id' in changes and applicant.exists()\
and applicant.recruitment_stage_id.template_id\
and not applicant._context.get('just_moved')\
and not applicant._context.get('just_unarchived'):
res['recruitment_stage_id'] = (applicant.recruitment_stage_id.template_id, {
'auto_delete_keep_log': False,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'email_layout_xmlid': 'hr_recruitment.mail_notification_light_without_background'
})
return res
def _track_subtype(self, init_values):
record = self[0]
if 'recruitment_stage_id' in init_values and record.recruitment_stage_id:
return self.env.ref('hr_recruitment.mt_applicant_stage_changed')
return super(HRApplicant, self)._track_subtype(init_values)
def message_new(self, msg, custom_values=None):
stage = False
defaults = {}
if custom_values and 'hr_job_recruitment' in custom_values:
recruitment_stage_id = self.env['hr.job.recruitment'].browse(custom_values['hr_job_recruitment'])._get_first_stage()
if stage and stage.id:
defaults['recruitment_stage_id'] = recruitment_stage_id.id
res = super(HRApplicant, self).message_new(msg, custom_values=defaults)
return res
def reset_applicant(self):
""" Reinsert the applicant into the recruitment pipe in the first stage"""
res = super(HRApplicant, self).reset_applicant()
default_stage = dict()
for hr_job_recruitment in self.mapped('hr_job_recruitment'):
default_stage[hr_job_recruitment.id] = self.env['hr.recruitment.stage'].search(
[
('job_recruitment_ids', '=', hr_job_recruitment.id),
('fold', '=', False)
], order='sequence asc', limit=1).id
for applicant in self:
applicant.write(
{'refused_state': False})
return res
{'recruitment_stage_id': applicant.hr_job_recruitment.id and default_stage[applicant.hr_job_recruitment.id],
'refuse_reason_id': False})
@api.depends('recruitment_stage_id.hired_stage')
def _compute_date_closed(self):
for applicant in self:
if applicant.recruitment_stage_id and applicant.recruitment_stage_id.hired_stage and not applicant.date_closed:
applicant.date_closed = fields.datetime.now()
if not applicant.recruitment_stage_id.hired_stage:
applicant.date_closed = False
@api.depends('hr_job_recruitment')
def _compute_recruitment_stage(self):
for applicant in self:
if applicant.hr_job_recruitment:
if not applicant.recruitment_stage_id:
stage_ids = self.env['hr.recruitment.stage'].search([
'|',
('job_recruitment_ids', '=', False),
('job_recruitment_ids', '=', applicant.hr_job_recruitment.id),
('fold', '=', False)
], order='sequence asc', limit=1).ids
applicant.recruitment_stage_id = stage_ids[0] if stage_ids else False
else:
applicant.recruitment_stage_id = False
def _get_duration_from_tracking(self, trackings):
json = super()._get_duration_from_tracking(trackings)
now = datetime.now()
for applicant in self:
if applicant.refuse_reason_id and applicant.refuse_date:
json[applicant.recruitment_stage_id.id] -= (now - applicant.refuse_date).total_seconds()
return json
def create_employee_from_applicant(self):
self.ensure_one()
action = self.candidate_id.create_employee_from_candidate()
employee = self.env['hr.employee'].browse(action['res_id'])
employee.write({
'image_1920': self.candidate_image,
'job_id': self.job_id.id,
'job_title': self.job_id.name,
'department_id': self.department_id.id,
'work_email': self.department_id.company_id.email or self.email_from, # To have a valid email address by default
'work_phone': self.department_id.company_id.phone,
})
return action
class ApplicantGetRefuseReason(models.TransientModel):
_inherit = 'applicant.get.refuse.reason'

View File

@ -0,0 +1,58 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class HRApplicant(models.Model):
_inherit = 'hr.applicant'
education_history = fields.One2many('education.history', 'applicant_id', string='Education Details')
employer_history = fields.One2many('employer.history', 'applicant_id', string='Education Details')
family_details = fields.One2many('family.details', 'applicant_id', string='Family Details')
family_education_employer_details_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
def action_validate_family_education_employer_details(self):
for rec in self:
if rec.employee_id and (rec.education_history or rec.employer_history or rec.family_details):
rec.education_history.write({'employee_id': rec.employee_id.id})
rec.employer_history.write({'employee_id': rec.employee_id.id})
rec.family_details.write({'employee_id': rec.employee_id.id})
rec.family_education_employer_details_status = 'validated'
else:
raise ValidationError(_("No Data to Validate"))
class HRCandidate(models.Model):
_inherit = 'hr.candidate'
education_history = fields.One2many('education.history', 'candidate_id', string='Education Details')
employer_history = fields.One2many('employer.history', 'candidate_id', string='Education Details')
family_details = fields.One2many('family.details', 'candidate_id', string='Family Details')
class FamilyDetails(models.Model):
_inherit = "family.details"
applicant_id = fields.Many2one('hr.applicant')
candidate_id = fields.Many2one('hr.candidate', related = 'applicant_id.candidate_id', readonly=False)
class EducationHistory(models.Model):
_inherit = "education.history"
applicant_id = fields.Many2one('hr.applicant')
candidate_id = fields.Many2one('hr.candidate', related = 'applicant_id.candidate_id', readonly=False)
class EmployerHistory(models.Model):
_inherit = 'employer.history'
applicant_id = fields.Many2one('hr.applicant')
candidate_id = fields.Many2one('hr.candidate', related='applicant_id.candidate_id', readonly=False)

View File

@ -0,0 +1,51 @@
from odoo import fields, api, models
class HRJob(models.Model):
_inherit = 'hr.job'
require_no_of_recruitment = fields.Integer(string='Target', copy=False,
help='Number of new employees you expect to recruit.', default=1, compute="_compute_no_of_recruitment")
hr_job_recruitments = fields.One2many('hr.job.recruitment', 'job_id', string='Recruitments')
@api.depends('hr_job_recruitments')
def _compute_no_of_recruitment(self):
for record in self:
# Sum the no_of_recruitment from the related hr.job.recruitment records
record.require_no_of_recruitment = sum(rec.no_of_recruitment for rec in record.hr_job_recruitments)
def write(self, vals):
if 'date_to' in vals:
vals.pop('date_to')
res = super().write(vals)
return res
def _compute_new_application_count(self):
self.env.cr.execute(
"""
WITH job_stage AS (
SELECT DISTINCT ON (j.id) j.id AS hr_job_recruitment, j.job_id AS job_id, s.id AS stage_id, s.sequence AS sequence
FROM hr_job_recruitment j
LEFT JOIN hr_job_recruitment_hr_recruitment_stage_rel rel
ON rel.hr_job_recruitment_id = j.id
JOIN hr_recruitment_stage s
ON s.id = rel.hr_recruitment_stage_id
WHERE j.job_id IN %s
ORDER BY j.id, s.sequence ASC
)
SELECT js.job_id, COALESCE(SUM(CASE WHEN a.id IS NOT NULL THEN 1 ELSE 0 END), 0) AS new_applicant
FROM job_stage js
LEFT JOIN hr_applicant a
ON a.hr_job_recruitment = js.hr_job_recruitment
AND a.recruitment_stage_id = js.stage_id
AND a.active IS TRUE
AND (a.company_id IN %s OR a.company_id IS NULL)
GROUP BY js.job_id;
""", [tuple(self.ids), tuple(self.env.companies.ids)]
)
new_applicant_count = dict(self.env.cr.fetchall())
for job in self:
job.new_application_count = new_applicant_count.get(job.id, 0)

View File

@ -0,0 +1,396 @@
from odoo import models, fields, api, _
from datetime import date
from odoo.exceptions import ValidationError
from datetime import timedelta
import datetime
class HRJobRecruitment(models.Model):
_name = 'hr.job.recruitment'
_inherit = ['mail.thread', 'mail.activity.mixin']
_inherits = {'hr.job': 'job_id'}
_rec_name = 'recruitment_sequence'
active = fields.Boolean(default=True)
_sql_constraints = [
('unique_recruitment_sequence', 'UNIQUE(recruitment_sequence)', 'Recruitment sequence must be unique!')
]
def _get_first_stage(self):
self.ensure_one()
return self.env['hr.recruitment.stage'].search([
('job_recruitment_ids', '=', self.id)], order='sequence asc', limit=1)
def _compute_application_count(self):
read_group_result = self.env['hr.applicant']._read_group([('hr_job_recruitment', 'in', self.ids)], ['hr_job_recruitment'], ['__count'])
result = {job.id: count for job, count in read_group_result}
for job in self:
job.application_count = result.get(job.id, 0)
def _compute_all_application_count(self):
read_group_result = self.env['hr.applicant'].with_context(active_test=False)._read_group([
('hr_job_recruitment', 'in', self.ids),
'|',
('active', '=', True),
'&',
('active', '=', False), ('refuse_reason_id', '!=', False),
], ['hr_job_recruitment'], ['__count'])
result = {job.id: count for job, count in read_group_result}
for job in self:
job.all_application_count = result.get(job.id, 0)
def _compute_applicant_hired(self):
hired_stages = self.env['hr.recruitment.stage'].search([('hired_stage', '=', True)])
hired_data = self.env['hr.applicant']._read_group([
('hr_job_recruitment', 'in', self.ids),
('recruitment_stage_id', 'in', hired_stages.ids),
], ['hr_job_recruitment'], ['__count'])
job_hires = {job.id: count for job, count in hired_data}
for job in self:
job.applicant_hired = job_hires.get(job.id, 0)
def _compute_new_application_count(self):
self.env.cr.execute(
"""
WITH job_stage AS (
SELECT DISTINCT ON (j.id) j.id AS hr_job_recruitment, s.id AS stage_id, s.sequence AS sequence
FROM hr_job_recruitment j
LEFT JOIN hr_job_recruitment_hr_recruitment_stage_rel rel
ON rel.hr_job_recruitment_id = j.id
JOIN hr_recruitment_stage s
ON s.id = rel.hr_recruitment_stage_id
WHERE j.id IN %s
ORDER BY j.id, s.sequence ASC
)
SELECT s.hr_job_recruitment, COUNT(a.id) AS new_applicant
FROM job_stage s
LEFT JOIN hr_applicant a
ON a.hr_job_recruitment = s.hr_job_recruitment
AND a.recruitment_stage_id = s.stage_id
AND a.active IS TRUE
AND (a.company_id IN %s OR a.company_id IS NULL)
GROUP BY s.hr_job_recruitment;
""", [tuple(self.ids), tuple(self.env.companies.ids)]
)
new_applicant_count = dict(self.env.cr.fetchall())
for job in self:
job.new_application_count = new_applicant_count.get(job.id, 0)
# # display_name = fields.Char(string='Name', compute='_compute_display_name', store=True)
application_count = fields.Integer(compute='_compute_application_count', string="Application Count")
all_application_count = fields.Integer(compute='_compute_all_application_count', string="All Application Count")
new_application_count = fields.Integer(
compute='_compute_new_application_count', string="New Application",
help="Number of applications that are new in the flow (typically at first step of the flow)")
applicant_hired = fields.Integer(compute='_compute_applicant_hired', string="Applicants Hired")
def _get_default_favorite_user_ids(self):
return [(6, 0, [self.env.uid])]
@api.model
def _default_address_id(self):
last_used_address = self.env['hr.job.recruitment'].search([('company_id', 'in', self.env.companies.ids)], order='id desc',
limit=1)
if last_used_address:
return last_used_address.address_id
else:
return self.env.company.partner_id
@api.onchange('job_id')
def onchange_job_id(self):
for rec in self:
if rec.job_id and not rec.description:
rec.description = rec.job_id.description
job_id = fields.Many2one('hr.job', required=True)
name = fields.Char(string='Job Position', required=True, index='trigram', translate=True, related='job_id.name')
recruitment_sequence = fields.Char(string='Recruitment Sequence', readonly=False, default='/', copy=False)
favorite_user_ids = fields.Many2many('res.users', 'job_recruitment_favorite_user_rel', 'job_id', 'user_id', default=_get_default_favorite_user_ids)
secondary_skill_ids = fields.Many2many('hr.skill', "hr_job_recruitment_hr_skill_rel",
'job_recruitment_id', 'hr_skill_id', "Secondary Skills")
no_of_recruitment = fields.Integer(string='Target', copy=False,
help='Number of new employees you expect to recruit.', default=1)
no_of_eligible_submissions = fields.Integer(string='Eligible Submissions', copy=False,
help='Number of Submissions you expected to send.', default=1)
@api.onchange("no_of_recruitment")
def onchange_no_of_recruitments(self):
for rec in self:
if rec.no_of_eligible_submissions <= 1:
rec.no_of_eligible_submissions = rec.no_of_recruitment
locations = fields.Many2many('hr.location')
target_from = fields.Date(string="This is the date in which we starting the recruitment process",
default=fields.Date.today)
target_to = fields.Date(string='This is the target end date')
# 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)
department_id = fields.Many2one('hr.department', string='Department', check_company=True)
description = fields.Html(string='Job Description', sanitize_attributes=False)
requirements = fields.Text('Requirements')
expected_employees = fields.Integer(compute='_compute_employees', string='Total Forecasted Employees', store=True,
help='Expected number of employees for this job position after new recruitment.')
no_of_employee = fields.Integer(compute='_compute_employees', string="Current Number of Employees", store=True,
help='Number of employees currently occupying this job position.')
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type')
# active = fields.Boolean(default=True)
user_id = fields.Many2one('res.users', "Recruiter",
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
default=lambda self: self.env.user,
tracking=True, help="The Recruiter will be the default value for all Applicants in this job \
position. The Recruiter is automatically added to all meetings with the Applicant.")
interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]", help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills")
address_id = fields.Many2one(
'res.partner', "Job Location", default=_default_address_id,
domain="[('is_company','=',True),('contact_type','=',recruitment_type)]",
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.")
recruitment_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='internal')
requested_by = fields.Many2one('res.partner', string="Requested By",
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]")
@api.onchange('recruitment_type')
def _onchange_recruitment_type(self):
self.requested_by = False
self.address_id = False
@api.onchange('requested_by')
def _onchange_requested_by(self):
for rec in self:
if rec.requested_by.parent_id:
rec.address_id = rec.requested_by.parent_id.id
elif rec.requested_by.is_company:
rec.address_id = rec.requested_by.id
def _fetch_requested_by_internal_domain(self):
return """
[('is_client','=',False)]
"""
def _fetch_requested_external_domain(self):
return """
[('is_client','=',True)]
"""
document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Documents", readonly=True)
documents_count = fields.Integer(compute='_compute_document_ids', string="Document Count")
color = fields.Integer("Color Index")
application_ids = fields.One2many('hr.applicant', 'hr_job_recruitment', "Job Applications")
no_of_hired_employee = fields.Integer(
compute='_compute_no_of_hired_employee',
string='Hired', copy=False,
help='Number of hired employees for this job position during recruitment phase.',
store=True)
no_of_submissions = fields.Integer(
compute='_compute_no_of_submissions',
string='Hired', copy=False,
help='Number of Application submissions for this job position during recruitment phase.',
)
no_of_refused_submissions = fields.Integer(
compute='_compute_no_of_refused_submissions',
string='Hired', copy=False,
help='Number of Refused Application submissions for this job position during recruitment phase.',
)
@api.depends('application_ids.submitted_to_client')
def _compute_no_of_submissions(self):
counts = dict(self.env['hr.applicant']._read_group(
domain=[
('hr_job_recruitment', 'in', self.ids),
('submitted_to_client', '!=', False),
'|',
('active', '=', False),
('active', '=', True),
],
groupby=['hr_job_recruitment'],
aggregates=['__count']))
for job in self:
job.no_of_submissions = counts.get(job, 0)
@api.depends('application_ids.application_status')
def _compute_no_of_refused_submissions(self):
counts = dict(self.env['hr.applicant']._read_group(
domain=[
('hr_job_recruitment', 'in', self.ids),
('submitted_to_client', '!=', False),
('application_status', '=', 'refused'),
'|',
('active', '=', False),
('active', '=', True),
],
groupby=['hr_job_recruitment'],
aggregates=['__count']))
for job in self:
job.no_of_refused_submissions = counts.get(job, 0)
@api.depends('application_ids.date_closed')
def _compute_no_of_hired_employee(self):
counts = dict(self.env['hr.applicant']._read_group(
domain=[
('hr_job_recruitment', 'in', self.ids),
('date_closed', '!=', False),
'|',
('active', '=', False),
('active', '=', True),
],
groupby=['hr_job_recruitment'],
aggregates=['__count']))
for job in self:
job.no_of_hired_employee = counts.get(job, 0)
def action_open_activities(self):
action = self.env["ir.actions.actions"]._for_xml_id("hr_recruitment_extended.action_hr_job_recruitment_applications")
views = ['activity'] + [view for view in action['view_mode'].split(',') if view != 'activity']
action['view_mode'] = ','.join(views)
action['views'] = [(False, view) for view in views]
return action
def _compute_document_ids(self):
applicants = self.mapped('application_ids').filtered(lambda self: not self.employee_id)
app_to_job = dict((applicant.id, applicant.hr_job_recruitment.id) for applicant in applicants)
attachments = self.env['ir.attachment'].search([
'|',
'&', ('res_model', '=', 'hr.job.recruitment'), ('res_id', 'in', self.ids),
'&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicants.ids)])
result = dict.fromkeys(self.ids, self.env['ir.attachment'])
for attachment in attachments:
if attachment.res_model == 'hr.applicant':
result[app_to_job[attachment.res_id]] |= attachment
else:
result[attachment.res_id] |= attachment
for job in self:
job.document_ids = result.get(job.id, False)
job.documents_count = len(job.document_ids)
@api.depends('no_of_recruitment', 'employee_ids.job_id', 'employee_ids.active')
def _compute_employees(self):
employee_data = self.env['hr.employee']._read_group([('job_id', 'in', self.job_id.ids)], ['job_id'], ['__count'])
result = {job.id: count for job, count in employee_data}
for job in self:
job.no_of_employee = result.get(job.job_id.id, 0)
job.expected_employees = result.get(job.job_id.id, 0) + job.no_of_recruitment
def _compute_is_favorite(self):
for job in self:
job.is_favorite = self.env.user in job.favorite_user_ids
def _inverse_is_favorite(self):
unfavorited_jobs = favorited_jobs = self.env['hr.job.recruitment']
for job in self:
if self.env.user in job.favorite_user_ids:
unfavorited_jobs |= job
else:
favorited_jobs |= job
favorited_jobs.write({'favorite_user_ids': [(4, self.env.uid)]})
unfavorited_jobs.write({'favorite_user_ids': [(3, self.env.uid)]})
def name_get(self):
""" Override name_get to display a custom name based on recruitment_sequence and job_id """
result = []
for record in self:
# Combine recruitment_sequence and job_id name for the display name
name = f"{record.recruitment_sequence} - {record.job_id.name}" if record.job_id else record.recruitment_sequence
result.append((record.id, name))
return result
def buttion_view_applicants(self):
if self.skill_ids:
a = self.env['hr.candidate'].search([])
applicants = self.env['hr.candidate']
for i in a:
if all(skill in i.skill_ids for skill in self.skill_ids):
applicants += i
else:
applicants = self.env['hr.candidate'].search([])
action = self.env['ir.actions.act_window']._for_xml_id('hr_recruitment.action_hr_candidate')
action['domain'] = [('id', 'in', applicants.ids)]
action['context'] = dict(self._context)
return action
def hr_job_recruitment_end_date_update(self):
tomorrow_date = fields.Date.today() + timedelta(days=1)
jobs_ending_tomorrow = self.sudo().search([('target_to', '=', tomorrow_date)])
for job in jobs_ending_tomorrow:
# Fetch recruiters (assuming job has a field recruiter_id or similar)
recruiter = job.sudo().user_id # Replacne with the appropriate field name
if recruiter:
# Send mail
template = self.env.ref(
'hr_recruitment_extended.template_recruitment_deadline_alert') # Replace with your email template XML ID
if template:
template.sudo().send_mail(recruiter.id, force_send=True)
return True
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
vals["favorite_user_ids"] = vals.get("favorite_user_ids", [])
if vals.get('recruitment_sequence', '/') == '/':
vals['recruitment_sequence'] = self.env['ir.sequence'].next_by_code(
'hr.job.recruitment.sequence') or '/'
jobs = super().create(vals_list)
utm_linkedin = self.env.ref("utm.utm_source_linkedin", raise_if_not_found=False)
if utm_linkedin:
source_vals = [{
'source_id': utm_linkedin.id,
'job_recruitment_id': job.id,
} for job in jobs]
self.env['hr.recruitment.source'].create(source_vals)
jobs.sudo().interviewer_ids._create_recruitment_interviewers()
# Automatically subscribe the department manager and the recruiter to a job position.
for job in jobs:
job.message_subscribe(
job.manager_id._get_related_partners().ids + job.user_id.partner_id.ids
)
return jobs
def buttion_view_applicants(self):
pass
def action_open_attachments(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'ir.attachment',
'name': _('Documents'),
'context': {
'default_res_model': self._name,
'default_res_id': self.ids[0],
'show_partner_name': 1,
},
'view_mode': 'list',
'views': [
(self.env.ref('hr_recruitment.ir_attachment_hr_recruitment_list_view').id, 'list')
],
'search_view_id': self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').ids,
'domain': ['|',
'&', ('res_model', '=', 'hr.job.recruitment'), ('res_id', 'in', self.ids),
'&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.application_ids.ids),
],
}
class HRSkill(models.Model):
_inherit = 'hr.skill'
job_recruitment_id = fields.Many2one('hr.job.recruitment')

View File

@ -1,110 +1,120 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from odoo.exceptions import ValidationError, UserError
from datetime import date
from datetime import timedelta
import datetime
#
# class Job(models.Model):
# _inherit = 'hr.job'
#
# hiring_history = fields.One2many('recruitment.status.history', 'job_id', string='History')
class Job(models.Model):
_inherit = 'hr.job'
secondary_skill_ids = fields.Many2many('hr.skill', "hr_job_secondary_hr_skill_rel",
'hr_job_id', 'hr_skill_id', "Secondary Skills")
locations = fields.Many2many('hr.location')
target_from = fields.Date(string="This is the date in which we starting the recruitment process", default=fields.Date.today)
target_to = fields.Date(string='This is the target end date')
hiring_history = fields.One2many('recruitment.status.history', 'job_id', string='History')
def buttion_view_applicants(self):
if self.skill_ids:
a = self.env['hr.candidate'].search([])
applicants = self.env['hr.candidate']
for i in a:
if all(skill in i.skill_ids for skill in self.skill_ids):
applicants += i
else:
applicants = self.env['hr.candidate'].search([])
action = self.env['ir.actions.act_window']._for_xml_id('hr_recruitment.action_hr_candidate')
action['domain'] = [('id', 'in', applicants.ids)]
action['context'] = dict(self._context)
return action
def hr_job_end_date_update(self):
# Find all jobs where the target_to is today's date
hr_jobs = self.sudo().search([('target_to', '=', fields.Date.today() - timedelta(days=1))])
# stage_ids = self.env['hr.recruitment.stage'].sudo().search([('hired_stage','=',True)])
for job in hr_jobs:
# Determine the hiring period
date_from = job.target_from
date_end = job.target_to
# Fetch hired applicants related to this job
hired_applicants = self.env['hr.applicant'].search([
('job_id', '=', job.id),
('stage_id.hired_stage', '=', True)
])
# Get today's date in the datetime format (with time set to midnight)
today_start = fields.Datetime.today()
# Get today's date at the end of the day (23:59:59) to include all records created today
today_end = fields.Datetime.today().now()
# Search for records where create_date is today
hiring_history_today = self.env['recruitment.status.history'].sudo().search([
('create_date', '>=', today_start),
('create_date', '<=', today_end),
('job_id','=',job.id)
])
# Create a hiring history record
if not hiring_history_today:
self.env['recruitment.status.history'].create({
'date_from': date_from,
'date_end': date_end,
'target': len(hired_applicants), # Number of hired applicants
'job_id': job.id,
'hired': [(6, 0, hired_applicants.ids)] # Many2many write operation
})
tomorrow_date = fields.Date.today() + timedelta(days=1)
jobs_ending_tomorrow = self.sudo().search([('target_to', '=', tomorrow_date)])
for job in jobs_ending_tomorrow:
# Fetch recruiters (assuming job has a field recruiter_id or similar)
recruiter = job.sudo().user_id # Replacne with the appropriate field name
if recruiter:
# Send mail
template = self.env.ref(
'hr_recruitment_extended.template_recruitment_deadline_alert') # Replace with your email template XML ID
if template:
template.sudo().send_mail(recruiter.id, force_send=True)
recruitment_history = self.env['recruitment.status.history'].sudo().search([('job_id','!=',False)])
for recruitment in recruitment_history:
# Determine the hiring period
if recruitment.date_from and recruitment.job_id:
# Use `date_end` or today's date if `date_end` is not provided
date_end = fields.Datetime.to_datetime(fields.Date.to_string(recruitment.date_end)) + datetime.timedelta(days=1,seconds=-1) or fields.Datetime.today().now()
current_hired_applicants = recruitment.hired
# Search for applicants matching the conditions
hired_applicants = self.env['hr.applicant'].search([
('date_closed', '>=', fields.Datetime.to_datetime(fields.Date.to_string(recruitment.date_from))),
('date_closed', '<=', date_end),
('job_id', '=', recruitment.job_id.id)
])
# Filter out the applicants that are already in the 'hired' field
new_hired_applicants = hired_applicants - current_hired_applicants
# Add the missing applicants to the 'hired' field
recruitment.hired = current_hired_applicants | new_hired_applicants
return True
class HrCandidate(models.Model):
_inherit = "hr.candidate"
first_name = fields.Char(string='First Name',required=True, help="This is the person's first name, given at birth or during a naming ceremony. Its the name people use to address you.")
#personal Details
first_name = fields.Char(string='First Name',required=False, help="This is the person's first name, given at birth or during a naming ceremony. Its the name people use to address you.")
middle_name = fields.Char(string='Middle Name', help="This is an extra name that comes between the first name and last name. Not everyone has a middle name")
last_name = fields.Char(string='Last Name',required=True, help="This is the family name, shared with other family members. Its usually the last name.")
last_name = fields.Char(string='Last Name',required=False, help="This is the family name, shared with other family members. Its usually the last name.")
alternate_phone = fields.Char(string='Alternate Phone')
candidate_image = fields.Image()
employee_code = fields.Char(related="employee_id.employee_id")
resume = fields.Binary()
def create_employee_from_candidate(self):
self.ensure_one()
self._check_interviewer_access()
if not self.partner_id:
if not self.partner_name:
raise UserError(_('Please provide an candidate name.'))
self.partner_id = self.env['res.partner'].create({
'is_company': False,
'name': self.partner_name,
'email': self.email_from,
})
action = self.env['ir.actions.act_window']._for_xml_id('hr.open_view_employee_list')
employee = self.env['hr.employee'].create(self._get_employee_create_vals())
action['res_id'] = employee.id
employee.write({
'image_1920': self.candidate_image})
return action
#
# doj = fields.Date(tracking=True)
# gender = fields.Selection([
# ('male', 'Male'),
# ('female', 'Female'),
# ('other', 'Other')
# ], tracking=True)
# birthday = fields.Date(tracking=True)
#
# blood_group = fields.Selection([
# ('A+', 'A+'),
# ('A-', 'A-'),
# ('B+', 'B+'),
# ('B-', 'B-'),
# ('O+', 'O+'),
# ('O-', 'O-'),
# ('AB+', 'AB+'),
# ('AB-', 'AB-'),
# ], string="Blood Group")
#
# private_street = fields.Char(string="Private Street", groups="hr.group_hr_user")
# private_street2 = fields.Char(string="Private Street2", groups="hr.group_hr_user")
# private_city = fields.Char(string="Private City", groups="hr.group_hr_user")
# private_state_id = fields.Many2one(
# "res.country.state", string="Private State",
# domain="[('country_id', '=?', private_country_id)]",
# groups="hr.group_hr_user")
# private_zip = fields.Char(string="Private Zip", groups="hr.group_hr_user")
# private_country_id = fields.Many2one("res.country", string="Private Country", groups="hr.group_hr_user")
#
# permanent_street = fields.Char(string="permanent Street", groups="hr.group_hr_user")
# permanent_street2 = fields.Char(string="permanent Street2", groups="hr.group_hr_user")
# permanent_city = fields.Char(string="permanent City", groups="hr.group_hr_user")
# permanent_state_id = fields.Many2one(
# "res.country.state", string="permanent State",
# domain="[('country_id', '=?', private_country_id)]",
# groups="hr.group_hr_user")
# permanent_zip = fields.Char(string="permanent Zip", groups="hr.group_hr_user")
# permanent_country_id = fields.Many2one("res.country", string="permanent Country", groups="hr.group_hr_user")
#
# marital = fields.Selection(
# selection='_get_marital_status_selection',
# string='Marital Status',
# groups="hr.group_hr_user",
# default='single',
# required=True,
# tracking=True)
#
# marriage_anniversary_date = fields.Date(string='Anniversary Date' ,tracking=True)
#
# #bank Details:
#
# full_name_as_in_bank = fields.Char(string='Full Name (as per bank)' ,tracking=True)
# bank_name = fields.Char(string='Bank Name' ,tracking=True)
# bank_branch = fields.Char(string='Bank Branch' ,tracking=True)
# bank_account_no = fields.Char(string='Bank Account N0' ,tracking=True)
# bank_ifsc_code = fields.Char(string='Bank IFSC Code' ,tracking=True)
#
#
# #passport details:
# passport_no = fields.Char(string="Passport No",tracking=True)
# passport_start_date = fields.Date(string="Start Date",tracking=True)
# passport_end_date = fields.Date(string="End Date",tracking=True)
# passport_issued_location = fields.Char(string="Start Date",tracking=True)
#
# #authotentication Details
# pan_no = fields.Char(string='PAN No',tracking=True)
# identification_id = fields.Char(string='Aadhar No',tracking=True)
# previous_company_pf_no = fields.Char(string='Previous Company PF No',tracking=True)
# previous_company_uan_no = fields.Char(string='Previous Company UAN No',tracking=True)
#
@api.constrains('partner_name')
def partner_name_constrain(self):
@ -139,6 +149,215 @@ class HRApplicant(models.Model):
applicant_comments = fields.Text(string='Applicant Comments')
recruiter_comments = fields.Text(string='Recruiter Comments')
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")
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)
personal_details_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
#contact details
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")
contact_details_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
# 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)
bank_details_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
# 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)
passport_details_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
# authentication Details
pan_no = fields.Char(string='PAN No', tracking=True)
identification_id = fields.Char(string='Aadhar No', tracking=True)
previous_company_pf_no = fields.Char(string='Previous Company PF No', tracking=True)
previous_company_uan_no = fields.Char(string='Previous Company UAN No', tracking=True)
authentication_details_status = fields.Selection([('pending', 'Pending'),
('validated', 'Validated')], default='pending')
def _get_marital_status_selection(self):
return [
('single', _('Single')),
('married', _('Married')),
('cohabitant', _('Legal Cohabitant')),
('widower', _('Widower')),
('divorced', _('Divorced')),
]
def action_validate_personal_details(self):
for rec in self:
if rec.employee_id:
vals = dict()
vals['doj'] = rec.doj if rec.doj else ''
vals['gender'] = rec.gender if rec.gender else ''
vals['birthday'] = rec.birthday if rec.birthday else ''
vals['blood_group'] = rec.blood_group if rec.blood_group else ''
vals['marital'] = rec.marital if rec.marital else ''
vals['marriage_anniversary_date'] = rec.marriage_anniversary_date if rec.marriage_anniversary_date else ''
vals = {k: v for k, v in vals.items() if v != '' and v != 0}
if len(vals) > 0:
rec.personal_details_status = 'validated'
else:
raise ValidationError(_("No values to validate"))
rec.employee_id.write(vals)
def action_validate_contact_details(self):
for rec in self:
if rec.employee_id:
vals = dict()
# Current Address
vals['private_street'] = rec.private_street if rec.private_street else ''
vals['private_street2'] = rec.private_street2 if rec.private_street2 else ''
vals['private_city'] = rec.private_city if rec.private_city else ''
vals['private_state_id'] = rec.private_state_id.id if rec.private_state_id else ''
vals['private_zip'] = rec.private_zip if rec.private_zip else ''
vals['private_country_id'] = rec.private_country_id.id if rec.private_country_id else ''
# Permanent Address
vals['permanent_street'] = rec.permanent_street if rec.permanent_street else ''
vals['permanent_street2'] = rec.permanent_street2 if rec.permanent_street2 else ''
vals['permanent_city'] = rec.permanent_city if rec.permanent_city else ''
vals['permanent_state_id'] = rec.permanent_state_id.id if rec.permanent_state_id else ''
vals['permanent_zip'] = rec.permanent_zip if rec.permanent_zip else ''
vals['permanent_country_id'] = rec.permanent_country_id.id if rec.permanent_country_id else ''
# Remove empty/False values from dict
vals = {k: v for k, v in vals.items() if v not in ['', False, 0]}
if len(vals) > 0:
rec.contact_details_status = 'validated'
rec.employee_id.write(vals)
else:
raise ValidationError(_("No values to validate"))
def action_validate_bank_details(self):
for rec in self:
if rec.employee_id and rec.full_name_as_in_bank and rec.bank_name and rec.bank_branch and rec.bank_account_no and rec.bank_ifsc_code:
account_no = self.env['res.partner.bank'].sudo().search([('acc_number','=',rec.bank_account_no)],limit=1)
if account_no:
rec.employee_id.bank_account_id = account_no.id
else:
bank = self.env['res.bank'].sudo().search([('bic','=',rec.bank_ifsc_code)],limit=1)
if bank:
bank_id = bank
else:
bank_id = self.env['res.bank'].sudo().create({
'name': rec.bank_name,
'bic': rec.bank_ifsc_code,
'branch': rec.bank_branch
})
partner_bank = rec.env['res.partner.bank'].sudo().create({
'acc_number': rec.bank_account_no,
'bank_id': bank_id.id,
'full_name': rec.full_name_as_in_bank,
'partner_id': rec.employee_id.work_contact_id.id | rec.employee_id.user_id.partner_id.id
})
rec.employee_id.bank_account_id = partner_bank.id
rec.bank_details_status = 'validated'
else:
raise ValidationError(_("Please Provide all the Bank Related Details"))
def action_validate_passport_details(self):
for rec in self:
if rec.employee_id:
vals = dict()
# Current Address
vals['passport_id'] = rec.passport_no if rec.passport_no else ''
vals['passport_start_date'] = rec.passport_start_date if rec.passport_start_date else ''
vals['passport_end_date'] = rec.passport_end_date if rec.passport_end_date else ''
vals['passport_issued_location'] = rec.passport_issued_location if rec.passport_issued_location else ''
# Remove empty/False values from dict
vals = {k: v for k, v in vals.items() if v not in ['', False, 0]}
if len(vals) > 0:
rec.passport_details_status = 'validated'
rec.employee_id.write(vals)
else:
raise ValidationError(_("No values to validate"))
def action_validate_authentication_details(self):
for rec in self:
if rec.employee_id:
vals = dict()
# Current Address
vals['pan_no'] = rec.pan_no if rec.pan_no else ''
vals['identification_id'] = rec.identification_id if rec.identification_id else ''
vals['previous_company_pf_no'] = rec.previous_company_pf_no if rec.previous_company_pf_no else ''
vals['previous_company_uan_no'] = rec.previous_company_uan_no if rec.previous_company_uan_no else ''
# Remove empty/False values from dict
vals = {k: v for k, v in vals.items() if v not in ['', False, 0]}
if len(vals) > 0:
rec.authentication_details_status = 'validated'
rec.employee_id.write(vals)
else:
raise ValidationError(_("No values to validate"))
class Location(models.Model):
_name = 'hr.location'
_rec_name = 'location_name'
@ -169,29 +388,30 @@ class Location(models.Model):
if record.zip_code and not record.zip_code.isdigit(): # Check if zip_code exists and is not digit
raise ValidationError("Zip Code should contain only numeric characters. Please enter a valid zip code.")
class RecruitmentHistory(models.Model):
_name='recruitment.status.history'
date_from = fields.Date(string='Date From')
date_end = fields.Date(string='Date End')
target = fields.Integer(string='Target')
job_id = fields.Many2one('hr.job', string='Job Position') # Ensure this field exists
hired = fields.Many2many('hr.applicant')
@api.depends('date_from', 'date_end', 'job_id')
def _total_hired_users(self):
for rec in self:
if rec.date_from:
# Use `date_end` or today's date if `date_end` is not provided
date_end = rec.date_end or date.today()
# Search for applicants matching the conditions
hired_applicants = self.env['hr.applicant'].search([
('date_closed', '>=', rec.date_from),
('date_closed', '<=', date_end),
('job_id', '=', rec.job_id.id)
])
rec.hired = hired_applicants
else:
rec.hired = False
#
# class RecruitmentHistory(models.Model):
# _name='recruitment.status.history'
#
# date_from = fields.Date(string='Date From')
# date_end = fields.Date(string='Date End')
# target = fields.Integer(string='Target')
# job_id = fields.Many2one('hr.job', string='Job Position') # Ensure this field exists
# hired = fields.Many2many('hr.applicant')
#
# @api.depends('date_from', 'date_end', 'job_id')
# def _total_hired_users(self):
# for rec in self:
# if rec.date_from:
# # Use `date_end` or today's date if `date_end` is not provided
# date_end = rec.date_end or date.today()
#
# # Search for applicants matching the conditions
# hired_applicants = self.env['hr.applicant'].search([
# ('date_closed', '>=', rec.date_from),
# ('date_closed', '<=', date_end),
# ('job_id', '=', rec.job_id.id)
# ])
# rec.hired = hired_applicants
# else:
# rec.hired = False
#

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class RecruitmentSource(models.Model):
_inherit = "hr.recruitment.source"
job_recruitment_id = fields.Many2one('hr.job.recruitment', "Job Recruitment", ondelete='cascade')
job_id = fields.Many2one(related='job_recruitment_id.job_id',store=True)
def _compute_has_domain(self):
for source in self:
if source.alias_id:
source.has_domain = bool(source.alias_id.alias_domain_id)
else:
source.has_domain = bool(source.job_recruitment_id.company_id.alias_domain_id
or self.env.company.alias_domain_id)
def create_alias(self):
campaign = self.env.ref('hr_recruitment.utm_campaign_job')
medium = self.env['utm.medium']._fetch_or_create_utm_medium('email')
for source in self.filtered(lambda s: not s.alias_id):
vals = {
'alias_defaults': {
'job_recruitment_id': source.job_recruitment_id.id,
'campaign_id': campaign.id,
'medium_id': medium.id,
'source_id': source.source_id.id,
},
'alias_domain_id': source.job_recruitment_id.company_id.alias_domain_id.id or self.env.company.alias_domain_id.id,
'alias_model_id': self.env['ir.model']._get_id('hr.applicant'),
'alias_name': f"{source.job_recruitment_id.alias_name or source.job_recruitment_id.name}+{source.name}",
'alias_parent_thread_id': source.job_recruitment_id.id,
'alias_parent_model_id': self.env['ir.model']._get_id('hr.job'),
}
# check that you can create source before to call mail.alias in sudo with known/controlled vals
source.check_access('create')
source.alias_id = self.env['mail.alias'].sudo().create(vals)
def unlink(self):
""" Cascade delete aliases to avoid useless / badly configured aliases. """
aliases = self.alias_id
res = super().unlink()
aliases.sudo().unlink()
return res

View File

@ -0,0 +1,38 @@
from odoo import models, fields, api
class RecruitmentAttachments(models.Model):
_name = 'recruitment.attachments'
_description = 'Recruitment Attachments'
name = fields.Char(string='Name', required=True)
attachment_type = fields.Selection([('personal','Personal Documents'),('education','Education Documents'),('previous_employer','Previous Employer'),('others','Others')],default="others",required=True)
is_default = fields.Boolean(string='Is Default')
employee_recruitment_attachments = fields.One2many('employee.recruitment.attachments','recruitment_attachment_id',string="Documents")
class EmployeeRecruitmentAttachments(models.Model):
_name = "employee.recruitment.attachments"
_rec_name = 'name'
name = fields.Char(string='Attachment Name', required=True)
employee_id = fields.Many2one('hr.employee')
applicant_id = fields.Many2one('hr.applicant')
candidate_id = fields.Many2one('hr.candidate')
recruitment_attachment_id = fields.Many2one('recruitment.attachments')
recruitment_attachment_type = fields.Selection([('personal','Personal Documents'),('education','Education Documents'),('previous_employer','Previous Employer'),('others','Others')],related='recruitment_attachment_id.attachment_type')
file = fields.Binary(string='File', required=True)
@api.model
def create(self, vals):
# Auto-link applicant_id if context is passed correctly
if self.env.context.get('default_applicant_id'):
vals['applicant_id'] = self.env.context['default_applicant_id']
return super().create(vals)
class Employee(models.Model):
_inherit='hr.employee'
employee_attachment_ids = fields.One2many('employee.recruitment.attachments','employee_id',string="Attachments")

View File

@ -0,0 +1,24 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class RecruitmentRequisition(models.Model):
_inherit = 'recruitment.requisition'
hr_job_recruitment = fields.Many2one('hr.job.recruitment')
position_title = fields.Char(string="Position Title", required=False,related='job_id.name')
def button_create_jd(self):
self.hr_job_recruitment = self.env['hr.job.recruitment'].create({
'job_id': self.job_id.id,
'department_id': self.department_id.id,
'no_of_recruitment':self.number_of_positions,
'description':self.job_description,
'skill_ids': [(6, 0, self.primary_skill_ids.ids)],
'secondary_skill_ids': [(6, 0, self.secondary_skill_ids.ids)],
'requested_by': self.requested_by.partner_id.id,
'user_id': self.assign_to.id
})
self.state ='done'

View File

@ -0,0 +1,7 @@
from odoo import models, fields, api, _
class ResPartner(models.Model):
_inherit = 'res.partner'
contact_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='internal')

View File

@ -0,0 +1,43 @@
from odoo import models, fields, api
from pyresparser import ResumeParser
import base64
import tempfile
class ResumeParserModel(models.Model):
_name = 'resume.parser'
_description = 'Resume Parser'
name = fields.Char(string="Candidate Name")
email = fields.Char(string="Email")
phone = fields.Char(string="Phone")
skills_text = fields.Text(string="Skills")
experience_text = fields.Text(string="Experience")
degree_text = fields.Text(string="Degree")
resume_file = fields.Binary(string="Resume File")
resume_filename = fields.Char(string="Filename")
def action_parse_resume(self):
for record in self:
if not record.resume_file:
raise ValueError('Please upload a resume file first.')
# Save file temporarily
temp_dir = tempfile.gettempdir()
file_path = f"{temp_dir}/{record.resume_filename}"
with open(file_path, 'wb') as f:
f.write(base64.b64decode(record.resume_file))
# Parse Resume using AI (pyresparser)
try:
data = ResumeParser(file_path).get_extracted_data()
# Update fields
record.name = data.get('name', '')
record.email = data.get('email', '')
record.phone = data.get('phone', '')
record.skills_text = ', '.join(data.get('skills', [])) if data.get('skills') else ''
record.experience_text = '\n'.join(data.get('experience', [])) if data.get('experience') else ''
record.degree_text = ', '.join(data.get('degree', [])) if data.get('degree') else ''
except Exception as e:
raise ValueError(f"Error parsing resume: {str(e)}")

View File

@ -0,0 +1,8 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class SkillLevels(models.Model):
_inherit = 'hr.skill.level'
sequence = fields.Integer(string='Sequence',default=10)

View File

@ -7,15 +7,23 @@ class RecruitmentStage(models.Model):
is_default_field = fields.Boolean(string='Is Default', help='Upon Activating this it will automatically come upon JD Creation', default=True)
job_recruitment_ids = fields.Many2many(
'hr.job.recruitment', string='Job Specific',
help='Specific jobs that use this stage. Other jobs will not use this stage.')
second_application_form = fields.Boolean(default=False)
post_onboarding_form = fields.Boolean(default=False)
require_approval = fields.Boolean(default=False)
stage_color = fields.Char('Stage Color', default='#FFFFFF', help="Choose a color for the recruitment stage", widget='color')
@api.model
def create(self, vals):
res = super(RecruitmentStage, self).create(vals)
if 'job_ids' in vals:
if 'job_recruitment_ids' in vals:
jobs = list()
for job_id in vals['job_ids']:
for job_id in vals['job_recruitment_ids']:
jobs.append(job_id[1] if len(job_id)>1 else job_id)
job_ids = self.env['hr.job'].browse(jobs)
job_ids = self.env['hr.job.recruitment'].browse(jobs)
for job_id in job_ids:
job_id.write({'recruitment_stage_ids': [(4, res.id)]})
return res
@ -24,15 +32,15 @@ class RecruitmentStage(models.Model):
res = super(RecruitmentStage, self).write(vals)
if model:
if 'job_ids' in vals:
if 'job_recruitment_ids' in vals:
previous_job_ids = self.job_ids
jobs = list()
for job_id in vals['job_ids']:
for job_id in vals['job_recruitment_ids']:
jobs.append(job_id[1] if len(job_id)>1 else job_id)
new_job_ids = self.env['hr.job'].browse(jobs)
new_job_ids = self.env['hr.job.recruitment'].browse(jobs)
for stage_id in new_job_ids:
stage_id.write({'recruitment_stage_ids': [(4, self.id)]})
# Remove jobs from stages no longer related
@ -50,7 +58,7 @@ class RecruitmentStage(models.Model):
class Job(models.Model):
_inherit = 'hr.job'
_inherit = 'hr.job.recruitment'
recruitment_stage_ids = fields.Many2many('hr.recruitment.stage')
@ -78,7 +86,7 @@ class Job(models.Model):
stage_ids = self.env['hr.recruitment.stage'].browse(stages)
for stage_id in stage_ids:
stage_id.write({'job_ids': [(4, res.id)]})
stage_id.write({'job_recruitment_ids': [(4, res.id)]})
return res
def write(self, vals, model=None):
@ -91,16 +99,16 @@ class Job(models.Model):
stages.append(stage_id[1] if len(stage_id)>1 else stage_id)
new_stage_ids = self.env['hr.recruitment.stage'].browse(stages)
for stage_id in new_stage_ids:
stage_id.write({'job_ids': [(4, self.id)]})
stage_id.write({'job_recruitment_ids': [(4, self.id)]})
# Remove jobs from stages no longer related
for stage in previous_stage_ids:
if stage.id not in new_stage_ids.ids:
stage.write({'job_ids': [(3, self.id)]})
stage.write({'job_recruitment_ids': [(3, self.id)]})
return res
def unlink(self):
# Remove stage from all related jobs when stage is deleted
for job in self:
for stage in stage.recruitment_stage_ids:
stage.write({'job_ids': [(3, job.id)]})
stage.write({'job_recruitment_ids': [(3, job.id)]})
return super(Job, self).unlink()

View File

@ -1,3 +1,11 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_location,hr.location,model_hr_location,base.group_user,1,1,1,1
access_recruitment_status_history,recruitment.status.history,model_recruitment_status_history,base.group_user,1,1,1,1
access_hr_job_recruitment_user,access.hr.job.recruitment.user,model_hr_job_recruitment,base.group_user,1,1,1,1
access_candidate_experience,access.candidate.experience.user,model_candidate_experience,base.group_user,1,1,1,1
access_recruitment_attachments_user,access.recruitment.attachments.user,model_recruitment_attachments,base.group_user,1,1,1,1
access_post_onboarding_attachment_wizard,access.post.onboarding.attachment.wizard,model_post_onboarding_attachment_wizard,base.group_user,1,1,1,1
access_employee_recruitment_attachments,employee.recruitment.attachments,model_employee_recruitment_attachments,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_location hr.location model_hr_location base.group_user 1 1 1 1
3 access_recruitment_status_history access_hr_job_recruitment_user recruitment.status.history access.hr.job.recruitment.user model_recruitment_status_history model_hr_job_recruitment base.group_user 1 1 1 1
4 access_candidate_experience access.candidate.experience.user model_candidate_experience base.group_user 1 1 1 1
5 access_recruitment_attachments_user access.recruitment.attachments.user model_recruitment_attachments base.group_user 1 1 1 1
6 access_post_onboarding_attachment_wizard access.post.onboarding.attachment.wizard model_post_onboarding_attachment_wizard base.group_user 1 1 1 1
7 access_employee_recruitment_attachments employee.recruitment.attachments model_employee_recruitment_attachments base.group_user 1 1 1 1
8
9
10
11

View File

@ -0,0 +1,105 @@
<odoo>
<record id="hr_job_recruitment_user_rule" model="ir.rule">
<field name="name">User: All Applicants</field>
<field name="model_id" ref="model_hr_job_recruitment"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_user'))]"/>
</record>
<record id="hr_job_recruitment_interviewer_user_rule" model="ir.rule">
<field name="name">User: All Applicants</field>
<field name="model_id" ref="model_hr_job_recruitment"/>
<field name="domain_force">[('interviewer_ids', 'in', user.id)]</field>
<field name="active">false</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'hr_recruitment_skills'), ('name','=','hr_applicant_skill_interviewer_rule')] "/>
</function>
<value eval=" {'noupdate': False} "/>
</function>
<record id="hr_recruitment_skills.hr_applicant_skill_interviewer_rule" model="ir.rule">
<field name="name">Applicant Skill: Interviewer</field>
<field name="domain_force">[
'|',
('candidate_id.applicant_ids.hr_job_recruitment.interviewer_ids', 'in', user.id),
('candidate_id.applicant_ids.interviewer_ids', 'in', user.id),
]
</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'hr_recruitment_skills'), ('name','=','hr_applicant_skill_interviewer_rule')] "/>
</function>
<value eval=" {'noupdate': True} "/>
</function>
<!-- <record id="hr_job_recruitment_rule" model="ir.rule">-->
<!-- <field name="name">Applicant Interviewer</field>-->
<!-- <field name="model_id" ref="model_hr_applicant"/>-->
<!-- <field name="domain_force">[-->
<!-- '|',-->
<!-- ('job_id.interviewer_ids', 'in', user.id),-->
<!-- ('interviewer_ids', 'in', user.id),-->
<!-- ]</field>-->
<!-- <field name="perm_create" eval="False"/>-->
<!-- <field name="perm_unlink" eval="False"/>-->
<!-- <field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>-->
<!-- </record>-->
<record id="hr_recruitment.hr_applicant_interviewer_rule" model="ir.rule">
<field name="name">Applicant Interviewer</field>
<field name="domain_force">[
'|',
('hr_job_recruitment.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_candidate_interviewer_rule" model="ir.rule">
<field name="name">Candidate Interviewer</field>
<field name="domain_force">[
'|',
('applicant_ids.hr_job_recruitment.interviewer_ids', 'in', user.id),
('applicant_ids.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_applicant_recruitment_interviewer_rule" model="ir.rule">
<field name="name">Applicant Interviewer</field>
<field name="model_id" ref="model_hr_applicant"/>
<field name="domain_force">[
'|',
('hr_job_recruitment.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_candidate_recruitment_interviewer_rule" model="ir.rule">
<field name="name">Candidate Interviewer</field>
<field name="model_id" ref="model_hr_candidate"/>
<field name="domain_force">[
'|',
('applicant_ids.hr_job_recruitment.interviewer_ids', 'in', user.id),
('applicant_ids.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>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,730 @@
import publicWidget from "@web/legacy/js/public/public_widget";
import { _t } from "@web/core/l10n/translation";
import { rpc } from "@web/core/network/rpc";
import { assets, loadCSS, loadJS } from "@web/core/assets";
publicWidget.registry.hrRecruitment = publicWidget.Widget.extend({
selector: "#post_onboarding_form",
events: {
"change [name='candidate_image']": "previewApplicantPhoto",
"click #delete-photo-btn": "deleteCandidatePhoto",
"click #preview-photo-btn": "previewFullImage",
"click #add-education-row": "addEducationRow", // Ensure button click event is correctly bound
'change .attachment-input': 'handleAttachmentUpload',
'click .upload-new-btn': 'handleUploadNewFile',
'click .delete-file-btn': 'handleDeleteUploadedFile',
'input .file-name-input': 'handleFileNameChange',
"click .remove-file": "removeFile",
"click .preview-file": "previewFile",
"click .view-attachments-btn": "openAttachmentModal", // Opens modal with files
"click .close-modal-btn": "closeAttachmentModal", // Close modal
"submit": "handleFormSubmit",
"change [name='experience']": "onChangeExperience",
"change [name='marital']": "onChangeMarital",
},
uploadedFiles: {}, // Store files per attachment ID
addUploadedFileRow(attachmentId, file, base64String) {
const tableBody = this.$(`#preview_body_${attachmentId}`);
if (!this.uploadedFiles[attachmentId]) {
this.uploadedFiles[attachmentId] = [];
}
// Generate a unique file ID using attachmentId and a timestamp
const fileId = `${attachmentId}-${Date.now()}`;
const fileRecord = {
attachment_rec_id : attachmentId,
id: fileId, // Unique file ID
name: file.name,
base64: base64String,
type: file.type,
};
this.uploadedFiles[attachmentId].push(fileRecord);
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
const previewImageId = `preview_image_${fileId}`;
const fileNameInputId = `file_name_input_${fileId}`;
let previewContent = '';
let previewClickHandler = '';
// Check if the file is an image or PDF and set preview content accordingly
if (file.type.startsWith('image/')) {
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
</div>`;
previewClickHandler = () => {
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
this.$('#modal_attachment_photo_preview').show();
this.$('#modal_attachment_pdf_preview').hide(); // Hide PDF preview
this.$('#attachmentPreviewModal').modal('show');
};
} else if (file.type === 'application/pdf') {
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
</div>`;
previewClickHandler = () => {
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
this.$('#modal_attachment_pdf_preview').show();
this.$('#modal_attachment_photo_preview').hide(); // Hide image preview
this.$('#attachmentPreviewModal').modal('show');
};
}
// Append new row to the table with a preview and buttons
tableBody.append(`
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
<td>
<input type="text" class="form-control file-name-input" id="${fileNameInputId}" value="${file.name}"/>
</td>
<td class="text-center">
<div class="d-flex flex-column align-items-center justify-content-center gap-2">
${previewContent}
<div class="d-flex gap-2">
<button type="button" class="btn btn-danger btn-sm delete-file-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
<i class="fa fa-trash"></i>
</button>
<button type="button" class="btn btn-info btn-sm preview-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
<i class="fa fa-eye"></i>
</button>
</div>
</div>
</td>
</tr>
`);
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
// Attach click handler for preview (image or PDF)
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
// Attach click handler for the preview button (to trigger modal)
this.$(`.preview-btn[data-attachment-id="${attachmentId}"][data-file-id="${fileId}"]`).on('click', previewClickHandler);
},
handleDeleteUploadedFile(ev) {
const button = ev.currentTarget;
const attachmentId = $(button).data('attachment-id');
const fileId = $(button).data('file-id');
// Find the index of the file to delete based on unique file ID
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
if (fileIndex !== -1) {
this.uploadedFiles[attachmentId].splice(fileIndex, 1); // Remove from array
}
// Remove the row from DOM
this.$(`tr[data-file-id="${fileId}"]`).remove();
// Hide table if no files left
if (this.uploadedFiles[attachmentId].length === 0) {
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
}
},
handleAttachmentUpload(ev) {
const input = ev.target;
const attachmentId = $(input).data('attachment-id');
if (input.files.length > 0) {
Array.from(input.files).forEach((file) => {
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target.result.split(',')[1];
this.addUploadedFileRow(attachmentId, file, base64String);
};
reader.readAsDataURL(file);
});
}
},
handleUploadNewFile(ev) {
console.log("hello upload file");
const button = $(ev.currentTarget);
const attachmentId = button.data('attachment-id');
const index = button.data('index');
// Find the hidden file input specific to this attachmentId
const hiddenInput = this.$(`.upload-new-file-input[data-attachment-id='${attachmentId}']`);
// When file is selected, update preview and replace old file
hiddenInput.off('change').on('change', (e) => {
const file = e.target.files[0];
if (!file) {
return; // Do nothing if no file is selected
}
const reader = new FileReader();
reader.onload = (event) => {
const base64String = event.target.result.split(',')[1];
// Replace the existing file in uploadedFiles
this.uploadedFiles[attachmentId][index] = {
name: file.name,
base64: base64String,
};
// Update the preview image in the table
const imageId = `preview_image_${attachmentId}_${index}`;
const filePreviewWrapperId = `preview_wrapper_${attachmentId}_${index}`;
// Check if the file is an image or PDF and update accordingly
const fileType = file.type;
if (fileType.startsWith('image/')) {
this.$(`#${filePreviewWrapperId}`).html(`
<img id="${imageId}" src="data:image/png;base64,${base64String}" class="img-thumbnail"
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;" />
`);
} else if (fileType === 'application/pdf') {
this.$(`#${filePreviewWrapperId}`).html(`
<iframe src="data:application/pdf;base64,${base64String}" width="80" height="80" class="img-thumbnail" style="border: none; cursor: pointer;"></iframe>
`);
}
// Optional: Update the file name in the input field
const fileNameInputId = `file_name_input_${attachmentId}_${index}`;
this.$(`#${fileNameInputId}`).val(file.name);
};
reader.readAsDataURL(file);
});
// Trigger the hidden file input to open the file selection dialog
console.log("Triggering file input...");
hiddenInput.trigger('click'); // Ensure this is working properly
},
handleFileNameChange(event) {
const attachmentId = $(event.target).closest('tr').data('attachment-id');
const fileId = $(event.target).closest('tr').data('file-id');
const newFileName = event.target.value;
if (!attachmentId || !fileId) {
console.error('Missing attachmentId or fileId');
return;
}
const fileList = this.uploadedFiles[attachmentId];
if (!fileList) {
console.error(`No files found for attachmentId: ${attachmentId}`);
return;
}
const fileRecord = fileList.find(file => file.id === fileId);
if (!fileRecord) {
console.error(`File with ID ${fileId} not found under attachment ${attachmentId}`);
return;
}
fileRecord.name = newFileName;
},
renderFilePreview(attachmentId) {
const container = this.$(`#preview_container_${attachmentId}`);
container.empty();
if (this.uploadedFiles[attachmentId].length === 0) {
container.addClass("d-none");
return;
}
container.removeClass("d-none");
this.uploadedFiles[attachmentId].forEach((file, index) => {
const fileHtml = $(`
<div class="d-flex flex-column align-items-center">
<div class="position-relative">
<img src="${file.base64}" class="rounded-circle shadow-sm" style="width: 80px; height: 80px; object-fit: cover; border: 1px solid #ddd; cursor: pointer;" data-index="${index}" data-attachment-id="${attachmentId}" />
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 remove-file" data-attachment-id="${attachmentId}" data-index="${index}" style="transform: translate(50%, -50%);"><i class="fa fa-trash"></i></button>
</div>
<small>${file.name}</small>
</div>
`);
fileHtml.find("img").on("click", this.previewAttachmentImage.bind(this));
fileHtml.find(".remove-file").on("click", this.removeFile.bind(this));
container.append(fileHtml);
});
},
previewAttachmentImage(ev) {
const attachmentId = $(ev.currentTarget).data("attachment-id");
const index = $(ev.currentTarget).data("index");
const fileData = this.uploadedFiles[attachmentId][index];
this.$("#attachment_modal_preview").attr("src", fileData.base64);
this.$("#attachmentPreviewModal").modal("show");
},
removeFile(ev) {
const attachmentId = $(ev.currentTarget).data("attachment-id");
const index = $(ev.currentTarget).data("index");
this.uploadedFiles[attachmentId].splice(index, 1);
this.renderFilePreview(attachmentId);
},
previewFile(ev) {
const fileUrl = ev.currentTarget.dataset.fileUrl;
const modal = this.$("#photoPreviewModal");
this.$("#modal_photo_preview").attr("src", fileUrl);
modal.modal("show");
},
onChangeExperience(event) {
const selectedValue = $(event.currentTarget).val();
const employerHistorySection = $("#employer_history_data");
if (selectedValue === "experienced") {
employerHistorySection.show();
} else {
employerHistorySection.hide();
}
},
onChangeMarital(event) {
const selectedValue = $(event.currentTarget).val();
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
const family_details_data_spouse = this.$('#family_details_data_spouse')
const family_details_data_kid1 = this.$('#family_details_data_kid1')
const family_details_data_kid2 = this.$('#family_details_data_kid2')
if (selectedValue === "married") {
marriageAnniversarySection.show();
// Show rows for spouse, kid1, and kid2
family_details_data_spouse.show();
family_details_data_kid1.show();
family_details_data_kid2.show();
} else {
marriageAnniversarySection.hide();
// Hide rows for spouse, kid1, and kid2
family_details_data_spouse.hide();
family_details_data_kid1.hide();
family_details_data_kid2.hide();
}
},
/**
* Open modal and display uploaded files
*/
openAttachmentModal(event) {
console.log("openAttachmentModal");
const rowId = $(event.currentTarget).closest("tr").index(); // Get the row index
this.currentRowId = rowId; // Store rowId for reference
this.renderAttachmentModal(rowId); // Render the modal for the row
},
renderAttachmentModal(rowId) {
const fileList = this.uploadedFiles && this.uploadedFiles[rowId] ? this.uploadedFiles[rowId] : [];
let modalHtml = `
<div id="attachmentModal" class="modal fade show" tabindex="-1" style="display: block; background: rgba(0,0,0,0.5);" aria-modal="true">
<div class="modal-dialog modal-lg" style="max-width: 600px;">
<div class="modal-content" style="border-radius: 8px; box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);">
<!-- Modal Header -->
<div class="modal-header" style="background: #143d5d; color: white; border-bottom: 2px solid #dee2e6; font-weight: bold; padding: 12px 15px;">
<h5 class="modal-title" style="margin: 0;">Uploaded Attachments</h5>
<button type="button" class="close close-modal-btn" data-dismiss="modal" aria-label="Close" style="background: none; border: none; font-size: 20px; color: white; cursor: pointer;">
&times;
</button>
</div>
<!-- Modal Body -->
<div class="modal-body" style="padding: 15px;">
<ul class="attachment-list" style="list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto;">
${fileList.length ? fileList.map((file, index) => `
<li style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa;">
<span style="font-weight: 500; flex-grow: 1;">${file.name}</span>
<button type="button" class="remove-file" data-index="${index}"
style="background: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;">
<i style="margin-right: 5px;">🗑</i>
</button>
</li>
`).join("") : `<p style="color: #6c757d; text-align: center; font-size: 16px; padding: 10px;">No files uploaded.</p>`}
</ul>
</div>
<!-- Modal Footer -->
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 12px;">
<button type="button" class="btn close-modal-btn" data-dismiss="modal"
style="background: #6c757d; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;">
Close
</button>
</div>
</div>
</div>
</div>`;
// Remove old modal and append new one
$("#attachmentModal").remove();
$("body").append(modalHtml);
// Attach remove event to new modal content
$("#attachmentModal").on("click", ".remove-file", this.removeFile.bind(this));
$("#attachmentModal").on("click", ".close-modal-btn", this.closeAttachmentModal.bind(this));
},
/**
* Close attachment modal
*/
closeAttachmentModal() {
console.log("Closing modal");
$("#attachmentModal").remove(); // Remove the modal from the DOM
},
// Function to preview the uploaded image
previewApplicantPhoto(ev) {
const input = ev.currentTarget;
const preview = this.$("#photo_preview");
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
preview.attr("src", e.target.result);
// Store the base64 in a hidden input field
this.$("input[name='candidate_image_base64']").val(base64String);
};
reader.readAsDataURL(input.files[0]);
}
},
// Function to delete the uploaded image
deleteCandidatePhoto() {
const preview = this.$("#photo_preview");
const inputFile = this.$("input[name='candidate_image']");
preview.attr("src", "data:image/png;base64,"); // Reset preview
inputFile.val(""); // Reset file input
},
// Function to preview full image inside a modal
previewFullImage() {
const previewSrc = this.$("#photo_preview").attr("src");
if (previewSrc) {
this.$("#modal_photo_preview").attr("src", previewSrc);
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
}
},
// Function to add a new education row dynamically
addEducationRow(ev) {
ev.preventDefault(); // Prevent default behavior
let newRow = `
<tr class="predefined-row">
<td class="education-relation-col">
<select name="education_type" class="form-control">
<option value="10">10th</option>
<option value="inter">Inter</option>
<option value="graduation">Graduation</option>
<option value="post_graduation">Post Graduation</option>
<option value="additional">Additional Qualification</option>
</select>
</td>
<td><input type="text" name="specialization" class="form-control" placeholder="Enter Specialization"/></td>
<td><input type="text" name="university" class="form-control" placeholder="Enter University"/></td>
<td><input type="number" name="start_year" class="form-control" placeholder="Start Year"/></td>
<td><input type="number" name="end_year" class="form-control" placeholder="End Year"/></td>
<td><input type="text" name="marks_grade" class="form-control" placeholder="Marks/Grade"/></td>
</tr>
`;
this.$("#education_details_data tbody").append(newRow); // Append new row inside the table
console.log("New education row added!");
},
validateFamilyDetails() {
let familyDetailsFilled = false;
let educationDetailsFilled = false;
const family_rows = document.querySelectorAll('#family_details_data tbody tr');
const education_rows = document.querySelectorAll('#education_details_data tbody tr');
family_rows.forEach(row => {
const inputs = row.querySelectorAll('input[type="text"], input[type="date"]');
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
if (isRowFilled) {
familyDetailsFilled = true;
}
});
education_rows.forEach(row => {
const inputs = row.querySelectorAll('input[type="text"]');
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
if (isRowFilled) {
educationDetailsFilled = true;
}
});
if (!familyDetailsFilled) {
alert('Please fill at least one family member detail.');
return false;
}
if (!educationDetailsFilled) {
alert('Please fill at least one Education details')
return false;
}
return true;
},
handleFormSubmit(ev) {
ev.preventDefault();
if (!this.validateFamilyDetails()) {
return;
}
let employerHistoryData = [];
this.$("#employer_history_data tbody tr").each((index, row) => {
let rowData = {
company_name: this.$(row).find("[name$='company_name']").val()?.trim() || "",
designation: this.$(row).find("[name$='designation']").val()?.trim() || "",
date_of_joining: this.$(row).find("[name$='doj']").val()?.trim() || "",
last_working_day: this.$(row).find("[name$='lwd']").val()?.trim() || "",
ctc: this.$(row).find("[name$='ctc']").val()?.trim() || "",
};
if (Object.values(rowData).some(value => value)) {
employerHistoryData.push(rowData);
}
});
let familyData = [];
this.$("#family_details_data tbody tr").each((index, row) => {
let rowData = {
relation: this.$(row).find(".relation-col").attr("value")?.trim(),
name: this.$(row).find("[name$='_name']").val()?.trim() || "",
contact: this.$(row).find("[name$='_contact']").val()?.trim() || "",
dob: this.$(row).find("[name$='_dob']").val()?.trim() || "",
location: this.$(row).find("[name$='_location']").val()?.trim() || "",
};
if (Object.entries(rowData).some(([key, value]) => key !== 'relation' && value)) {
familyData.push(rowData);
}
});
let educationData = [];
this.$("#education_details_data tbody tr").each((index, row) => {
let educationCell = this.$(row).find(".education-relation-col");
let educationType = educationCell.find("select").val()?.trim() || educationCell.attr("value")?.trim();
let rowData = {
education_type: educationType,
specialization: this.$(row).find("[name='specialization']").val()?.trim() || "",
university: this.$(row).find("[name='university']").val()?.trim() || "",
start_year: this.$(row).find("[name='start_year']").val()?.trim() || "",
end_year: this.$(row).find("[name='end_year']").val()?.trim() || "",
marks_or_grade: this.$(row).find("[name='marks_grade']").val()?.trim() || "",
};
if (Object.entries(rowData).some(([key, value]) => key !== 'education_type' && value)) {
educationData.push(rowData);
}
});
let attachments = [];
let fileReadPromises = [];
let attachmentInputs = this.uploadedFiles; // your object {1: Array(1), 2: Array(2)}
Object.keys(attachmentInputs).forEach(key => {
let filesArray = attachmentInputs[key]; // This is an array
filesArray.forEach(file => {
attachments.push({
attachment_rec_id: file.attachment_rec_id,
file_name: file.name,
file_content: file.base64, // Assuming base64 is already present
});
});
});
Promise.all(fileReadPromises).then(() => {
this.$("#family_data_json").val(JSON.stringify(familyData));
this.$("#education_data_json").val(JSON.stringify(educationData));
this.$("#employer_history_data_json").val(JSON.stringify(employerHistoryData));
this.$("#attachments_data_json").val(JSON.stringify(attachments));
let formElement = this.$el.is("form") ? this.$el[0] : this.$el.find("form")[0];
if (formElement) {
formElement.submit();
} else {
console.error("Form element not found.");
}
}).catch((error) => {
console.error("Error reading files:", error);
});
},
// Enforce required fields when any field in a row is filled
enforceRowValidation() {
// Education Details Validation - Make fields required when typing
this.$("#education_details_data").on("input", "tbody tr input", function () {
let row = $(this).closest("tr");
let inputs = row.find("input");
// Check if at least one field has a value
let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
if (isFilled) {
inputs.attr("required", true);
} else {
inputs.removeAttr("required"); // Remove required if all fields are empty
}
});
// Remove empty rows only when the user leaves the last input field
this.$("#education_details_data").on("blur", "tbody tr input", function () {
let row = $(this).closest("tr");
let inputs = row.find("input");
if (row.hasClass("predefined-row")) {
let isEmpty = inputs.toArray().every(input => $(input).val().trim() === "");
if (isEmpty) {
row.remove();
}
}
});
// Family Details Validation
// this.$("#family_details_data").on("input", "tbody tr input", function () {
// let row = $(this).closest("tr");
// let inputs = row.find("input");
//
// let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
//
// if (isFilled) {
// inputs.attr("required", true);
// } else {
// inputs.removeAttr("required");
// }
// });
// Remove row functionality (Manual delete using button)
this.$("#education_details_data").on("click", ".remove-edu-row", function () {
$(this).closest("tr").remove();
});
this.$("#family_details_data").on("click", ".remove-family-row", function () {
$(this).closest("tr").remove();
});
},
async _renderStateIds() {
console.log("Fetching States...");
const country_id = $('#present_state_ids_container').data('country_id');
const state_ids = await rpc("/hr_recruitment_extended/fetch_related_state_ids", {
country_id: country_id,
});
const present_state_ids_container = $("#present_state_ids_container");
const permanent_state_ids_container = $("#permanent_state_ids_container");
present_state_ids_container.empty();
permanent_state_ids_container.empty();
console.log(state_ids);
if (typeof state_ids === 'object' && !Array.isArray(state_ids)) {
const stateOptions = Object.entries(state_ids).map(([id, name]) => `
<option value="${id}">${name}</option>
`).join('');
const stateHtml = `
<select name='permanent_state' id="permanent_state" class="form-control" required>
<option value="" disabled selected>Select State</option>
${stateOptions}
</select>
`;
const presentStateHtml = `
<select name='present_state' id="present_state" class="form-control" required>
<option value="" disabled selected>Select State</option>
${stateOptions}
</select>
`;
permanent_state_ids_container.append(stateHtml);
present_state_ids_container.append(presentStateHtml);
} else {
console.error("Expected an object like {id: name}, but got:", state_ids);
}
console.log("Hello World");
},
async start() {
this._super(...arguments);
this._renderStateIds();
// Ensure form submit event is properly bound
this.$el.on("submit", this.handleFormSubmit.bind(this));
// Bind validation enforcement
this.enforceRowValidation();
const selectedExperience = this.$("[name='experience']:checked").val();
const selectedMarital = this.$("[name='marital']:checked").val();
const employerHistorySection = this.$("#employer_history_data");
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
const family_details_data_spouse = this.$('#family_details_data_spouse')
const family_details_data_kid1 = this.$('#family_details_data_kid1')
const family_details_data_kid2 = this.$('#family_details_data_kid2')
if (selectedExperience === "experienced") {
employerHistorySection.show();
} else {
employerHistorySection.hide();
}
if (selectedMarital === 'married') {
marriageAnniversarySection.show();
// Show rows for spouse, kid1, and kid2 if married
family_details_data_spouse.show();
family_details_data_kid1.show();
family_details_data_kid2.show();
} else {
marriageAnniversarySection.hide();
// Show rows for spouse, kid1, and kid2 if married
family_details_data_spouse.hide();
family_details_data_kid1.hide();
family_details_data_kid2.hide();
}
},
});

View File

@ -1,71 +1,75 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { _t } from "@web/core/l10n/translation";
import { rpc } from "@web/core/network/rpc";
// Inherit the hrRecruitment widget
publicWidget.registry.CustomHrRecruitment = publicWidget.registry.hrRecruitment.extend({
selector: '#hr_recruitment_form',
publicWidget.registry.preOnboardHrRecruitment = publicWidget.Widget.extend({
selector : '#hr_recruitment_second_form_applicant',
events: {
'focusout #recruitmentctc' : '_onFocusOutCTC',
'focusout #recruitmentctc2' : '_onFocusOutCTC2',
'focusout #recruitmentphone' : '_onFocusOutPhoneNumber',
'focusout #recruitmentphone2': '_onFocusOutPhone2Number',
'change [name="exp_type"]': '_onExperienceTypeChange' // Add event listener for Experience Type change
'change [name="candidate_image"]': 'previewApplicantPhoto',
'click #delete-photo-btn': 'deleteCandidatePhoto',
'click #preview-photo-btn': 'previewFullImage',
'change [name="exp_type"]': '_onExperienceTypeChange'
},
async _onFocusOutCTC(ev) {
const regex = /^[\d]*$/;
const field='ctc'
const value = ev.currentTarget.value;
const messageContainerId = "#ctcwarning-message";
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "InValid CTC");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
// Function to preview the uploaded image
previewApplicantPhoto(ev) {
const input = ev.currentTarget;
const preview = this.$("#photo_preview");
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
preview.attr("src", e.target.result);
// Store the base64 in a hidden input field
this.$("input[name='candidate_image_base64']").val(base64String);
};
reader.readAsDataURL(input.files[0]);
}
},
async _onFocusOutCTC2(ev) {
const regex = /^[\d]*$/;
const field='ctc'
const value = ev.currentTarget.value;
const messageContainerId = "#ctc2warning-message";
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "InValid CTC");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
// Function to delete the uploaded image
deleteCandidatePhoto() {
const preview = this.$("#photo_preview");
const inputFile = this.$('input[name="candidate_image"]');
preview.attr("src", "data:image/png;base64,"); // Reset preview
inputFile.val(""); // Reset file input
},
// Function to preview full image inside a modal
previewFullImage() {
const previewSrc = this.$("#photo_preview").attr("src");
if (previewSrc) {
this.$("#modal_photo_preview").attr("src", previewSrc);
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
}
},
async _onFocusOutPhoneNumber (ev) {
const regex = /^[\d\+\-\s]*$/;
const field = "phone"
const value = ev.currentTarget.value;
const messageContainerId = "#phone1-warning";
await this.checkRedundant(ev.currentTarget, field, messageContainerId);
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "Invalid Number");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
async _populateExperienceDropdowns() {
const yearsDropdown = $('#experience_years');
const monthsDropdown = $('#experience_months');
// Clear existing options
yearsDropdown.empty();
monthsDropdown.empty();
// Populate Years dropdown (1 to 30+)
yearsDropdown.append(`<option value="">Select Years</option>`);
for (let i = 1; i <= 30; i++) {
yearsDropdown.append(`<option value="${i}">${i} Year${i > 1 ? 's' : ''}</option>`);
}
yearsDropdown.append(`<option value="30+">30+ Years</option>`);
// Populate Months dropdown (1 to 11)
monthsDropdown.append(`<option value="">Select Months</option>`);
for (let i = 1; i <= 11; i++) {
monthsDropdown.append(`<option value="${i}">${i} Month${i > 1 ? 's' : ''}</option>`);
}
},
async _onFocusOutPhone2Number (ev) {
const regex = /^[\d\+\-\s]*$/;
const field = "phone"
const value = ev.currentTarget.value;
const messageContainerId = "#phone2-warning";
await this.checkRedundant(ev.currentTarget, field, messageContainerId);
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "Invalid Number");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
}
},
// Function to toggle visibility of current_ctc and current_organization based on Experience Type
_onExperienceTypeChange(ev) {
@ -73,116 +77,67 @@ publicWidget.registry.CustomHrRecruitment = publicWidget.registry.hrRecruitment.
const currentCtcField = $('#current_ctc_field');
const currentOrgField = $('#current_organization_field');
const noticePeriodField = $('#notice_period_field');
const totalExperienceField = $('#total_experience_field');
const ctcInput = $('#recruitmentctc');
const orgInput = $('#current_organization');
const noticePeriodInput = $('#notice_period')
const totalExperienceInput = $('#total_exp');
if (expType === 'fresher') {
currentCtcField.hide();
currentOrgField.hide();
noticePeriodField.hide();
totalExperienceField.hide();
ctcInput.val('')
orgInput.val('')
noticePeriodInput.val('')
totalExperienceInput.val('')
} else {
currentCtcField.show();
currentOrgField.show();
noticePeriodField.show();
totalExperienceField.show();
}
},
async _renderPreferredLocations() {
console.log("hello world")
console.log(this)
const value = $('#preferred_locations_container').data('job_id');
console.log(value)
console.log("Job ID:", value); // You can now use this jobId
if (value){
let locationsArray = value.match(/\d+/g).map(Number); // [1, 2, 4, 5]
console.log(locationsArray)
const locations_data = await rpc("/hr_recruitment_extended/fetch_preferred_locations", {
loc_ids : locationsArray
});
try {
// Assuming location_ids is a many2many field in hr.job
const locationsField = $('#preferred_locations_container');
locationsField.empty(); // Clear any existing options
// Add checkboxes for each location
Object.keys(locations_data).forEach(key => {
const value = locations_data[key]; // value for the current key
const checkboxHtml = `
<div class="checkbox-container">
<input type="checkbox" name="preferred_locations" value="${key}" id="location_${key}">
<label for="location_${key}">${value}</label>
</div>
`;
locationsField.append(checkboxHtml);
});
} catch (error) {
console.error('Error fetching locations:', error);
}
} else {
console.log("no values")
const preferredLocation = $('#preferred_location_field');
preferredLocation.hide();
}
},
async _hrRecruitmentDegrees() {
try {
const degrees_data = await rpc("/hr_recruitment_extended/fetch_hr_recruitment_degree", {
});
// Assuming location_ids is a many2many field in hr.job
const degreesSelection = $('#fetch_hr_recruitment_degree');
degreesSelection.empty(); // Clear any existing options
// Add checkboxes for each location
Object.keys(degrees_data).forEach(key => {
const value = degrees_data[key]; // value for the current key
const checkboxHtml = `
<option value="${key}">${value}</option>
`;
degreesSelection.append(checkboxHtml);
});
} catch (error) {
console.error('Error fetching degrees:', error);
}
},
async start() {
this._super(...arguments);
await this._renderPreferredLocations(); // Render the preferred locations checkboxes
await this._hrRecruitmentDegrees();
await this._populateExperienceDropdowns();
const currentCtcField = $('#current_ctc_field');
const currentOrgField = $('#current_organization_field');
const noticePeriodField = $('#notice_period_field');
const ctcInput = $('#recruitmentctc');
const totalExperienceField = $('#total_experience_field');
const ctcInputCurrent = $('#current_ctc');
const ctcInputExpected = $('#salary_expected');
const orgInput = $('#current_organization');
const noticePeriodInput = $('#notice_period')
const noticePeriodInput = $('#notice_period');
const totalExperienceInput = $('#total_exp');
const exp_type = $('#exp_type');
currentCtcField.hide();
currentOrgField.hide();
noticePeriodField.hide();
// Attach event listener
exp_type.on('change', this._onExperienceTypeChange.bind(this));
ctcInput.val('')
orgInput.val('')
noticePeriodInput.val('')
},
// Get initial value of Experience Type
const initialExpType = exp_type.val();
if (initialExpType === 'fresher') {
currentCtcField.hide();
currentOrgField.hide();
noticePeriodField.hide();
totalExperienceField.hide();
// You can also override the _onClickApplyButton method if needed
// _onClickApplyButton(ev) {
// this._super(ev); // Call the parent method if you want to retain its functionality
// console.log("Custom behavior after clicking apply button!");
// // Add your custom logic here if needed
// }
});
ctcInputCurrent.val('');
ctcInputExpected.val('');
orgInput.val('');
noticePeriodInput.val('');
totalExperienceInput.val('');
} else {
currentCtcField.show();
currentOrgField.show();
noticePeriodField.show();
totalExperienceField.show();
}
}
})

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- List View -->
<record id="view_candidate_experience_list" model="ir.ui.view">
<field name="name">candidate.experience.list</field>
<field name="model">candidate.experience</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="experience_code" placeholder = "E1" required="1" col="3"/>
<field name="experience_from" required="1" placeholder="0" col="3"/>
<field name="experience_to" required="1" placeholder="2" col="3"/>
<!-- <field name="active"/>-->
</list>
</field>
</record>
<!-- Candidate Experience Menu & Action -->
<record id="action_candidate_experience" model="ir.actions.act_window">
<field name="name">Candidate Experience</field>
<field name="res_model">candidate.experience</field>
<field name="view_mode">list</field>
<field name="help" type="html">
<p>
Manage candidate experience records here.
</p>
</field>
</record>
<menuitem
id="menu_candidate_experience"
name="Experience"
parent="hr_recruitment.menu_hr_recruitment_config_applications"
action="action_candidate_experience"
groups="base.group_no_one"
sequence="30"/>
</odoo>

View File

@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_applicant_view_form_inherit" model="ir.ui.view">
<field name="name">hr.applicant.view.form</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='archive_applicant']" position="before">
<button string="Submit" name="submit_for_approval" type="object" class="oe_stat_button" invisible="not approval_required or application_submitted" groups="base.group_user" />
<button string="Approve" name="approve_applicant" type="object" class="oe_stat_button" invisible="not approval_required or not application_submitted" groups="hr_recruitment.group_hr_recruitment_user" />
<button name="submit_to_client" string="Send to Client" type="object" class="btn-primary" groups="hr_recruitment.group_hr_recruitment_user" invisible="submitted_to_client or application_status in ['refused']"/>
</xpath>
<xpath expr="//field[@name='kanban_state']" position="after">
<div class="o_employee_avatar m-0 p-0">
<field name="candidate_image" widget="image" class="oe_avatar m-0"
options="{&quot;zoom&quot;: true, &quot;preview_image&quot;:&quot;candidate_image&quot;}"/>
</div>
</xpath>
<xpath expr="//field[@name='job_id']" position="before">
<field name="hr_job_recruitment"/>
<field name="approval_required" invisible="1"/>
<field name="application_submitted" invisible="1"/>
<field name="stage_color" invisible="1"/>
</xpath>
<xpath expr="//field[@name='job_id']" position="after">
<field name="employee_id" invisible="1"/>
<field name="send_second_application_form"/>
<field name="second_application_form_status" readonly="not send_second_application_form"/>
<field name="send_post_onboarding_form"/>
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
</xpath>
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='stage_id']" position="before">
<field name="recruitment_stage_id" widget="statusbar_duration"
options="{'clickable': '1', 'fold_field': 'fold'}" invisible="not active and not employee_id" readonly="approval_required" force_save="1"/>
</xpath>
<!-- <xpath expr="//form" position="after">-->
<!-- </xpath>-->
<xpath expr="//header" position="inside">
<button string="Send Second Application Form" name="send_second_application_form_to_candidate"
type="object" groups="hr.group_hr_user"
invisible="not send_second_application_form or second_application_form_status in ['email_sent_to_candidate','done']"/>
<button string="Send Post Onboarding Form" name="send_post_onboarding_form_to_candidate" type="object"
groups="hr.group_hr_user"
invisible="not employee_id or not send_post_onboarding_form or post_onboarding_form_status in ['email_sent_to_candidate','done']"/>
</xpath>
<xpath expr="//notebook" position="inside">
<page name="Attachments" id="attachment_ids_page">
<field name="recruitment_attachments" widget="many2many_tags"/>
<!-- Separate Group for Buttons -->
<group colspan="2">
<button string="Validate/update"
name="action_validate_attachments"
type="object"
class="btn btn-success"
invisible="attachments_validation_status == 'pending'"
style="width: 100%;">
<div>
Click here to save &amp; Update Employee Data (attachments)
<field name="attachments_validation_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_attachments"
type="object"
class="btn btn-danger"
invisible="attachments_validation_status == 'validated'"
style="width: 100%;">
<div>
Click here to save, Validate and Update into Employee Data (attachments)
<field name="attachments_validation_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
</group>
<!-- Group for One2many field with Full Width -->
<group string="Post Onboarding Attachments" colspan="2">
<field name="joining_attachment_ids" nolabel="1">
<list editable="bottom" default_group_by="recruitment_attachment_id">
<field name="recruitment_attachment_id"/>
<field name="name"/>
<field name="recruitment_attachment_type"/>
<field name="file" widget="binary" options="{'download':true}"/>
</list>
</field>
</group>
</page>
</xpath>
</field>
</record>
<record id="hr_applicant_view_search_bis_inherit" model="ir.ui.view">
<field name="name">hr.applicant.view.search</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_search_bis"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='job_id']" position="after">
<field name="hr_job_recruitment"/>
<field name="recruitment_stage_id" domain="[]"/>
</xpath>
<xpath expr="//search/group" position="inside">
<filter string="Job Recruitment" name="job_recruitment" domain="[]"
context="{'group_by': 'hr_job_recruitment'}"/>
<filter string="Job Recruitment Stage" name="job_recruitment_stage" domain="[]"
context="{'group_by': 'recruitment_stage_id'}"/>
</xpath>
</field>
</record>
<record id="hr_kanban_view_applicant_inherit" model="ir.ui.view">
<field name="name">hr.applicant.view.kanban.inherit</field>
<field name="model">hr.applicant</field>
<field name="arch" type="xml">
<kanban highlight_color="color" default_group_by="recruitment_stage_id"
class="o_kanban_applicant o_search_matching_applicant"
quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1">
<field name="recruitment_stage_id" options='{"group_by_tooltip": {"requirements": "Requirements"}}'/>
<field name="legend_normal"/>
<field name="legend_blocked"/>
<field name="legend_done"/>
<field name="date_closed"/>
<field name="color"/>
<field name="user_id"/>
<field name="active"/>
<field name="application_status"/>
<field name="company_id"
invisible="1"/> <!-- We need to keep this field as it is used in the domain of user_id in the model -->
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger"}'/>
<templates>
<t t-name="menu">
<a role="menuitem" name="action_create_meeting" type="object" class="dropdown-item">Schedule
Interview
</a>
<a role="menuitem" name="archive_applicant" type="object" class="dropdown-item">Refuse</a>
<a t-if="record.active.raw_value" role="menuitem" type="archive" class="dropdown-item">Archive
</a>
<a t-if="!record.active.raw_value" role="menuitem" type="unarchive" class="dropdown-item">
Unarchive
</a>
<t t-if="widget.deletable">
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
</t>
</t>
<t t-name="card">
<widget name="web_ribbon" title="Hired" bg_color="text-bg-success" invisible="not date_closed"/>
<widget name="web_ribbon" title="Refused" bg_color="text-bg-danger"
invisible="application_status != 'refused'"/>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-secondary"
invisible="application_status != 'archived'"/>
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="applicant_properties" widget="properties"/>
<footer>
<field name="priority" widget="priority"/>
<field class="ms-1 align-items-center" name="activity_ids" widget="kanban_activity"/>
<div class="d-flex ms-auto align-items-center">
<a name="action_open_attachments" type="object">
<i class='fa fa-paperclip' role="img" aria-label="Documents"/>
<field name="attachment_number"/>
</a>
<field name="kanban_state" class="mx-1" widget="state_selection"/>
<field name="user_id" widget="many2one_avatar_user"/>
</div>
</footer>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="action_hr_job_recruitment_applications">
<field name="name">Applications</field>
<field name="res_model">hr.applicant</field>
<field name="view_mode">kanban,list,form,graph,calendar,pivot,activity</field>
<field name="search_view_id" ref="hr_applicant_view_search_bis_inherit"/>
<field name="context">{'search_default_hr_job_recruitment': [active_id], 'default_hr_job_recruitment':
active_id,
'search_default_job_recruitment_stage':1, 'dialog_size':'medium', 'allow_search_matching_applicants': 1}
</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('hr_kanban_view_applicant_inherit')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No applications yet
</p>
<p>
Odoo helps you track applicants in the recruitment
process and follow up all operations: meetings, interviews, etc.
</p>
<p>
Applicants and their attached résumé are created automatically when an email is sent.
If you install the document management modules, all resumes are indexed automatically,
so that you can easily search through their content.
</p>
</field>
</record>
<record model="ir.actions.act_window" id="action_hr_applicant_new_job_recruitment">
<field name="res_model">hr.applicant</field>
<field name="view_mode">form</field>
<field name="context">{'default_hr_job_recruitment': active_id}</field>
</record>
<record id="action_hr_recruitment_report_filtered_job_recruitment" model="ir.actions.act_window">
<field name="name">Recruitment Analysis</field>
<field name="res_model">hr.applicant</field>
<field name="view_mode">graph,pivot</field>
<field name="search_view_id" ref="hr_applicant_view_search_bis_inherit"/>
<field name="context">{
'search_default_creation_month': 1,
'search_default_hr_job_recruitment': [active_id],
'default_hr_job_recruitment': active_id}
</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p>
</field>
</record>
<!-- <record model="ir.ui.view" id="hr_recruitment_hr_job_survey_extended">-->
<!-- <field name="name">hr.job.survey.extended</field>-->
<!-- <field name="model">hr.job</field>-->
<!-- <field name="inherit_id" ref="hr_recruitment.hr_job_survey"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//div[@name='button_box']/button[@name='%(hr_recruitment.action_hr_job_applications)d']" position="attributes">-->
<!-- <attribute name="name">%(action_hr_job_recruitment_applications)d</attribute>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
<record id="hr_recruitment.crm_case_categ0_act_job" model="ir.actions.act_window">
<field name="search_view_id" ref="hr_applicant_view_search_bis_inherit"/>
<field name="context">{"search_default_job_recruitment_stage":1,"search_default_job_recruitment":1}</field>
</record>
</odoo>

View File

@ -0,0 +1,437 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_applicant_view_form_additional_info_inherit" model="ir.ui.view">
<field name="name">hr.applicant.view.additional.info.form</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="//notebook" position="inside">
<page name="additional_info" string="Additional Info" invisible="not employee_id">
<group colspan="2">
<group string="Personal Details">
<button string="Validate/update"
name="action_validate_personal_details"
type="object"
class="btn btn-success"
invisible="personal_details_status == 'pending'">
<div>
Click here to save &amp; Update Employee Data
<field name="personal_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_personal_details"
type="object"
class="btn btn-danger"
invisible="personal_details_status == 'validated'">
<div>
Click here to save, Validate and Update into Employee Data
<field name="personal_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<group colspan="2">
<field name="doj" string="Date of Joining"/>
<field name="gender"/>
<field name="birthday"/>
<field name="blood_group"/>
<field name="marital"/>
<field name="marriage_anniversary_date"
invisible="marital == 'single'"/>
</group>
</group>
<group string="Contact Details">
<button string="Validate/update"
name="action_validate_contact_details"
type="object"
class="btn btn-success"
invisible="contact_details_status == 'pending'">
<div>
Click here to save &amp; Update Employee Data
<field name="contact_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_contact_details"
type="object"
class="btn btn-danger"
invisible="contact_details_status == 'validated'">
<div>
Click here to save, Validate and Update into Employee Data
<field name="contact_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<group colspan="2">
<label for="private_street" string="Current Address"/>
<div class="o_address_format">
<field name="private_street" placeholder="Street..." class="o_address_street"/>
<field name="private_street2" placeholder="Street 2..." class="o_address_street"/>
<field name="private_city" placeholder="City" class="o_address_city"/>
<field name="private_state_id" class="o_address_state" placeholder="State"
options="{'no_open': True, 'no_quick_create': True}"
context="{'default_country_id': private_country_id}"/>
<field name="private_zip" placeholder="ZIP" class="o_address_zip"/>
<field name="private_country_id" placeholder="Country" class="o_address_country"
options="{&quot;no_open&quot;: True, &quot;no_create&quot;: True}"/>
</div>
<label for="permanent_street" string="Permanent Address"/>
<div class="o_address_format">
<field name="permanent_street" placeholder="Street..." class="o_address_street"/>
<field name="permanent_street2" placeholder="Street 2..." class="o_address_street"/>
<field name="permanent_city" placeholder="City" class="o_address_city"/>
<field name="permanent_state_id" class="o_address_state" placeholder="State"
options="{'no_open': True, 'no_quick_create': True}"
context="{'default_country_id': private_country_id}"/>
<field name="permanent_zip" placeholder="ZIP" class="o_address_zip"/>
<field name="permanent_country_id" placeholder="Country" class="o_address_country"
options="{&quot;no_open&quot;: True, &quot;no_create&quot;: True}"/>
</div>
</group>
</group>
<group string="Bank Details">
<button string="Validate/update"
name="action_validate_bank_details"
type="object"
class="btn btn-success"
invisible="bank_details_status == 'pending'">
<div>
Click here to save &amp; Update Employee Data
<field name="bank_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_bank_details"
type="object"
class="btn btn-danger"
invisible="bank_details_status == 'validated'">
<div>
Click here to save, Validate and Update into Employee Data
<field name="bank_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<group colspan="2">
<field name="full_name_as_in_bank"/>
<field name="bank_name"/>
<field name="bank_branch"/>
<field name="bank_account_no"/>
<field name="bank_ifsc_code"/>
</group>
</group>
<group string="Passport Details">
<button string="Validate/update"
name="action_validate_passport_details"
type="object"
class="btn btn-success"
invisible="passport_details_status == 'pending'">
<div>
Click here to save &amp; Update Employee Data
<field name="passport_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_passport_details"
type="object"
class="btn btn-danger"
invisible="passport_details_status == 'validated'">
<div>
Click here to save, Validate and Update into Employee Data
<field name="passport_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<group colspan="2">
<field name="passport_no"/>
<field name="passport_start_date"/>
<field name="passport_end_date"/>
<field name="passport_issued_location" string="Issued Location"/>
</group>
</group>
<group string="Authentication Details">
<button string="Validate/update"
name="action_validate_authentication_details"
type="object"
class="btn btn-success"
invisible="authentication_details_status == 'pending'">
<div>
Click here to save &amp; Update Employee Data
<field name="authentication_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_authentication_details"
type="object"
class="btn btn-danger"
invisible="authentication_details_status == 'validated'">
<div>
Click here to save, Validate and Update into Employee Data
<field name="authentication_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<group colspan="2">
<field name="pan_no"/>
<field name="identification_id"/>
<field name="previous_company_pf_no"/>
<field name="previous_company_uan_no"/>
</group>
</group>
</group>
<group>
<button string="Validate/update"
name="action_validate_family_education_employer_details"
type="object"
class="btn btn-success"
invisible="family_education_employer_details_status == 'pending'">
<div>
Click here to save &amp; Update Employee Data (Education, Employer, Family Details)
<field name="family_education_employer_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<button string="Validate/update"
name="action_validate_family_education_employer_details"
type="object"
class="btn btn-danger"
invisible="family_education_employer_details_status == 'validated'">
<div>
Click here to save, Validate and Update into Employee Data (Education, Employer, Family Details)
<field name="family_education_employer_details_status"
widget="badge"
options="{'pending': 'danger', 'validated': 'success'}"/>
</div>
</button>
<group string="Employer History" colspan="2">
<field mode="list" nolabel="1" name="employer_history">
<list string="Employer Details">
<field name="company_name"/>
<field name="designation"/>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" column_invisible="1"/>
</list>
<form string="Employer Details">
<sheet>
<group>
<group>
<field name="company_name"/>
<field name="designation"/>
</group>
<group>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</group>
<group string="Education History" colspan="2">
<field mode="list" nolabel="1" name="education_history" class="mt-2">
<list string="Education Details">
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<field name="employee_id" column_invisible="1"/>
</list>
<form string="Education Details">
<sheet>
<group>
<group>
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
</group>
<group>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<field name="employee_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</group>
<group string="Family Details" colspan="2">
<field name="family_details" nolabel="1">
<list editable="bottom">
<field name="relation_type"/>
<field name="name"/>
<field name="contact_no"/>
<field name="dob"/>
<field name="location"/>
</list>
</field>
</group>
</group>
</page>
</xpath>
</field>
</record>
<record id="hr_candidate_view_form_additional_info_inherit" model="ir.ui.view">
<field name="name">hr.candidate.view.additional.info.form</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page name="additional_info" string="Additional Info" invisible="not employee_id">
<!-- <group>-->
<group string="Employer History">
<field mode="list" nolabel="1" name="employer_history">
<list string="Employer Details">
<field name="company_name"/>
<field name="designation"/>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" column_invisible="1"/>
</list>
<form string="Employer Details">
<sheet>
<group>
<group>
<field name="company_name"/>
<field name="designation"/>
</group>
<group>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="employee_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</group>
<group string="Education History">
<field mode="list" nolabel="1" name="education_history"
class="mt-2">
<list string="Education Details">
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<!-- <field name="attachments"/>-->
<field name="employee_id" column_invisible="1"/>
</list>
<form string="Education Details">
<sheet>
<group>
<group>
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
</group>
<group>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
<field name="employee_id" invisible="1"/>
</group>
</group>
<!-- <group>-->
<!-- <field name="attachments" widget="one2many_list">-->
<!-- <list editable="bottom">-->
<!-- <field name="name"/>-->
<!-- <field name="file"/>-->
<!-- </list>-->
<!-- </field>-->
<!-- </group>-->
</sheet>
</form>
</field>
</group>
<group string="Family Details">
<field name="family_details" nolabel="1">
<list editable="bottom">
<field name="relation_type"/>
<field name="name"/>
<field name="contact_no"/>
<field name="dob"/>
<field name="location"/>
</list>
</field>
</group>
<!-- </group>-->
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,367 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="view_hr_job_recruitment_tree" model="ir.ui.view">
<field name="name">hr.job.recruitment.form</field>
<field name="model">hr.job.recruitment</field>
<field name="arch" type="xml">
<list js_class="recruitment_list_view">
<field name="recruitment_sequence"/>
<field name="job_id"/>
<field name="recruitment_type" optional="hide"/>
<field name="department_id"/>
<field name="no_of_recruitment"/>
<field name="application_count" string="Applications"
groups="hr_recruitment.group_hr_recruitment_interviewer"/>
<field name="expected_employees" optional="hide"/>
<field name="no_of_hired_employee" optional="hide"/>
<field name="message_needaction" column_invisible="True"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="company_id" column_invisible="True"/>
<field name="alias_name" column_invisible="True"/>
<field name="alias_id" invisible="not alias_name" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user" optional="hide"/>
<field name="no_of_employee"/>
</list>
</field>
</record>
<!-- Inherit the hr.job form view and add the custom field -->
<record id="view_hr_job_recruitment_form" model="ir.ui.view">
<field name="name">hr.job.recruitment.form</field>
<field name="model">hr.job.recruitment</field>
<field name="arch" type="xml">
<!-- <form string="job">-->
<!-- <sheet>-->
<!-- <group>-->
<!-- <div class="oe_title">-->
<!-- <field name="recruitment_sequence" readonly="1" force_save="1"/>-->
<!-- <field name="job_id"/>-->
<!-- </div>-->
<!-- </group>-->
<!-- </sheet>-->
<!-- </form>-->
<!-- Add the recruitment_sequence field into the form -->
<form string="Job" js_class="recruitment_form_view">
<header/> <!-- inherited in other module -->
<field name="active" invisible="1"/>
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
<sheet>
<div name="button_box" position="inside">
<button class="oe_stat_button"
icon="fa-pencil"
name="%(hr_recruitment_extended.action_hr_job_recruitment_applications)d"
context="{'default_user_id': user_id, 'active_test': False}"
type="action">
<field name="all_application_count" widget="statinfo" string="Job Applications form"/>
</button>
<button class="oe_stat_button"
icon="fa-file-text-o"
name="action_open_attachments"
type="object"
invisible="documents_count == 0">
<field name="documents_count" widget="statinfo" string="Documents"/>
</button>
<button class="oe_stat_button" type="action"
name="%(hr_recruitment.action_hr_job_sources)d" icon="fa-bar-chart-o"
context="{'default_job_recruitment_id': id}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Trackers</span>
</div>
</button>
</div>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" icon="fa-pencil" name="%(action_hr_job_recruitment_applications)d"
context="{'default_user_id': user_id, 'active_test': False}" type="action">
<field name="all_application_count" widget="statinfo" string="Job Applications"/>
</button>
<button class="oe_stat_button" icon="fa-file-text-o" name="action_open_attachments"
type="object" invisible="documents_count == 0">
<field name="documents_count" widget="statinfo" string="Documents"/>
</button>
<button class="oe_stat_button" type="action" name="233" icon="fa-bar-chart-o"
context="{'default_job_recruitment_id': id}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Trackers</span>
</div>
</button>
<field name="is_published" widget="website_redirect_button" on_change="1"/>
<button name="buttion_view_applicants" type="object" class="oe_stat_button"
string="Candidates" widget="statinfo" icon="fa-th-large"/>
</div>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="float-end">
<field name="website_published" widget="boolean_toggle_labeled" nolabel="1"
options="{'false_label': 'Not Published', 'true_label': 'Published'}" on_change="1"/>
</div>
<div class="oe_title">
<field name="recruitment_sequence" readonly="0" force_save="1"/>
<group>
<field name="job_id" string="Job Position"/>
</group>
</div>
<notebook>
<page string="Recruitment" name="recruitment_page" invisible="0">
<group>
<group name="recruitment">
<field name="company_id" options="{'no_create': True}" invisible="1"
on_change="1" can_create="True" can_write="True"/>
<field name="recruitment_type"/>
<field name="requested_by" can_create="True" can_write="True"/>
<field name="department_id" can_create="True" can_write="True" invisible="1"/>
<label for="address_id"/>
<div class="o_row">
<span invisible="address_id" class="oe_read_only">Remote</span>
<field name="address_id" context="{'show_address': 1}" placeholder="Remote"
can_create="True" can_write="True"/>
</div>
<field name="industry_id" can_create="True" can_write="True"/>
<label for="alias_name" string="Email Alias"
help="Define a specific contact address for this job position. If you keep it empty, the default email address will be used which is in human resources settings"/>
<div name="alias_def">
<field name="alias_id" class="oe_read_only" string="Email Alias"
required="0" on_change="1" can_create="True" can_write="True"/>
<div class="oe_edit_only" name="edit_alias">
<field name="alias_name" class="oe_inline" placeholder="e.g. jobs"
on_change="1"/>@
<field name="alias_domain_id" class="oe_inline"
placeholder="e.g. domain.com"
options="{'no_create': True, 'no_open': True}" can_create="True"
can_write="True"/>
</div>
</div>
<field name="contract_type_id" can_create="True" can_write="True"/>
<field name="skill_ids" widget="many2many_tags"
options="{'color_field': 'color'}"
context="{'search_default_group_skill_type_id': 1}" can_create="True"
can_write="True"/>
<field name="secondary_skill_ids" widget="many2many_tags"
options="{'color_field': 'color'}"
context="{'search_default_group_skill_type_id': 1}" can_create="True"
can_write="True"/>
<field name="company_id" on_change="1" can_create="True" can_write="True"/>
<field name="target_from" widget="daterange" string="Mission Dates"
options="{'end_date_field': 'target_to'}"/>
<field name="target_to" invisible="1"/>
<field name="date_from" widget="daterange" string="Mission Dates"
options="{'end_date_field': 'date_to'}" invisible="1"/>
<field name="date_to" invisible="1"/>
</group>
<group name="recruitment2">
<label for="no_of_recruitment"/>
<div class="o_row" name="recruitment_target">
<field name="no_of_recruitment" class="o_hr_narrow_field"/>
<span>new Employees to hire</span>
</div>
<field name="no_of_eligible_submissions"/>
<field name="website_id" options="{'no_create': True}"
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"
on_change="1" can_create="True" can_write="True"/>
<field name="user_id" widget="many2one_avatar_user" can_create="True"
can_write="True" string="Primary Recruiter"/>
<field name="interviewer_ids" string="Secondary Recruiters" widget="many2many_tags_avatar"
options="{'no_create': True, 'no_create_edit': True}" can_create="True"
can_write="True"/>
<field name="locations" widget="many2many_tags" can_create="True"
can_write="True"/>
<field name="recruitment_stage_ids" widget="many2many_tags" can_create="True"
can_write="True"/>
</group>
</group>
<field name="job_properties" columns="2"/>
</page>
<page string="Job Summary" name="job_description_page" invisible="0">
<field name="description" options="{'collaborative': true}"
placeholder="e.g. Summarize the position in one or two lines that will be displayed on the Jobs list page..."/>
</page>
<page string="Application Info" name="recruitment_page">
<separator string="Process Details"/>
<field name="job_details" nolabel="1"/>
</page>
</notebook>
</sheet>
<chatter open_attachments="True"/>
</form>
</field>
</record>
<record id="view_job_recruitment_filter" model="ir.ui.view">
<field name="name">hr.job.recruitment.search</field>
<field name="model">hr.job.recruitment</field>
<field name="arch" type="xml">
<search string="Jobs">
<field name="job_id" string="Job Position"/>
<field name="department_id" operator="child_of"/>
<separator/>
<filter name="message_needaction" string="Unread Messages"
domain="[('message_needaction', '=', True)]"
groups="mail.group_mail_notification_type_inbox"/>
<separator/>
<filter name="archived" string="Archived" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Department" name="department" domain="[]"
context="{'group_by': 'department_id'}"/>
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}"
groups="base.group_multi_company"/>
<filter string="Employment Type" name="employment_type" domain="[]"
context="{'group_by': 'contract_type_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_job_recruitment_kanban">
<field name="name">hr.job.recruitment.kanban</field>
<field name="model">hr.job.recruitment</field>
<field name="arch" type="xml">
<kanban highlight_color="color" class="o_hr_recruitment_kanban" sample="1" limit="40" action="%(action_hr_job_recruitment_applications)d" type="action" js_class="recruitment_kanban_view">
<field name="active"/>
<field name="alias_email"/>
<templates>
<t t-name="menu" groups="hr_recruitment.group_hr_recruitment_user">
<div class="container">
<div class="row">
<div class="col-6">
<h5 role="menuitem" class="o_kanban_card_manage_title">
<span>View</span>
</h5>
<div role="menuitem" name="menu_view_applications">
<a name="%(action_hr_job_recruitment_applications)d" type="action">Applications</a>
</div>
<div role="menuitem">
<a name="action_open_activities" type="object">Activities</a>
</div>
<div role="menuitem" name="menu_view_job_posts">
<a name="%(hr_recruitment.action_hr_job_sources)d" type="action" context="{'default_job_recruitment_id': id}">Trackers</a>
</div>
</div>
<div class="col-6">
<h5 role="menuitem" class="o_kanban_card_manage_title">
<span>New</span>
</h5>
<div role="menuitem" name="menu_new_applications">
<a name="%(hr_recruitment_extended.action_hr_applicant_new_job_recruitment)d" type="action">Application</a>
</div>
</div>
<div class="col-6">
<h5 role="menuitem" class="o_kanban_card_manage_title">
<span>Reporting</span>
</h5>
<div role="menuitem" name="kanban_job_reporting">
<a name="%(hr_recruitment_extended.action_hr_recruitment_report_filtered_job_recruitment)d" type="action">Analysis</a>
</div>
</div>
</div>
<div class="o_kanban_card_manage_settings row">
<div class="col-6" role="menuitem" aria-haspopup="true">
<field name="color" widget="kanban_color_picker"/>
</div>
<div class="col-6" role="menuitem">
<a class="dropdown-item" t-if="widget.editable" name="edit_job" type="open">Configuration</a>
<a class="dropdown-item" t-if="record.active.raw_value" type="archive">Archive</a>
<a class="dropdown-item" t-if="!record.active.raw_value" name="toggle_active" type="object">Unarchive</a>
</div>
</div>
</div>
</t>
<t t-name="card">
<div class="d-flex align-items-baseline gap-1 ms-2">
<field name="is_favorite" widget="boolean_favorite" nolabel="1"/>
<div class="o_kanban_card_header_title d-flex flex-column">
<div class="oe_row">
<field name="recruitment_sequence" class="fw-bold fs-4"/> -
<field name="job_id"/>
</div>
<field name="requested_by" class="text-muted"/>
<div class="small" groups="base.group_multi_company">
<i class="fa fa-building-o" role="img" aria-label="Company" title="Company"/> <field name="company_id"/>
</div>
<div t-if="record.alias_email.value" class="small o_job_alias">
<i class="fa fa-envelope-o" role="img" aria-label="Alias" title="Alias"/> <field name="alias_id"/>
</div>
</div>
</div>
<div class="row g-0 mt-0 mt-sm-3 ms-2">
<div class="col-7">
<button class="btn btn-primary" name="%(action_hr_job_recruitment_applications)d" type="action">
<field name="new_application_count"/> New Applications
</button>
</div>
<div class="col-5">
<a name="edit_job" type="open" t-attf-class="{{ record.no_of_recruitment.raw_value &gt; 0 ? 'text-primary fw-bolder' : 'text-secondary' }}" groups="hr_recruitment.group_hr_recruitment_user">
<field name="no_of_recruitment"/> To Recruit
</a>
<span t-attf-class="{{ record.no_of_recruitment.raw_value &gt; 0 ? 'text-primary fw-bolder' : 'text-secondary' }}" groups="!hr_recruitment.group_hr_recruitment_user">
<field name="no_of_recruitment"/> To Recruit
</span>
<div t-if="record.application_count.raw_value &gt; 0">
<field name="application_count"/> Applications
</div>
<div t-if="record.no_of_hired_employee.raw_value &gt; 0">
<field name="no_of_hired_employee"/> Hired
</div>
<div t-if="record.no_of_submissions.raw_value &gt; 0">
<field name="no_of_submissions"/>
Submissions
</div>
<div t-if="record.no_of_refused_submissions.raw_value &gt; 0">
<field name="no_of_refused_submissions"/>
Refused Submissions
</div>
<a t-if="record.activities_today.raw_value &gt; 0" name="action_open_today_activities" type="object" class="text-warning"><field name="activities_today"/> Activities Today</a>
<br t-if="record.activities_today.raw_value &gt; 0 and record.activities_overdue.raw_value &gt; 0"/>
<a t-if="record.activities_overdue.raw_value &gt; 0" name="action_open_late_activities" type="object" class="text-danger"><field name="activities_overdue"/> Late Activities</a>
</div>
</div>
<div name="kanban_boxes" class="row g-0 flex-nowrap mt-auto" groups="hr_recruitment.group_hr_recruitment_user"/>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_hr_job_recruitment" model="ir.actions.act_window">
<field name="name">Job Positions Recruitment</field>
<field name="res_model">hr.job.recruitment</field>
<field name="view_mode">kanban,list,form,search</field>
<field name="search_view_id" ref="view_job_recruitment_filter"/>
<field name="context">{"search_default_Current":1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Ready to recruit more efficiently?
</p>
<p>
Let's create a job position Recruitment Requests.
</p>
</field>
</record>
<menuitem
name="Job Positions Recruitment"
id="menu_hr_job_recruitment_interviewer"
parent="hr_recruitment.menu_crm_case_categ0_act_job"
action="action_hr_job_recruitment"
sequence="1"
groups="base.group_user"/>
</data>
</odoo>

View File

@ -1,220 +1,324 @@
<odoo>
<data>
<data>
<record model="ir.ui.view" id="hr_view_hr_job_form_extended">
<field name="name">hr.job.form.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="History" name="hiring_history_page">
<field name="hiring_history">
<list editable="bottom">
<field name="date_from"/>
<field name="date_end"/>
<field name="target"/>
<field name="hired" widget="many2many_tags"/>
<field name="job_id" invisible="1"/>
</list>
</field>
</page>
</xpath>
<!-- <xpath expr="//page[@name='job_description_page']" position="after">-->
<!-- <page string="History" name="hiring_history_page">-->
<!-- <field name="hiring_history">-->
<!-- <tree editable="top">-->
<!-- <field name="date_from"/>-->
<!-- <field name="date_end"/>-->
<!-- <field name="target"/>-->
<!-- <field name="hired"/>-->
<!-- </tree>-->
<!-- </field>-->
<!-- </page>-->
<!-- </xpath>-->
</field>
</record>
<record id="action_hr_job_report_filtered_job_recruitment" model="ir.actions.act_window">
<field name="name">Recruitment Analysis</field>
<field name="res_model">hr.applicant</field>
<field name="view_mode">kanban,list,form,graph,pivot</field>
<field name="view_mode">kanban,list,form,graph,calendar,pivot,activity</field>
<field name="search_view_id" ref="hr_applicant_view_search_bis_inherit"/>
<field name="context">{'search_default_job_id': [active_id], 'default_job_id':
active_id,
'search_default_job_recruitment_stage':1, 'dialog_size':'medium', 'allow_search_matching_applicants': 1}
</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('hr_kanban_view_applicant_inherit')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p>
</field>
<record model="ir.ui.view" id="hr_recruitment_hr_job_survey_extended">
<field name="name">hr.job.survey.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment.hr_job_survey"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date_from']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='date_from']" position="before">
<field name="target_from" widget="daterange" string="Mission Dates"
options="{'end_date_field': 'target_to'}"/>
<field name="target_to" invisible="1"/>
</xpath>
</record>
<record id="action_hr_job_recruitment_requests" model="ir.actions.act_window">
<field name="name">Job Positions Recruitment</field>
<field name="res_model">hr.job.recruitment</field>
<field name="view_mode">kanban,list,form,search</field>
<field name="search_view_id" ref="view_job_recruitment_filter"/>
<field name="context">{'search_default_job_id': [active_id], 'default_job_id':
active_id}
</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Ready to recruit more efficiently?
</p>
<p>
Let's create a job position Recruitment Requests.
</p>
</field>
</record>
</field>
</record>
<record id="hr_recruitment_hr_job_simple_form_inherit" model="ir.ui.view">
<field name="name">hr.job.simple.form.inherit</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment.hr_job_simple_form"/>
<field name="arch" type="xml">
<xpath expr="//label[@for='alias_name']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//div[@name='alias_def']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_job_form_extended">
<field name="name">hr.job.form.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment_skills.hr_job_form_inherit_hr_recruitment_skills"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="buttion_view_applicants" type="object" class="oe_stat_button" string="Candidates" widget="statinfo" icon="fa-th-large"/>
</xpath>
<xpath expr="//field[@name='skill_ids']" position="after">
<field name="secondary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}"
context="{'search_default_group_skill_type_id': 1}"/>
</xpath>
<xpath expr="//group[@name='recruitment2']" position="inside">
<field name="locations" widget="many2many_tags"/>
<field name="recruitment_stage_ids" widget="many2many_tags"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_job_view_tree_inherit_extended">
<field name="name">hr.job.tree.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment.hr_job_view_tree_inherit"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='no_of_recruitment']" position="attributes">
<attribute name="column_invisible">1</attribute>
</xpath>
<record model="ir.ui.view" id="hr_recruitment_hr_applicant_view_form_extend">
<field name="name">hr.applicant.view.form.extended</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
<xpath expr="//field[@name='no_of_recruitment']" position="before">
<field name="require_no_of_recruitment"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_view_hr_job_kanban_extended">
<field name="name">hr.job.kanban.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment.view_hr_job_kanban"/>
<field name="arch" type="xml">
<xpath expr="//t[@t-name='card']/div[@class='row g-0 mt-0 mt-sm-3 ms-2']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//div[@name='kanban_boxes']" position="attributes">
<attribute name="class" add=""/>
</xpath>
<xpath expr="//kanban" position="attributes">
<!-- action="%(action_hr_job_recruitment_applications)d" type="action"-->
<attribute name="action">%(hr_recruitment_extended.action_hr_job_recruitment_requests)d</attribute>
</xpath>
<xpath expr="//field[@name='no_of_recruitment']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='refuse_reason_id']" position="after">
<field name="refused_state" invisible="not refuse_reason_id"/>
</xpath>
<xpath expr="//field[@name='no_of_recruitment']" position="before">
<field name="require_no_of_recruitment"/>
</xpath>
<xpath expr="//button[@name='%(hr_recruitment.action_hr_job_applications)d']" position="attributes">
<attribute name="name">%(action_hr_job_report_filtered_job_recruitment)d</attribute>
</xpath>
</field>
</record>
<record id="hr_job_survey_inherit" model="ir.ui.view">
<field name="name">hr.job.form1.inherit</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment.hr_job_survey"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']//button[@name='%(hr_recruitment.action_hr_job_applications)d']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//div[@name='button_box']//button[@name='action_open_attachments']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_view_hr_job_form_extended">
<field name="name">hr.job.form.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="All Recruitments" name="hr_job_recruitments_page">
<field name="hr_job_recruitments"/>
<!-- <field name="hr_job_recruitments">-->
<!-- <list editable="bottom">-->
<!-- <field name="recruitment_sequence"/>-->
<!-- <field name="date_from"/>-->
<!-- <field name="date_end"/>-->
<!-- <field name="target"/>-->
<!-- <field name="application_count"/>-->
<!-- <field name="applicant_hired"/>-->
<!-- </list>-->
<!-- </field>-->
</page>
</xpath>
</field>
</record>
<xpath expr="//field[@name='linkedin_profile']" position="after">
<field name="exp_type"/>
</xpath>
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
<field name="current_ctc"/>
</xpath>
<xpath expr="//page[@name='application_details']" position="inside">
<group>
<group string="Location" name="location_details">
<field name="current_location"/>
<field name="preferred_location" widget="many2many_tags"/>
<field name="current_organization"/>
</group>
<group string="Experience" name="applicant_experience">
<label for="total_exp" string="Total Experience"/>
<div class="o_row">
<field name="total_exp" placeholder="Total Experience"/>
<field name="total_exp_type" placeholder="Experience Type" required="total_exp &gt; 0"/>
</div>
<label for="relevant_exp" string="Relevant Experience"/>
<div class="o_row">
<field name="relevant_exp" placeholder="Relevant Experience"/>
<field name="relevant_exp_type" placeholder="Experience Type" required="relevant_exp &gt; 0"/>
</div>
<label for="notice_period" string="Notice Period"/>
<div class="o_row">
<field name="notice_period" placeholder="Relevant Experience"/>
<field name="notice_period_type" placeholder="Experience Type" required="relevant_exp &gt; 0"/>
</div>
</group>
</group>
<!-- <record model="ir.ui.view" id="hr_job_form_extended">-->
<!-- <field name="name">hr.job.form.extended</field>-->
<!-- <field name="model">hr.job</field>-->
<!-- <field name="inherit_id" ref="hr_recruitment_skills.hr_job_form_inherit_hr_recruitment_skills"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//div[hasclass('oe_button_box')]" position="inside">-->
<!-- <button name="buttion_view_applicants" type="object" class="oe_stat_button" string="Candidates" widget="statinfo" icon="fa-th-large"/>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='skill_ids']" position="after">-->
<!-- <field name="secondary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}"-->
<!-- context="{'search_default_group_skill_type_id': 1}"/>-->
<!-- </xpath>-->
<!-- <xpath expr="//group[@name='recruitment2']" position="inside">-->
<!-- <field name="locations" widget="many2many_tags"/>-->
<!-- <field name="recruitment_stage_ids" widget="many2many_tags"/>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
<record model="ir.ui.view" id="hr_recruitment_hr_applicant_view_form_extend">
<field name="name">hr.applicant.view.form.extended</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
<xpath expr="//field[@name='refuse_reason_id']" position="after">
<field name="refused_state" invisible="not refuse_reason_id"/>
</xpath>
<xpath expr="//field[@name='linkedin_profile']" position="after">
<field name="exp_type"/>
<field name="resume" force_save="1"/>
<field name="submitted_to_client" force_save="1" readonly="1"/>
<field name="client_submission_date" force_save="1" readonly="1"/>
</xpath>
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
<field name="current_ctc"/>
</xpath>
<xpath expr="//page[@name='application_details']" position="inside">
<group>
<group string="Negotiation" name="negotiation_details">
<field name="salary_negotiable"/>
<field name="np_negotiable"/>
<field name="holding_offer"/>
</group>
<group string="Comments" name="comments">
<field name="applicant_comments"/>
<field name="recruiter_comments"/>
</group>
</group>
<group string="Location" name="location_details">
<field name="current_location"/>
<field name="preferred_location" widget="many2many_tags"/>
<field name="current_organization"/>
</group>
<group string="Experience" name="applicant_experience">
<label for="total_exp" string="Total Experience"/>
<div class="o_row">
<field name="total_exp" placeholder="Total Experience"/>
<field name="total_exp_type" placeholder="Experience Type" required="total_exp &gt; 0"/>
</div>
<label for="relevant_exp" string="Relevant Experience"/>
<div class="o_row">
<field name="relevant_exp" placeholder="Relevant Experience"/>
<field name="relevant_exp_type" placeholder="Experience Type"
required="relevant_exp &gt; 0"/>
</div>
<label for="notice_period" string="Notice Period"/>
<div class="o_row">
<field name="notice_period" placeholder="Relevant Experience"/>
<field name="notice_period_type" placeholder="Experience Type"
required="relevant_exp &gt; 0"/>
</div>
</group>
</group>
<group>
<group string="Negotiation" name="negotiation_details">
<field name="salary_negotiable"/>
<field name="np_negotiable"/>
<field name="holding_offer"/>
</group>
<group string="Comments" name="comments">
<field name="applicant_comments"/>
<field name="recruiter_comments"/>
</group>
</group>
</xpath>
</field>
</record>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_candidate_view_form_inherit">
<field name="name">hr.candidate.view.form.inherit</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_form"/>
<field name="arch" type="xml">
<!-- <xpath expr="//field[@name='partner_name']" position="attributes">-->
<!-- <attribute name="readonly">1</attribute>-->
<!-- </xpath>-->
<record model="ir.ui.view" id="hr_candidate_view_form_inherit">
<field name="name">hr.candidate.view.form.inherit</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_form"/>
<field name="arch" type="xml">
<!-- <xpath expr="//field[@name='partner_name']" position="attributes">-->
<!-- <attribute name="readonly">1</attribute>-->
<!-- </xpath>-->
<xpath expr="//widget[@name='web_ribbon']" position="after">
<div class="o_employee_avatar m-0 p-0">
<field name="candidate_image" widget="image" class="oe_avatar m-0"
options="{&quot;zoom&quot;: true, &quot;preview_image&quot;:&quot;candidate_image&quot;}"/>
<xpath expr="//form/sheet/group" position="before">
<group>
<group string="Candidate's Name">
<field name="first_name"/>
<field name="middle_name"/>
<field name="last_name"/>
</group>
</group>
</xpath>
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
</div>
</xpath>
</field>
</record>
<xpath expr="//form/sheet/group" position="before">
<group>
<group string="Candidate's Name">
<field name="first_name"/>
<field name="middle_name"/>
<field name="last_name"/>
</group>
</group>
</xpath>
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
<xpath expr="//field[@name='categ_ids']" position="after">
<field name="resume"/>
</xpath>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="hr_recruitment_extended.list">
<field name="name">hr_recruitment_extended list</field>
<field name="model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
</field>
</record>
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="hr_recruitment_extended.action_window">
<field name="name">hr_recruitment_extended window</field>
<field name="res_model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="hr_recruitment_extended.list">
<field name="name">hr_recruitment_extended list</field>
<field name="model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="hr_recruitment_extended.action_server">
<field name="name">hr_recruitment_extended server</field>
<field name="model_id" ref="model_hr_recruitment_extended_hr_recruitment_extended"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="hr_recruitment_extended.action_window">
<field name="name">hr_recruitment_extended window</field>
<field name="res_model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="hr_recruitment_extended" id="hr_recruitment_extended.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="hr_recruitment_extended.menu_1" parent="hr_recruitment_extended.menu_root"/>
<menuitem name="Menu 2" id="hr_recruitment_extended.menu_2" parent="hr_recruitment_extended.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="hr_recruitment_extended.menu_1_list" parent="hr_recruitment_extended.menu_1"
action="hr_recruitment_extended.action_window"/>
<menuitem name="Server to list" id="hr_recruitment_extended" parent="hr_recruitment_extended.menu_2"
action="hr_recruitment_extended.action_server"/>
-->
</data>
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="hr_recruitment_extended.action_server">
<field name="name">hr_recruitment_extended server</field>
<field name="model_id" ref="model_hr_recruitment_extended_hr_recruitment_extended"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="hr_recruitment_extended" id="hr_recruitment_extended.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="hr_recruitment_extended.menu_1" parent="hr_recruitment_extended.menu_root"/>
<menuitem name="Menu 2" id="hr_recruitment_extended.menu_2" parent="hr_recruitment_extended.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="hr_recruitment_extended.menu_1_list" parent="hr_recruitment_extended.menu_1"
action="hr_recruitment_extended.action_window"/>
<menuitem name="Server to list" id="hr_recruitment_extended" parent="hr_recruitment_extended.menu_2"
action="hr_recruitment_extended.action_server"/>
-->
<menuitem
name="By Job Positions"
id="hr_recruitment.menu_hr_job_position"
parent="hr_recruitment.menu_crm_case_categ0_act_job"
action="hr_recruitment.action_hr_job"
sequence="30"
groups="hr_recruitment.group_hr_recruitment_user"/>
</data>
</odoo>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.ui.view" id="hr_recruitment_source_tree_inherit">
<field name="name">hr.recruitment.source.list.inherit</field>
<field name="model">hr.recruitment.source</field>
<field name="inherit_id" ref="hr_recruitment.hr_recruitment_source_tree"/>
<field name="arch" type="xml">
<field name="job_id" position="after">
<field name="job_recruitment_id"/>
</field>
</field>
</record>
<record id="hr_recruitment_source_view_search_inherit" model="ir.ui.view">
<field name="name">hr.recruitment.source.view.search.inherit</field>
<field name="model">hr.recruitment.source</field>
<field name="inherit_id" ref="hr_recruitment.hr_recruitment_source_view_search"/>
<field name="arch" type="xml">
<field name="job_id" position="after">
<field name="job_recruitment_id"/>
</field>
</field>
</record>
<record model="ir.actions.act_window" id="hr_recruitment.action_hr_job_sources">
<field name="search_view_id" ref="hr_recruitment_source_view_search_inherit"/>
<field name="context">{'search_default_job_recruitment_id': [active_id], 'default_job_recruitment_id':
active_id}
</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Want to analyse where applications come from ?
</p>
<p>
Use emails and links trackers
</p>
</field>
</record>
</odoo>

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- list View -->
<record id="view_recruitment_attachments_list" model="ir.ui.view">
<field name="name">recruitment.attachments.list</field>
<field name="model">recruitment.attachments</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="name"/>
<field name="attachment_type"/>
<field name="is_default"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_recruitment_attachments_form" model="ir.ui.view">
<field name="name">recruitment.attachments.form</field>
<field name="model">recruitment.attachments</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="is_default"/>
</group>
<group>
<field name="employee_recruitment_attachments" widget="one2many_list">
<list editable="top">
<field name="name"/>
<field name="applicant_id"/>
<field name="file" widget="binary" filename="name" options="{'preview_image': 'file','download': true}" />
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_view_employee_form_inherit" model="ir.ui.view">
<field name="name">hr.view.employee.form.inherit</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page name="Attachments " id="attachment_ids_page">
<group>
<field name="employee_attachment_ids">
<list editable="bottom">
<field name="recruitment_attachment_id"/>
<field name="name"/>
<field name="recruitment_attachment_type"/>
<field name="file" widget="binary" options="{'download':true}"/>
</list>
</field>
</group>
</page>
</xpath>
</field>
</record>
<!-- Action -->
<record id="action_recruitment_attachments" model="ir.actions.act_window">
<field name="name">Recruitment Attachments</field>
<field name="res_model">recruitment.attachments</field>
<field name="view_mode">list</field>
</record>
<!-- Menu Item -->
<menuitem
id="menu_recruitment_attachments"
name="Attachments"
parent="hr_recruitment.menu_hr_recruitment_config_applications"
action="action_recruitment_attachments"
groups="base.group_no_one"
sequence="33"/>
</odoo>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="view_requisition_form_inherit" model="ir.ui.view">
<field name="name">requisition.form.inherit</field>
<field name="model">recruitment.requisition</field>
<field name="inherit_id" ref="requisitions.view_requisition_form"/>
<field name="arch" type="xml">
<!-- <field name="job_id" invisible="job_id == False" readonly="1" force_save="1"/>-->
<xpath expr="//field[@name='job_id']" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="readonly">state not in ['draft']</attribute>
<attribute name="required">1</attribute>
</xpath>
<xpath expr="//field[@name='job_id']" position="after">
<field name="hr_job_recruitment" readonly="1" force_save="1"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="view_res_partner_filter_recruitment" model="ir.ui.view">
<field name="name">view.res.partner.filter.inherit.recruitment</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<field name="category_id" position="before">
<field name="contact_type"/>
</field>
<filter name="salesperson" position="after">
<filter string="Client Type" name="contact_type" context="{'group_by': 'contact_type'}"/>
</filter>
<xpath expr="//search" position="inside">
<filter name="internal_contact" string="Internal Contact" domain="[('contact_type', '=', 'internal')]"/>
<filter name="external_contact" string="External Contact" domain="[('contact_type', '=', 'external')]"/>
</xpath>
</field>
</record>
<record id="view_partner_form_inherit_recruitment" model="ir.ui.view">
<field name="name">base.view.partner.form.inherit.recruitment</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='category_id']" position="after">
<!-- <group>-->
<field name="contact_type"/>
<!-- </group>-->
</xpath>
</field>
</record>
<record id="action_contacts_recruitments" model="ir.actions.act_window">
<field name="name">Contacts</field>
<field name="path"></field>
<field name="res_model">res.partner</field>
<field name="view_mode">kanban,list,form,activity</field>
<field name="search_view_id" ref="base.view_res_partner_filter"/>
<field name="context">{'search_default_external_contact': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Contact in your address book
</p>
<p>
Odoo helps you track all activities related to your contacts.
</p>
</field>
</record>
<menuitem
id="menu_hr_recruitment_config_contacts"
name="Contacts"
parent="hr_recruitment.menu_hr_recruitment_configuration"
sequence="10"/>
<menuitem
id="menu_hr_recruitment_stage"
name="Clients"
parent="menu_hr_recruitment_config_contacts"
action="action_contacts_recruitments"
groups="base.group_user"
sequence="1"/>
</data>
</odoo>

View File

@ -0,0 +1,50 @@
<odoo>
<record id="view_resume_parser_form" model="ir.ui.view">
<field name="name">resume.parser.form</field>
<field name="model">resume.parser</field>
<field name="arch" type="xml">
<form string="Resume Parser">
<sheet>
<group>
<field name="resume_file" filename="resume_filename"/>
<button name="action_parse_resume" type="object" string="Parse Resume" class="btn-primary"/>
</group>
<group>
<field name="name" readonly="1"/>
<field name="email" readonly="1"/>
<field name="phone" readonly="1"/>
<field name="skills_text" readonly="1"/>
<field name="experience_text" readonly="1"/>
<field name="degree_text" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_resume_parser_list" model="ir.ui.view">
<field name="name">resume.parser.list</field>
<field name="model">resume.parser</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="email"/>
<field name="phone"/>
<field name="skills_text"/>
</list>
</field>
</record>
<record id="action_resume_parser" model="ir.actions.act_window">
<field name="name">Resumes</field>
<field name="res_model">resume.parser</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_resume_parser_root" name="Resume Parsing" sequence="10"/>
<menuitem id="menu_resume_parser_main" name="Resumes"
parent="menu_resume_parser_root" action="action_resume_parser"/>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="employee_skill_level_view_tree_inherit" model="ir.ui.view">
<field name="name">hr.skill.level.list.inherit</field>
<field name="model">hr.skill.level</field>
<field name="inherit_id" ref="hr_skills.employee_skill_level_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="sequence" widget="handle"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -9,6 +9,13 @@
<xpath expr="//field[@name='fold']" position="before">
<field name="is_default_field" string="Is Default"/>
</xpath>
<xpath expr="//field[@name='job_ids']" position="after">
<field name="job_recruitment_ids" string="Job Recruitment Ids" widget="many2many_tags"/>
<field name="second_application_form"/>
<field name="post_onboarding_form"/>
<field name="require_approval" string="Approval Required"/>
<field name="stage_color" widget="color" string="Select Stage Color"/>
</xpath>
</field>
</record>

View File

@ -1,364 +0,0 @@
<odoo>
<template id="apply_extend" inherit_id="website_hr_recruitment.apply">
<xpath expr="//form[@id='hr_recruitment_form']" position="replace">
<!-- Your custom content here -->
<form id="hr_recruitment_form" action="/website/form/" method="post"
enctype="multipart/form-data" class="o_mark_required row"
data-mark="*" data-model_name="hr.applicant"
data-success-mode="redirect" data-success-page="/job-thank-you"
hide-change-model="true">
<div class="s_website_form_rows s_col_no_bgcolor">
<div class="s_website_form_rows row s_col_no_bgcolor">
<!-- Main Heading for Name Group -->
<!-- <div class="col-12">-->
<!-- <h3 class="section-heading">Name</h3>-->
<!-- </div>-->
<!-- First Name, Middle Name, Last Name in a row -->
<div class="col-12 col-sm-4 mb-0 py-2 s_website_form_field s_website_form_required s_website_form_model_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-12 s_website_form_label" for="recruitment1">
<span class="s_website_form_label_content">First Name</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-12">
<input id="recruitment1" type="text"
class="form-control s_website_form_input"
name="first_name" required=""
data-fill-with="first_name"
placeholder="e.g. Pranay"/>
</div>
</div>
</div>
<div class="col-12 col-sm-4 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-12 s_website_form_label" for="recruitment2">
<span class="s_website_form_label_content">Middle Name</span>
</label>
<div class="col-12">
<input id="recruitment2" type="text"
class="form-control s_website_form_input"
name="middle_name"
data-fill-with="middle_name"
placeholder="e.g. Kumar"/>
</div>
</div>
</div>
<div class="col-12 col-sm-4 mb-0 py-2 s_website_form_field s_website_form_required s_website_form_model_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-12 s_website_form_label" for="recruitment3">
<span class="s_website_form_label_content">Last Name</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-12">
<input id="recruitment3" type="text"
class="form-control s_website_form_input"
name="last_name" required=""
data-fill-with="last_name"
placeholder="e.g. Gadi (SURNAME)"/>
</div>
</div>
</div>
</div>
<!-- Contact Section (Email, Phone, Alternate Phone) -->
<div>
<div class="row s_col_no_resize s_col_no_bgcolor">
<!-- Main Heading for Contact Group -->
<!-- <div class="col-12">-->
<!-- <h3 class="section-heading">Contact</h3>-->
<!-- </div>-->
<!-- Email Field -->
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="email" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment2">
<span class="s_website_form_label_content">Email</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-sm">
<input id="recruitment2" type="email"
class="form-control s_website_form_input"
name="email_from" required=""
placeholder="e.g. abc@gmail.com"
data-fill-with="email_from"/>
</div>
</div>
</div>
<!-- Phone Number Field -->
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentphone">
<span class="s_website_form_label_content">Phone Number</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-sm">
<input id="recruitmentphone" type="tel"
class="form-control s_website_form_input"
name="partner_phone" required=""
placeholder="+91 1112223334"
data-fill-with="partner_phone"/>
<div class="alert alert-warning mt-2 d-none" id="phone1-warning"></div>
</div>
</div>
</div>
<!-- Alternate Phone Field -->
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentphone2">
<span class="s_website_form_label_content">Alternate Number</span>
</label>
<div class="col-sm">
<input id="recruitmentphone2" type="tel"
class="form-control s_website_form_input"
name="alternate_phone"
placeholder="+91 1112223334"
data-fill-with="alternate_phone"/>
<div class="alert alert-warning mt-2 d-none" id="phone2-warning"></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Degree</span>
</label>
<div class="col-sm">
<select id="fetch_hr_recruitment_degree" class="form-control s_website_form_input"
name="degree">
</select>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Experience Type</span>
</label>
<div class="col-sm">
<div class="o-row">
<select class="form-control s_website_form_input"
name="exp_type" required="">
<option value="fresher" selected="">Fresher</option>
<option value="experienced">Experienced</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="current_organization_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Current Organization</span>
</label>
<div class="col-sm">
<input type="text"
class="form-control s_website_form_input"
name="current_organization"
/>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="current_ctc_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentctc">
<span class="s_website_form_label_content">Current CTC (LPA)</span>
</label>
<div class="col-sm">
<input id="recruitmentctc" type="number"
class="form-control s_website_form_input"
name="current_ctc"
data-fill-with="ctc"/>
<div class="alert alert-warning mt-2 d-none" id="ctcwarning-message"></div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentctc2">
<span class="s_website_form_label_content">Expected CTC (LPA)</span>
</label>
<div class="col-sm">
<input id="recruitmentctc2" type="number"
class="form-control s_website_form_input"
name="expected_ctc"
/>
<div class="alert alert-warning mt-2 d-none" id="ctc2warning-message"></div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Current Location</span>
</label>
<div class="col-sm">
<input type="text"
class="form-control s_website_form_input"
name="current_location"
/>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="preferred_location_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<t t-set="job_id" t-value="job.id"/>
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="preferred_locations">
<span class="s_website_form_label_content">Preferred Locations</span>
</label>
<div class="col-sm">
<div id="preferred_locations_container" t-att-data-job_id="job.locations">
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="notice_period_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Notice Period</span>
</label>
<div class="col-sm">
<div class="o-row">
<input type="number"
class="form-control s_website_form_input"
name="notice_period"
/>
<select class="form-control s_website_form_input"
name="notice_period_type">
<option value="day">Day's</option>
<option value="month">Month's</option>
<option value="year">Year's</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment4">
<span class="s_website_form_label_content">LinkedIn Profile</span>
</label>
<div class="col-sm">
<i class="fa fa-linkedin fa-2x o_linkedin_icon"></i>
<input id="recruitment4" type="text"
class="form-control s_website_form_input pl64"
placeholder="e.g. https://www.linkedin.com/in/fpodoo"
style="padding-inline-start: calc(40px + 0.375rem)"
name="linkedin_profile"
data-fill-with="linkedin_profile"/>
<div class="alert alert-warning mt-2 d-none" id="linkedin-message"></div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_custom"
data-type="binary" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment6">
<span class="s_website_form_label_content">Resume</span>
</label>
<div class="col-sm">
<input id="recruitment6" type="file"
class="form-control s_website_form_input o_resume_input"
name="Resume"/>
<span class="text-muted small">Provide either a resume file or a linkedin profile</span>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="text" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment5">
<span class="s_website_form_label_content">Short Introduction</span>
</label>
<div class="col-sm">
<textarea id="recruitment5"
class="form-control s_website_form_input"
placeholder="Optional introduction, or any question you might have about the job…"
name="applicant_notes" rows="5"></textarea>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_dnone"
data-type="record" data-model="hr.job">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment7">
<span class="s_website_form_label_content">Job</span>
</label>
<div class="col-sm">
<input id="recruitment7" type="hidden"
class="form-control s_website_form_input"
name="job_id"/>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_dnone"
data-type="record" data-model="hr.department">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment8">
<span class="s_website_form_label_content">Department</span>
</label>
<div class="col-sm">
<input id="recruitment8" type="hidden"
class="form-control s_website_form_input"
name="department_id"/>
</div>
</div>
</div>
<div class="col-12 s_website_form_submit mb64" data-name="Submit Button">
<div class="alert alert-warning mt-2 d-none" id="warning-message"></div>
<div style="width: 200px" class="s_website_form_label"/>
<a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send" id="apply-btn">I'm
feeling lucky
</a>
<span id="s_website_form_result"></span>
</div>
</div>
</form>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1 @@
from . import post_onboarding_attachment_wizard

View File

@ -0,0 +1,58 @@
from odoo import models, fields, api
class PostOnboardingAttachmentWizard(models.TransientModel):
_name = 'post.onboarding.attachment.wizard'
_description = 'Post Onboarding Attachment Wizard'
attachment_ids = fields.Many2many(
'recruitment.attachments',
string='Attachments to Request'
)
@api.model
def default_get(self, fields_list):
"""Pre-fill attachments with is_default=True"""
defaults = super(PostOnboardingAttachmentWizard, self).default_get(fields_list)
default_attachments = self.env['recruitment.attachments'].search([('is_default', '=', True)])
if default_attachments:
defaults['attachment_ids'] = [(6, 0, default_attachments.ids)]
return defaults
def action_confirm(self):
self.ensure_one()
context = self.env.context
active_id = context.get('active_id')
applicant = self.env['hr.applicant'].browse(active_id)
applicant.recruitment_attachments = [(4, attachment.id) for attachment in self.attachment_ids]
template = self.env.ref('hr_recruitment_extended.email_template_post_onboarding_form', raise_if_not_found=False)
personal_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'personal').mapped('name')
education_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'education').mapped('name')
previous_employer_docs = self.attachment_ids.filtered(
lambda a: a.attachment_type == 'previous_employer').mapped('name')
other_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
# Prepare context for the template
email_context = {
'personal_docs': personal_docs,
'education_docs': education_docs,
'previous_employer_docs': previous_employer_docs,
'other_docs': other_docs,
}
email_values = {
'email_to': applicant.email_from,
'auto_delete': True,
}
template.with_context(**email_context).send_mail(
applicant.id, force_send=True, email_values=email_values
)
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_post_onboarding_attachment_wizard_form" model="ir.ui.view">
<field name="name">post.onboarding.attachment.wizard.form</field>
<field name="model">post.onboarding.attachment.wizard</field>
<field name="arch" type="xml">
<form string="Select Attachments">
<group>
<field name="attachment_ids" widget="many2many_tags"/>
</group>
<footer>
<button name="action_confirm" type="object" string="Send Email" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,69 @@
Open Source Job Board
---------------------
### Organize your job postings and applications
Organize your job board, promote your job announces and keep track of application submissions easily. Follow every applicant and build up a database of skills and profiles.
Create Awsome Job Description Pages
-----------------------------------
### Get rid of old WYSIWYG editors
Get a clean and professional look for your job annouces. Odoo's unique *'edit inline'* approach makes website and job descriptions creation surprisingly easy.
Drag & Drop well designed *'Building Blocks'* to create beautifull job descriptions and announces that emphasizes the quality of your company.
Post Your Jobs on Best Job Boards
---------------------------------
### LinkedIn, Monster, Craigslist, Careerbuilder,...
Connect automatically to most famous job board websites; linkedIn, Monster, Craigslist, ... Every job position has a new email address automatically assigned to route applications automatically to the right job position.
Whether applicants contact you by email or using an online form, you get all the data indexed automatically (resumes, motivation letter) and you can answer in just a click, reusing templates of answers.
Customize Your Recruitment Process
----------------------------------
### Define your own stages and interviewers
Use the kanban view and customize the steps of your recruitments process; pre-qualification, first interview, second interview, negociaiton, ...
Get accurate statistics on your recruitment pipeline. Get reports to compare the performance of your different investments on external job boards.
Streamline Your Recruitment Process
-----------------------------------
### Index resumes, track applicants, search profiles
Follow applicants in your recruitment process with the smart kanban view. Save time by automating some communications with email templates.
Documents like resumes and motivation letters are indexed automatically, allowing you to easily find for specific skills and build up a database of profiles.
Integrated Surveys
------------------
### Define your own online or offline surveys
Create your own interview canvas based on our best practices. Use the survey designer to adapt questions to your own process. Ask the applicant to fill in the survey online, or the interviewer to use it during real interviews.
Fully Integrated With Others Apps
---------------------------------
### Get hundreds of open source apps for free
### CMS
Easily create awesome websites with no technical knowledge required.
### Blog
Write news, attract new visitors, build customer loyalty.
### Online Events
Schedule, organize, promote or sell events online; conferences, webinars, trainings, etc.

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Online Jobs Extended',
'category': 'Website/Website',
'sequence': 310,
'version': '1.1',
'summary': 'Manage your online hiring process',
'description': "This module allows to publish your available job positions on your website and keep track of application submissions easily. It comes as an add-on of *Recruitment* app.",
'depends': ['hr_recruitment_extended','website_hr_recruitment', 'website_mail'],
'data': [
'security/ir.model.access.csv',
'security/website_hr_recruitment_security.xml',
'data/config_data.xml',
'views/website_hr_recruitment_templates.xml',
'views/hr_recruitment_views.xml',
'views/hr_job_views.xml',
'views/website_pages_views.xml',
'views/snippets.xml',
],
'installable': True,
'application': True,
'auto_install': ['hr_recruitment_extended', 'website_mail'],
'assets': {
'web.assets_frontend': [
'web/static/lib/jquery/jquery.js', # Ensure jQuery is loaded first
# Load Select2 CSS and JS
# 'website_hr_recruitment_extended/static/src/lib/select2/select2.min.css',
# 'website_hr_recruitment_extended/static/src/lib/select2/select2.min.js',
'website_hr_recruitment_extended/static/src/lib/select2/selecttwo.css',
# Load Custom JS to Initialize Select2
'website_hr_recruitment_extended/static/src/js/select2_init.js',
# Your existing scripts
'website_hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
],
},
# 'assets': {
# 'web.assets_frontend': [
# 'website_hr_recruitment/static/src/scss/**/*',
# 'website_hr_recruitment/static/src/js/website_hr_applicant_form.js',
# ],
# 'web.assets_backend': [
# 'website_hr_recruitment/static/src/js/widgets/copy_link_menuitem.js',
# 'website_hr_recruitment/static/src/js/widgets/copy_link_menuitem.xml',
# 'website_hr_recruitment/static/src/fields/**/*',
# ],
# 'website.assets_wysiwyg': [
# 'website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js',
# ],
# 'website.assets_editor': [
# 'website_hr_recruitment/static/src/js/systray_items/new_content.js',
# ],
# },
'license': 'LGPL-3',
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -0,0 +1,502 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import warnings
from datetime import datetime
from dateutil.relativedelta import relativedelta
from operator import itemgetter
from werkzeug.urls import url_encode
from odoo import http, _
from odoo.addons.website_hr_recruitment.controllers.main import WebsiteHrRecruitment
from odoo.osv.expression import AND
from odoo.http import request
from odoo.tools import email_normalize
from odoo.tools.misc import groupby
import ast
import base64
class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
_jobs_per_page = 12
def sitemap_jobs(env, rule, qs):
if not qs or qs.lower() in '/jobs':
yield {'loc': '/jobs'}
@http.route([
'/jobs',
'/jobs/page/<int:page>',
], type='http', auth="public", website=True, sitemap=sitemap_jobs)
def jobs(self, country_id=None, department_id=None, office_id=None, contract_type_id=None,
is_remote=False, is_other_department=False, is_untyped=None, page=1, search=None, **kwargs):
env = request.env(context=dict(request.env.context, show_address=True, no_tag_br=True))
Country = env['res.country']
Jobs = env['hr.job.recruitment']
Department = env['hr.department']
country = Country.browse(int(country_id)) if country_id else None
department = Department.browse(int(department_id)) if department_id else None
office_id = int(office_id) if office_id else None
contract_type_id = int(contract_type_id) if contract_type_id else None
# Default search by user country
if not (country or department or office_id or contract_type_id or kwargs.get('all_countries')):
if request.geoip.country_code:
countries_ = Country.search([('code', '=', request.geoip.country_code)])
country = countries_[0] if countries_ else None
if country:
country_count = Jobs.search_count(AND([
request.website.website_domain(),
[('address_id.country_id', '=', country.id)]
]))
if not country_count:
country = False
options = {
'displayDescription': True,
'allowFuzzy': not request.params.get('noFuzzy'),
'country_id': country.id if country else None,
'department_id': department.id if department else None,
'office_id': office_id,
'contract_type_id': contract_type_id,
'is_remote': is_remote,
'is_other_department': is_other_department,
'is_untyped': is_untyped,
}
total, details, fuzzy_search_term = request.website._search_with_fuzzy("job_requests", search,
limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=options)
# Browse jobs as superuser, because address is restricted
jobs = details[0].get('results', Jobs).sudo()
def sort(records_list, field_name):
""" Sort records in the given collection according to the given
field name, alphabetically. None values instead of records are
placed at the end.
:param list records_list: collection of records or None values
:param str field_name: field on which to sort
:return: sorted list
"""
return sorted(
records_list,
key=lambda item: (item is None, item.sudo()[field_name] if item and item.sudo()[field_name] else ''),
)
# Countries
if country or is_remote:
cross_country_options = options.copy()
cross_country_options.update({
'allowFuzzy': False,
'country_id': None,
'is_remote': False,
})
cross_country_total, cross_country_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_country_options)
# Browse jobs as superuser, because address is restricted
cross_country_jobs = cross_country_details[1].get('results', Jobs).sudo()
else:
cross_country_total = total
cross_country_jobs = jobs
country_offices = set(j.address_id or None for j in cross_country_jobs)
countries = sort(set(o and o.country_id or None for o in country_offices), 'name')
count_per_country = {'all': cross_country_total}
for c, jobs_list in groupby(cross_country_jobs, lambda job: job.address_id.country_id):
count_per_country[c] = len(jobs_list)
count_remote = len(cross_country_jobs.filtered(lambda job: not job.address_id))
if count_remote:
count_per_country[None] = count_remote
# Departments
if department or is_other_department:
cross_department_options = options.copy()
cross_department_options.update({
'allowFuzzy': False,
'department_id': None,
'is_other_department': False,
})
cross_department_total, cross_department_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_department_options)
cross_department_jobs = cross_department_details[1].get('results', Jobs)
else:
cross_department_total = total
cross_department_jobs = jobs
departments = sort(set(j.department_id or None for j in cross_department_jobs), 'name')
count_per_department = {'all': cross_department_total}
for d, jobs_list in groupby(cross_department_jobs, lambda job: job.department_id):
count_per_department[d] = len(jobs_list)
count_other_department = len(cross_department_jobs.filtered(lambda job: not job.department_id))
if count_other_department:
count_per_department[None] = count_other_department
# Offices
if office_id or is_remote:
cross_office_options = options.copy()
cross_office_options.update({
'allowFuzzy': False,
'office_id': None,
'is_remote': False,
})
cross_office_total, cross_office_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_office_options)
# Browse jobs as superuser, because address is restricted
cross_office_jobs = cross_office_details[1].get('results', Jobs).sudo()
else:
cross_office_total = total
cross_office_jobs = jobs
offices = sort(set(j.address_id or None for j in cross_office_jobs), 'city')
count_per_office = {'all': cross_office_total}
for o, jobs_list in groupby(cross_office_jobs, lambda job: job.address_id):
count_per_office[o] = len(jobs_list)
count_remote = len(cross_office_jobs.filtered(lambda job: not job.address_id))
if count_remote:
count_per_office[None] = count_remote
# Employment types
if contract_type_id or is_untyped:
cross_type_options = options.copy()
cross_type_options.update({
'allowFuzzy': False,
'contract_type_id': None,
'is_untyped': False,
})
cross_type_total, cross_type_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_type_options)
cross_type_jobs = cross_type_details[1].get('results', Jobs)
else:
cross_type_total = total
cross_type_jobs = jobs
employment_types = sort(set(j.contract_type_id for j in jobs if j.contract_type_id), 'name')
count_per_employment_type = {'all': cross_type_total}
for t, jobs_list in groupby(cross_type_jobs, lambda job: job.contract_type_id):
count_per_employment_type[t] = len(jobs_list)
count_untyped = len(cross_type_jobs.filtered(lambda job: not job.contract_type_id))
if count_untyped:
count_per_employment_type[None] = count_untyped
pager = request.website.pager(
url=request.httprequest.path.partition('/page/')[0],
url_args=request.httprequest.args,
total=total,
page=page,
step=self._jobs_per_page,
)
offset = pager['offset']
jobs = jobs[offset:offset + self._jobs_per_page]
office = env['res.partner'].browse(int(office_id)) if office_id else None
contract_type = env['hr.contract.type'].browse(int(contract_type_id)) if contract_type_id else None
# Render page
return request.render("website_hr_recruitment_extended.recruitment_index", {
'jobs': jobs,
'countries': countries,
'departments': departments,
'offices': offices,
'employment_types': employment_types,
'country_id': country,
'department_id': department,
'office_id': office,
'contract_type_id': contract_type,
'is_remote': is_remote,
'is_other_department': is_other_department,
'is_untyped': is_untyped,
'pager': pager,
'search': fuzzy_search_term or search,
'search_count': total,
'original_search': fuzzy_search_term and search,
'count_per_country': count_per_country,
'count_per_department': count_per_department,
'count_per_office': count_per_office,
'count_per_employment_type': count_per_employment_type,
})
@http.route('/jobs/add', type='json', auth="user", website=True)
def jobs_add(self, **kwargs):
# avoid branding of website_description by setting rendering_bundle in context
job = request.env['hr.job.recruitment'].with_context(rendering_bundle=True).create({
'name': _('Job Title'),
})
return f"/jobs/{request.env['ir.http']._slug(job)}"
@http.route('''/jobs/detail/<model("hr.job.recruitment"):job>''', type='http', auth="public", website=True, sitemap=True)
def jobs_detail(self, job, **kwargs):
redirect_url = f"/jobs/{request.env['ir.http']._slug(job)}"
return request.redirect(redirect_url, code=301)
@http.route('''/jobs/<model("hr.job.recruitment"):job>''', type='http', auth="public", website=True, sitemap=True)
def job(self, job, **kwargs):
return request.render("website_hr_recruitment_extended.recruitment_detail", {
'job': job,
'main_object': job,
})
@http.route('''/jobs/apply/<model("hr.job.recruitment"):job>''', type='http', auth="public", website=True, sitemap=True)
def jobs_apply(self, job, **kwargs):
error = {}
default = {}
if 'website_hr_recruitment_error' in request.session:
error = request.session.pop('website_hr_recruitment_error')
default = request.session.pop('website_hr_recruitment_default')
return request.render("website_hr_recruitment_extended.recruitment_apply", {
'job': job,
'error': error,
'default': default,
})
# Compatibility routes
@http.route([
'/jobs/country/<model("res.country"):country>',
'/jobs/department/<model("hr.department"):department>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>',
'/jobs/office/<int:office_id>',
'/jobs/country/<model("res.country"):country>/office/<int:office_id>',
'/jobs/department/<model("hr.department"):department>/office/<int:office_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/office/<int:office_id>',
'/jobs/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/employment_type/<int:contract_type_id>',
'/jobs/department/<model("hr.department"):department>/employment_type/<int:contract_type_id>',
'/jobs/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/department/<model("hr.department"):department>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/office/<int:office_id>/employment_type/<int:contract_type_id>',
], type='http', auth="public", website=True, sitemap=False)
def jobs_compatibility(self, country=None, department=None, office_id=None, contract_type_id=None, **kwargs):
"""
Deprecated since Odoo 16.3: those routes are kept by compatibility.
They should not be used in Odoo code anymore.
"""
warnings.warn(
"This route is deprecated since Odoo 16.3: the jobs list is now available at /jobs or /jobs/page/XXX",
DeprecationWarning
)
url_params = {
'country_id': country and country.id,
'department_id': department and department.id,
'office_id': office_id,
'contract_type_id': contract_type_id,
**kwargs,
}
return request.redirect(
'/jobs?%s' % url_encode(url_params),
code=301,
)
@http.route('/hr_recruitment_extended/fetch_hr_recruitment_degree', type='json', auth="public", website=True)
def fetch_recruitment_degrees(self):
degrees = {}
all_degrees = http.request.env['hr.recruitment.degree'].sudo().search([])
if all_degrees:
for degree in all_degrees:
degrees[degree.id] = degree.name
return degrees
@http.route('/hr_recruitment_extended/fetch_preferred_locations', type='json', auth="public", website=True)
def fetch_preferred_locations(self, loc_ids):
locations = {}
for id in loc_ids:
location = http.request.env['hr.location'].sudo().browse(id)
if location:
locations[location.id] = location.location_name
return locations
@http.route('/hr_recruitment_extended/fetch_preferred_skills', type='json', auth="public", website=True)
def fetch_preferred_skills(self, skill_ids,fetch_others=False):
skills = {}
Skill = http.request.env['hr.skill'].sudo()
SkillLevel = http.request.env['hr.skill.level'].sudo()
if fetch_others:
for skill in Skill.search([('id','not in',skill_ids)]):
levels = SkillLevel.search([('skill_type_id', '=', skill.skill_type_id.id)],order='sequence')
skills[skill.id] = {
'id': skill.id,
'name': skill.name,
'levels': [{'id': lvl.id, 'name': lvl.name, 'percentage': lvl.level_progress, 'sequence': lvl.sequence} for lvl in levels]
}
else:
for skill in Skill.browse(skill_ids):
levels = SkillLevel.search([('skill_type_id', '=', skill.skill_type_id.id)],order='sequence')
skills[skill.id] = {
'id': skill.id,
'name': skill.name,
'levels': [{'id': lvl.id, 'name': lvl.name, 'percentage': lvl.level_progress, 'sequence': lvl.sequence} for lvl in levels]
}
return skills
@http.route('/website_hr_recruitment_extended/check_recent_application', type='json', auth="public", website=True)
def check_recent_application(self, value, job_id):
# Function to check if the applicant has an existing record based on email, phone, or linkedin
def refused_applicants_condition(applicant):
return not applicant.active \
and applicant.hr_job_recruitment.id == int(job_id) \
and applicant.create_date >= (datetime.now() - relativedelta(months=6))
# Search for applicants with the same email, phone, or linkedin (only if the value is not False/None)
applicants_with_similar_info = http.request.env['hr.applicant'].sudo().search([
('hr_job_recruitment','=',int(job_id)),
'|',
('email_normalized', '=', email_normalize(value)),
'|',
('partner_phone', '=', value),
('linkedin_profile', '=ilike', value),
], order='create_date DESC')
if not applicants_with_similar_info:
return {'message':None}
# Group applications by their status
applications_by_status = applicants_with_similar_info.grouped('application_status')
# Check for refused applicants with the same value within the last 6 months
refused_applicants = applications_by_status.get('refused', http.request.env['hr.applicant'])
if any(applicant for applicant in refused_applicants if refused_applicants_condition(applicant)):
return {
'message': _(
'We\'ve found a previous closed application in our system within the last 6 months.'
' Please consider before applying in order not to duplicate efforts.'
)
}
# Check for ongoing applications with the same value
ongoing_applications = applications_by_status.get('ongoing', [])
if ongoing_applications:
ongoing_application = ongoing_applications[0]
if ongoing_application.hr_job_recruitment.id == int(job_id):
recruiter_contact = "" if not ongoing_application.user_id else _(
' In case of issue, contact %(contact_infos)s',
contact_infos=", ".join(
[value for value in itemgetter('name', 'email', 'phone')(ongoing_application.user_id) if value]
))
error_message = 'An application already exists for %s Duplicates might be rejected. %s '%(value,recruiter_contact)
print(error_message)
return {
'message': _(error_message)
}
# If no existing application found, show the following message
return {
'message': _(
'We found a recent application with a similar name, email, phone number.'
' You can continue if it\'s not a mistake.'
)
}
def _should_log_authenticate_message(self, record):
if record._name == "hr.applicant" and not request.session.uid:
return False
return super()._should_log_authenticate_message(record)
def extract_data(self, model, values):
candidate = False
extracted_resume = values.pop('resume_base64', None)
current_ctc = values.pop('current_ctc', None)
expected_ctc = values.pop('expected_ctc', None)
available_joining_date = values.pop('available_joining_date', None)
exp_type = values.pop('exp_type', None)
current_location = values.pop('current_location', None)
preferred_locations_str = values.pop('preferred_locations', '')
department_id = values.pop('department_id',None)
hr_job_recruitment = values.pop('job_id', None)
preferred_locations = [int(x) for x in preferred_locations_str.split(',')] if len(
preferred_locations_str) > 0 else []
current_organization = values.pop('current_organization', None)
notice_period = values.pop('notice_period', 0)
notice_period_type = values.pop('notice_period_type', 'day')
experience_years = values.pop('experience_years',0)
experience_months = values.pop('experience_months',0)
# If there are months, convert everything to months
if int(experience_months) > 0:
total_experience = (int(experience_years) * 12) + int(experience_months)
total_experience_type = 'month'
else:
total_experience = int(experience_years)
total_experience_type = 'year'
skill_dict = {key: ast.literal_eval(value) for key,value in values.items() if "skill" in key and value != '0'}
if model.model == 'hr.applicant':
partner_name = values.pop('full_name', None)
partner_phone = values.pop('partner_phone', None)
alternate_phone = values.pop('alternate_phone', None)
partner_email = values.pop('email_from', None)
degree = values.pop('degree', None)
if partner_phone and partner_email:
candidate = request.env['hr.candidate'].sudo().search([
'|', ('email_from', '=', partner_email),
('partner_phone', '=', partner_phone),
], limit=1)
if candidate:
candidate.sudo().write({
'partner_name': partner_name,
'alternate_phone': alternate_phone,
'email_from': partner_email,
'partner_phone': partner_phone,
'type_id': int(degree) if degree.isdigit() else False,
'resume': extracted_resume
})
if not candidate:
candidate = request.env['hr.candidate'].sudo().create({
'partner_name': partner_name,
'email_from': partner_email,
'partner_phone': partner_phone,
'alternate_phone': alternate_phone,
'type_id': int(degree) if degree.isdigit() else False,
'resume': extracted_resume
})
if len(skill_dict) > 0:
# candidate_skills_list = []
for key, value in skill_dict.items():
candidate_skills = dict()
skill_type_id = request.env['hr.skill'].sudo().browse(int(key.split("_")[1])).skill_type_id.id
candidate_skills['candidate_id'] = candidate.id
candidate_skills['skill_id'] = int(key.split("_")[1])
candidate_skills['skill_level_id'] = value[0]
candidate_skills['level_progress'] = value[1]
candidate_skills['skill_type_id'] = skill_type_id
# skill = request.env['hr.candidate.skill'].sudo().create(candidate_skills)
# candidate_skills_list.append(skill.id)
candidate.write({'candidate_skill_ids':[(0,4,candidate_skills)]})
else:
skills = None
values['partner_name'] = partner_name
if partner_phone:
values['partner_phone'] = partner_phone
if partner_email:
values['email_from'] = partner_email
data = super().extract_data(model, values)
data['record']['current_ctc'] = float(current_ctc if current_ctc else 0)
data['record']['salary_expected'] = float(expected_ctc if expected_ctc else 0)
data['record']['exp_type'] = exp_type if exp_type else 'fresher'
data['record']['current_location'] = current_location if current_location else ''
data['record']['current_organization'] = current_organization if current_organization else ''
data['record']['notice_period'] = notice_period if notice_period else 0
data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day'
data['record']['hr_job_recruitment'] = int(hr_job_recruitment) if str(hr_job_recruitment).isdigit() else ''
data['record']['department_id'] = int(department_id) if str(department_id).isdigit() else ''
data['record']['availability'] = datetime.strptime(available_joining_date, '%Y-%m-%d').date() if available_joining_date else ''
data['record']['total_exp'] = total_experience if total_experience else 0
data['record']['total_exp_type'] = total_experience_type if total_experience_type else 'year'
# data['record']['resume'] = resume if resume else None
if len(preferred_locations_str) > 0:
data['record']['preferred_location'] = preferred_locations
if candidate:
data['record']['candidate_id'] = candidate.id
data['record']['type_id'] = candidate.type_id.id
return data

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Recruitment Form</field>
<field name="target">self</field>
<field name="url">/jobs</field>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_website"/>
<field name="state">open</field>
</record>
</data>
<data>
<record id="hr_recruitment.model_hr_applicant" model="ir.model">
<field name="website_form_key">apply_job</field>
<field name="website_form_access">True</field>
<field name="website_form_label">Apply for a Job</field>
</record>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>hr.applicant</value>
<value eval="[
'email_from',
'partner_name',
'partner_phone',
'job_id',
'department_id',
'linkedin_profile',
'applicant_properties',
]"/>
</function>
</data>
</odoo>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,799 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_hr_recruitment
#
# Translators:
# Martin Trigaux, 2022
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0beta\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-16 13:48+0000\n"
"PO-Revision-Date: 2022-09-22 05:56+0000\n"
"Last-Translator: Martin Trigaux, 2022\n"
"Language-Team: Afrikaans (https://www.transifex.com/odoo/teams/41243/af/)\n"
"Language: af\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "%s open positions"
msgstr ""
#. module: website_hr_recruitment
#. odoo-python
#: code:addons/website_hr_recruitment/models/hr_applicant.py:0
msgid "%s's Application"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "'. Showing results for '"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "+1 (650) 691-3277"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "12 days / year, including <br/>6 of your choice."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_kanban_referal_extends
msgid ""
"<i class=\"fa fa-fw fa-external-link\" role=\"img\"/>\n"
" Job Page"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_pages_kanban_view
msgid "<i class=\"fa fa-globe me-1\" title=\"Website\"/>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.detail
msgid "<i class=\"fa fa-long-arrow-left text-primary me-2\"/>All Jobs"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "<i class=\"fa fa-suitcase fa-fw\" title=\"Employment type\" role=\"img\" aria-label=\"Employment type\"/>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<i class=\"oi oi-arrow-left\"/> Job Description"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "<small><b>READ</b></small>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "<span class=\"fst-italic\">No address specified</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid ""
"<span class=\"h5 fw-light\">In the meantime,</span><br/>\n"
" <span class=\"h3 mt8 mb32 fw-bold\">Take a look around our website:</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "<span class=\"navbar-brand h5 my-0 me-sm-auto\">Our Job Offers</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Department</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Job</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">LinkedIn Profile</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Resume</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Short Introduction</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid ""
"<span class=\"s_website_form_label_content\">Your Email</span>\n"
" <span class=\"s_website_form_mark\"> *</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid ""
"<span class=\"s_website_form_label_content\">Your Name</span>\n"
" <span class=\"s_website_form_mark\"> *</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid ""
"<span class=\"s_website_form_label_content\">Your Phone Number</span>\n"
" <span class=\"s_website_form_mark\"> *</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_website_inherit
msgid "<span class=\"text-bg-success\">Published</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Department</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Employment Type</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Job</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Location</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted\" style=\"margin-left: 200px; font-size: 0.8rem\">The resume is optional if you have a Linkedin profile</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "A full-time position <br/>Attractive salary package."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "About Us"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "About us"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Achieve monthly sales objectives"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Additional languages"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Administrative Work"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_countries
msgid "All Countries"
msgstr "Alle Lande"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_departments
msgid "All Departments"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "All Offices"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_employment_type
msgid "All Types"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_hr_applicant
msgid "Applicant"
msgstr ""
#. module: website_hr_recruitment
#. odoo-javascript
#: code:addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js:0
msgid "Applied Job"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "Apply Job"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.detail
msgid "Apply Now!"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid ""
"As an employee of our company, you will <b>collaborate with each department\n"
" to create and deploy disruptive products.</b> Come work at a growing company\n"
" that offers great benefits with opportunities to moving forward and learn\n"
" alongside accomplished leaders. We're seeking an experienced and outstanding\n"
" member of staff.\n"
" <br/><br/>\n"
" This position is both <b>creative and rigorous</b> by nature you need to think\n"
" outside the box. We expect the candidate to be proactive and have a \"get it done\"\n"
" spirit. To be successful, you will have solid solving problem skills."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Autonomy"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Bachelor Degree or Higher"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__can_publish
msgid "Can Publish"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__job_details
msgid "Complementary information that will appear on the job submission page"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Congratulations!"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "Contact us"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Countries Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Create content that will help our users on a daily basis"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "Create new job pages from the <strong>+ <i>New</i></strong> top-right button."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Customer Relationship"
msgstr ""
#. module: website_hr_recruitment
#. odoo-javascript
#: code:addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js:0
#: model:ir.model,name:website_hr_recruitment.model_hr_department
msgid "Department"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Departments Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.jobs_searchbar_input_snippet_options
msgid "Description"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Discover our products."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid ""
"Each employee has a chance to see the impact of his work.\n"
" You can make a real contribution to the success of the company.\n"
" <br/>\n"
" Several activities are often organized all over the year, such as weekly\n"
" sports sessions, team building events, monthly drink, and much more"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Eat &amp; Drink"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Employment Types Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Expand your knowledge of various business industries"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Experience in writing online content"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "Follow us"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Fruit, coffee and <br/>snacks provided."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Google Adwords experience"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Great team of smart people, in a friendly and open culture"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Highly creative and autonomous"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Home"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid ""
"I usually <strong>answer applications within 3 days</strong>.\n"
" <br/><br/>\n"
" The next step is either a call or a meeting in our offices.\n"
" <br/><br/>\n"
" Feel free to <strong>contact me if you want a faster\n"
" feedback</strong> or if you don't get news from me\n"
" quickly enough."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "I'm feeling lucky"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "Insert a Job Description..."
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__is_published
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_form_website_published_button
msgid "Is Published"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "Job Application Form"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__description
msgid "Job Description"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.actions.act_window,name:website_hr_recruitment.action_job_pages_list
msgid "Job Pages"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_hr_job
msgid "Job Position"
msgstr "Werksposisie"
#. module: website_hr_recruitment
#. odoo-python
#: code:addons/website_hr_recruitment/controllers/main.py:0
msgid "Job Title"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "Job not found"
msgstr ""
#. module: website_hr_recruitment
#. odoo-python
#: code:addons/website_hr_recruitment/models/website.py:0
#: model:ir.ui.menu,name:website_hr_recruitment.menu_job_pages
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.jobs_searchbar_input_snippet_options
msgid "Jobs"
msgstr "Werke"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Jobs Page"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Lead the entire sales cycle"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Master demos of our software"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Must Have"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Negotiate and contract"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Nice to have"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "No address specified"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "No dumb managers, no stupid tools to use, no rigid working hours"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "No results found for '"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "No waste of time in enterprise processes, real responsibilities and autonomy"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_website_inherit
msgid "Not published"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Offices Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "Optional introduction, or any question you might have about the job…"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_departments
msgid "Others"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Our Product"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Passion for software products"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Perfect written English"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Perks"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Personal Evolution"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Play any sport with colleagues, <br/>the bill is covered."
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__job_details
msgid "Process Details"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Products"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_search_view_inherit
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_website_inherit
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_tree_inherit_website
msgid "Published"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__published_date
msgid "Published Date"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Qualify the customer needs"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Real responsibilities and challenges in a fast evolving company"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_countries
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "Remote"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Responsibilities"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__website_id
msgid "Restrict publishing to this website."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_pages_kanban_view
msgid "SEO Optimized"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__is_seo_optimized
msgid "SEO optimized"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Search Bar"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__seo_name
msgid "Seo name"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__website_published
msgid "Set if the application is published on the website of the company."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Sidebar"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_hr_recruitment_source
msgid "Source of Applicants"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Sport Activity"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Strong analytical skills"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Technical Expertise"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__website_url
msgid "The full URL to access the document through the website."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid ""
"There are currently no open job opportunities,<br class=\"mb-2\"/>\n"
" but feel free to <span class=\"fw-bold\">contact us</span> for a spontaneous application."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Trainings"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_employment_type
msgid "Unspecified type"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_recruitment_source__url
msgid "Url Parameters"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Valid work permit for Belgium"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_published
msgid "Visible on current website"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid ""
"We are a team of passionate people whose goal is to improve everyone's life through disruptive products.\n"
" We build great products to solve your business problems."
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_website
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_id
msgid "Website"
msgstr "Webtuiste"
#. module: website_hr_recruitment
#: model:ir.actions.act_url,name:website_hr_recruitment.action_open_website
msgid "Website Recruitment Form"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_url
msgid "Website URL"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_description
msgid "Website description"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_description
msgid "Website meta description"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_keywords
msgid "Website meta keywords"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_title
msgid "Website meta title"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_og_img
msgid "Website opengraph image"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "What We Offer"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "What's great in the job?"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_description
msgid ""
"You can customize this sample job description with a short overview of\n"
" the job position, defining specific terms and benefits. Keep it simple\n"
" an clear."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_form_inherit_website
msgid "You can write here a short description of your Job Description that will be displayed on the main Jobs' list page."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Your <b>contact</b> information is:"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid ""
"Your application has been posted successfully,<br class=\"mb-2\"/>\n"
" We usually respond within 3 days..."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "breadcrumb"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "e.g. https://www.linkedin.com/in/fpodoo/"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "info@yourcompany.example.com"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_recruitment_source_kanban_inherit_website
msgid "share it"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "unpublished"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,800 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_hr_recruitment
#
# Translators:
# Jumshud Sultanov <cumshud@gmail.com>, 2022
# erpgo translator <jumshud@erpgo.az>, 2022
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0beta\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-16 13:48+0000\n"
"PO-Revision-Date: 2022-09-22 05:56+0000\n"
"Last-Translator: erpgo translator <jumshud@erpgo.az>, 2022\n"
"Language-Team: Azerbaijani (https://app.transifex.com/odoo/teams/41243/az/)\n"
"Language: az\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "%s open positions"
msgstr ""
#. module: website_hr_recruitment
#. odoo-python
#: code:addons/website_hr_recruitment/models/hr_applicant.py:0
msgid "%s's Application"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "'. Showing results for '"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "+1 (650) 691-3277"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "12 days / year, including <br/>6 of your choice."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_kanban_referal_extends
msgid ""
"<i class=\"fa fa-fw fa-external-link\" role=\"img\"/>\n"
" Job Page"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_pages_kanban_view
msgid "<i class=\"fa fa-globe me-1\" title=\"Website\"/>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.detail
msgid "<i class=\"fa fa-long-arrow-left text-primary me-2\"/>All Jobs"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "<i class=\"fa fa-suitcase fa-fw\" title=\"Employment type\" role=\"img\" aria-label=\"Employment type\"/>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<i class=\"oi oi-arrow-left\"/> Job Description"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "<small><b>READ</b></small>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "<span class=\"fst-italic\">No address specified</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid ""
"<span class=\"h5 fw-light\">In the meantime,</span><br/>\n"
" <span class=\"h3 mt8 mb32 fw-bold\">Take a look around our website:</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "<span class=\"navbar-brand h5 my-0 me-sm-auto\">Our Job Offers</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Department</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Job</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">LinkedIn Profile</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Resume</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"s_website_form_label_content\">Short Introduction</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid ""
"<span class=\"s_website_form_label_content\">Your Email</span>\n"
" <span class=\"s_website_form_mark\"> *</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid ""
"<span class=\"s_website_form_label_content\">Your Name</span>\n"
" <span class=\"s_website_form_mark\"> *</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid ""
"<span class=\"s_website_form_label_content\">Your Phone Number</span>\n"
" <span class=\"s_website_form_mark\"> *</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_website_inherit
msgid "<span class=\"text-bg-success\">Published</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Department</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Employment Type</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Job</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted small\">Location</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "<span class=\"text-muted\" style=\"margin-left: 200px; font-size: 0.8rem\">The resume is optional if you have a Linkedin profile</span>"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "A full-time position <br/>Attractive salary package."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "About Us"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "About us"
msgstr "Haqqımızda"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Achieve monthly sales objectives"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Additional languages"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Administrative Work"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_countries
msgid "All Countries"
msgstr "Bütün Ölkələr"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_departments
msgid "All Departments"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "All Offices"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_employment_type
msgid "All Types"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_hr_applicant
msgid "Applicant"
msgstr "Namizəd"
#. module: website_hr_recruitment
#. odoo-javascript
#: code:addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js:0
msgid "Applied Job"
msgstr "Tətbiq olunan İş"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "Apply Job"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.detail
msgid "Apply Now!"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid ""
"As an employee of our company, you will <b>collaborate with each department\n"
" to create and deploy disruptive products.</b> Come work at a growing company\n"
" that offers great benefits with opportunities to moving forward and learn\n"
" alongside accomplished leaders. We're seeking an experienced and outstanding\n"
" member of staff.\n"
" <br/><br/>\n"
" This position is both <b>creative and rigorous</b> by nature you need to think\n"
" outside the box. We expect the candidate to be proactive and have a \"get it done\"\n"
" spirit. To be successful, you will have solid solving problem skills."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Autonomy"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Bachelor Degree or Higher"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__can_publish
msgid "Can Publish"
msgstr "Dərc Oluna Bilər"
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__job_details
msgid "Complementary information that will appear on the job submission page"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Congratulations!"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "Contact us"
msgstr "Bizimlə Əlaqə Saxlayın"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Countries Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Create content that will help our users on a daily basis"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "Create new job pages from the <strong>+ <i>New</i></strong> top-right button."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Customer Relationship"
msgstr ""
#. module: website_hr_recruitment
#. odoo-javascript
#: code:addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js:0
#: model:ir.model,name:website_hr_recruitment.model_hr_department
msgid "Department"
msgstr "Şöbə"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Departments Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.jobs_searchbar_input_snippet_options
msgid "Description"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Discover our products."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid ""
"Each employee has a chance to see the impact of his work.\n"
" You can make a real contribution to the success of the company.\n"
" <br/>\n"
" Several activities are often organized all over the year, such as weekly\n"
" sports sessions, team building events, monthly drink, and much more"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Eat &amp; Drink"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Employment Types Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Expand your knowledge of various business industries"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Experience in writing online content"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "Follow us"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Fruit, coffee and <br/>snacks provided."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Google Adwords experience"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Great team of smart people, in a friendly and open culture"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Highly creative and autonomous"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Home"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid ""
"I usually <strong>answer applications within 3 days</strong>.\n"
" <br/><br/>\n"
" The next step is either a call or a meeting in our offices.\n"
" <br/><br/>\n"
" Feel free to <strong>contact me if you want a faster\n"
" feedback</strong> or if you don't get news from me\n"
" quickly enough."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "I'm feeling lucky"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "Insert a Job Description..."
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__is_published
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_form_website_published_button
msgid "Is Published"
msgstr "Paylaşılıb"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "Job Application Form"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__description
msgid "Job Description"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.actions.act_window,name:website_hr_recruitment.action_job_pages_list
msgid "Job Pages"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_hr_job
msgid "Job Position"
msgstr "İş Mövqeyi"
#. module: website_hr_recruitment
#. odoo-python
#: code:addons/website_hr_recruitment/controllers/main.py:0
msgid "Job Title"
msgstr "Vəzifənin Adı"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "Job not found"
msgstr ""
#. module: website_hr_recruitment
#. odoo-python
#: code:addons/website_hr_recruitment/models/website.py:0
#: model:ir.ui.menu,name:website_hr_recruitment.menu_job_pages
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.jobs_searchbar_input_snippet_options
msgid "Jobs"
msgstr "İşlər"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Jobs Page"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Lead the entire sales cycle"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Master demos of our software"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Must Have"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Negotiate and contract"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Nice to have"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "No address specified"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "No dumb managers, no stupid tools to use, no rigid working hours"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "No results found for '"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "No waste of time in enterprise processes, real responsibilities and autonomy"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_website_inherit
msgid "Not published"
msgstr "Dərc edilməyib"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Offices Filter"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "Optional introduction, or any question you might have about the job…"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_departments
msgid "Others"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Our Product"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Passion for software products"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Perfect written English"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Perks"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Personal Evolution"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Play any sport with colleagues, <br/>the bill is covered."
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__job_details
msgid "Process Details"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Products"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_search_view_inherit
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_job_website_inherit
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_tree_inherit_website
msgid "Published"
msgstr "Dərc edilib"
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__published_date
msgid "Published Date"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Qualify the customer needs"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Real responsibilities and challenges in a fast evolving company"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_countries
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_offices
msgid "Remote"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Responsibilities"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__website_id
msgid "Restrict publishing to this website."
msgstr "Bu veb saytda dərc etməni məhdudlaşdırın."
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_pages_kanban_view
msgid "SEO Optimized"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__is_seo_optimized
msgid "SEO optimized"
msgstr "SEO optimallaşdırılıb"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Search Bar"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__seo_name
msgid "Seo name"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__website_published
msgid "Set if the application is published on the website of the company."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.snippet_options
msgid "Sidebar"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_hr_recruitment_source
msgid "Source of Applicants"
msgstr "Müraciət Edənlərin Mənbəyi"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Sport Activity"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Strong analytical skills"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Technical Expertise"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,help:website_hr_recruitment.field_hr_job__website_url
msgid "The full URL to access the document through the website."
msgstr "Veb sayt vasitəsilə sənədə daxil olmaq üçün tam URL."
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid ""
"There are currently no open job opportunities,<br class=\"mb-2\"/>\n"
" but feel free to <span class=\"fw-bold\">contact us</span> for a spontaneous application."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Trainings"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_filter_by_employment_type
msgid "Unspecified type"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_recruitment_source__url
msgid "Url Parameters"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "Valid work permit for Belgium"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_published
msgid "Visible on current website"
msgstr "Mövcud veb saytda görünür"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid ""
"We are a team of passionate people whose goal is to improve everyone's life through disruptive products.\n"
" We build great products to solve your business problems."
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model,name:website_hr_recruitment.model_website
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_id
msgid "Website"
msgstr "Veb sayt"
#. module: website_hr_recruitment
#: model:ir.actions.act_url,name:website_hr_recruitment.action_open_website
msgid "Website Recruitment Form"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_url
msgid "Website URL"
msgstr "Veb sayt URL-u"
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_description
msgid "Website description"
msgstr ""
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_description
msgid "Website meta description"
msgstr "Veb saytın meta təsviri"
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_keywords
msgid "Website meta keywords"
msgstr "Veb sayt meta açar sözləri"
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_title
msgid "Website meta title"
msgstr "Veb sayt meta başlığı"
#. module: website_hr_recruitment
#: model:ir.model.fields,field_description:website_hr_recruitment.field_hr_job__website_meta_og_img
msgid "Website opengraph image"
msgstr "Veb sayt opengraph ikonu"
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "What We Offer"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_website_description
msgid "What's great in the job?"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.default_description
msgid ""
"You can customize this sample job description with a short overview of\n"
" the job position, defining specific terms and benefits. Keep it simple\n"
" an clear."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.view_hr_job_form_inherit_website
msgid "You can write here a short description of your Job Description that will be displayed on the main Jobs' list page."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid "Your <b>contact</b> information is:"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.thankyou_ir_ui_view
msgid ""
"Your application has been posted successfully,<br class=\"mb-2\"/>\n"
" We usually respond within 3 days..."
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "breadcrumb"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.apply
msgid "e.g. https://www.linkedin.com/in/fpodoo/"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.job_right_side_bar
msgid "info@yourcompany.example.com"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.hr_recruitment_source_kanban_inherit_website
msgid "share it"
msgstr ""
#. module: website_hr_recruitment
#: model_terms:ir.ui.view,arch_db:website_hr_recruitment.index
msgid "unpublished"
msgstr "Dərc edilməyib"

Some files were not shown because too many files have changed in this diff Show More