Compare commits
18 Commits
8638551d38
...
a79a1a7b0e
| Author | SHA1 | Date |
|---|---|---|
|
|
a79a1a7b0e | |
|
|
2033d5c227 | |
|
|
cabb3c85a1 | |
|
|
a64fdf9a43 | |
|
|
a9650eb637 | |
|
|
dec0db69f2 | |
|
|
a421881fda | |
|
|
56179c5d18 | |
|
|
106171103a | |
|
|
9631ef27b3 | |
|
|
6667746ba8 | |
|
|
af9fe42e8a | |
|
|
1fa260bb26 | |
|
|
3dbf149bd5 | |
|
|
74f41caf00 | |
|
|
e8358b050b | |
|
|
4ab920555c | |
|
|
1fb8f3b552 |
|
|
@ -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 |
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import controllers
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import dashboard_menu
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
3
addons_extensions/ftp_custom_dashboards/static/src/lib/interact/interact.min.js
vendored
Normal file
3
addons_extensions/ftp_custom_dashboards/static/src/lib/interact/interact.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
addons_extensions/ftp_custom_dashboards/static/src/lib/select2/select2.main.js
vendored
Normal file
2
addons_extensions/ftp_custom_dashboards/static/src/lib/select2/select2.main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
25
addons_extensions/ftp_custom_dashboards/static/src/lib/select2/select2_init.js
vendored
Normal file
25
addons_extensions/ftp_custom_dashboards/static/src/lib/select2/select2_init.js
vendored
Normal 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
2
addons_extensions/ftp_custom_dashboards/static/src/lib/sortable/Sortable.min.js
vendored
Normal file
2
addons_extensions/ftp_custom_dashboards/static/src/lib/sortable/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from . import hr_attendance
|
||||
from . import hr_attendance
|
||||
from . import hr_attendance_report
|
||||
|
|
@ -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),
|
||||
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from . import models,wizards
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -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'
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="{"no_open": True, "no_create": 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>
|
||||
|
|
@ -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 **** -->
|
||||
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -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',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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&model=hr.job&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&model=hr.job.recruitment&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 & 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 & 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&model=hr.applicant&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 candidate’s 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&model=hr.applicant&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&model=hr.applicant&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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
@ -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. It’s 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. It’s the name people use to address you.")
|
||||
middle_name = fields.Char(string='Middle Name', help="This is an extra name that comes between the first name and last name. Not everyone has a middle name")
|
||||
last_name = fields.Char(string='Last Name',required=True, help="This is the family name, shared with other family members. It’s usually the last name.")
|
||||
last_name = fields.Char(string='Last Name',required=False, help="This is the family name, shared with other family members. It’s 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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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'
|
||||
|
|
@ -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')
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
|
@ -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;">
|
||||
×
|
||||
</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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="{"zoom": true, "preview_image":"candidate_image"}"/>
|
||||
|
||||
</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 & 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>
|
||||
|
|
@ -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 & 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 & 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="{"no_open": True, "no_create": 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="{"no_open": True, "no_create": 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 & 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 & 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 & 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 & 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>
|
||||
|
|
@ -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 > 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 > 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 > 0">
|
||||
<field name="application_count"/> Applications
|
||||
</div>
|
||||
<div t-if="record.no_of_hired_employee.raw_value > 0">
|
||||
<field name="no_of_hired_employee"/> Hired
|
||||
</div>
|
||||
|
||||
<div t-if="record.no_of_submissions.raw_value > 0">
|
||||
<field name="no_of_submissions"/>
|
||||
Submissions
|
||||
</div>
|
||||
|
||||
<div t-if="record.no_of_refused_submissions.raw_value > 0">
|
||||
<field name="no_of_refused_submissions"/>
|
||||
Refused Submissions
|
||||
</div>
|
||||
|
||||
<a t-if="record.activities_today.raw_value > 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 > 0 and record.activities_overdue.raw_value > 0"/>
|
||||
<a t-if="record.activities_overdue.raw_value > 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>
|
||||
|
|
@ -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 > 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 > 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 > 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 > 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 > 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 > 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="{"zoom": true, "preview_image":"candidate_image"}"/>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import post_onboarding_attachment_wizard
|
||||
|
|
@ -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'}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
|
@ -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 & 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
|
|
@ -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 & 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
Loading…
Reference in New Issue