srivyn_test #32
|
|
@ -107,7 +107,8 @@ RUN if [ -f requirements.txt ]; then \
|
||||||
pypdf \
|
pypdf \
|
||||||
phonenumbers \
|
phonenumbers \
|
||||||
python-docx \
|
python-docx \
|
||||||
pyzk
|
pyzk \
|
||||||
|
firebase-admin
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Create Required Directories
|
# Create Required Directories
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Base Changes',
|
||||||
|
'category': 'Base',
|
||||||
|
'version': '1.1',
|
||||||
|
'author': 'Seshi Kanth',
|
||||||
|
'summary': 'Changes of the form',
|
||||||
|
'description': 'This module contains form changes.',
|
||||||
|
'depends': [
|
||||||
|
'base',
|
||||||
|
'web',
|
||||||
|
],
|
||||||
|
|
||||||
|
'data': [
|
||||||
|
# 'views/user_menu.xml',
|
||||||
|
],
|
||||||
|
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'base_custom/static/src/css/backend.css',
|
||||||
|
'base_custom/static/src/js/user_menu_patch.js',
|
||||||
|
'base_custom/static/src/js/hide_user_menu.js',
|
||||||
|
'base_custom/static/src/xml/user_menu.xml',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import ir_http
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
_inherit = "ir.http"
|
||||||
|
|
||||||
|
def session_info(self):
|
||||||
|
session_info = super().session_info()
|
||||||
|
|
||||||
|
employee = self.env.user.employee_id
|
||||||
|
|
||||||
|
session_info.update({
|
||||||
|
"employee_name": employee.name or self.env.user.name,
|
||||||
|
"employee_designation": employee.job_id.name if employee.job_id else "",
|
||||||
|
})
|
||||||
|
|
||||||
|
return session_info
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
.o_switch_company_menu .oe_topbar_name{
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_topbar_name{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
line-height:1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_topbar_name .fw-bold{
|
||||||
|
font-size:13px;
|
||||||
|
font-weight:600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_topbar_name .designation{
|
||||||
|
font-size:11px;
|
||||||
|
color:#7b7b7b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
|
||||||
|
const userMenuRegistry = registry.category("user_menuitems");
|
||||||
|
|
||||||
|
const itemsToHide = [
|
||||||
|
"documentation",
|
||||||
|
"support",
|
||||||
|
"shortcuts",
|
||||||
|
"odoo_account",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of itemsToHide) {
|
||||||
|
if (userMenuRegistry.contains(item)) {
|
||||||
|
userMenuRegistry.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { UserMenu } from "@web/webclient/user_menu/user_menu";
|
||||||
|
import { session } from "@web/session";
|
||||||
|
|
||||||
|
patch(UserMenu.prototype, {
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
|
||||||
|
this.employeeName = session.employee_name;
|
||||||
|
this.employeeDesignation = session.employee_designation;
|
||||||
|
},
|
||||||
|
|
||||||
|
get employeeInfo() {
|
||||||
|
return {
|
||||||
|
employeeName: this.employeeName,
|
||||||
|
employeeDesignation: this.employeeDesignation,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-inherit="web.UserMenu"
|
||||||
|
t-inherit-mode="extension">
|
||||||
|
|
||||||
|
<xpath expr="//small[contains(@class,'oe_topbar_name')]"
|
||||||
|
position="replace">
|
||||||
|
|
||||||
|
<small class="oe_topbar_name d-none d-lg-inline-block ms-2 text-start"
|
||||||
|
style="max-width:220px">
|
||||||
|
|
||||||
|
<div class="fw-bold text-truncate">
|
||||||
|
<t t-esc="employeeInfo.employeeName"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted text-truncate designation">
|
||||||
|
<t t-esc="employeeInfo.employeeDesignation"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</small>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
|
from . import wizards
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
"name": "Onboarding",
|
||||||
|
"summary": "Employee-side onboarding bridge for JOD and onboarding links",
|
||||||
|
"description": """
|
||||||
|
Provides an employee-owned bridge record that can be used without
|
||||||
|
installing recruitment or ATS modules.
|
||||||
|
""",
|
||||||
|
"author": "FTPROTECH",
|
||||||
|
"website": "https://www.ftprotech.com",
|
||||||
|
"category": "Human Resources",
|
||||||
|
"version": "1.0",
|
||||||
|
"depends": [
|
||||||
|
"hr_employee_extended",
|
||||||
|
"mail",
|
||||||
|
"website",
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"data/ir_sequence_data.xml",
|
||||||
|
"data/onboarding_attachment_data.xml",
|
||||||
|
"data/mail_template.xml",
|
||||||
|
"data/template.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"wizards/employee_bridge_attachment_wizard.xml",
|
||||||
|
"views/recruitment_employee_bridge_views.xml",
|
||||||
|
"views/onboarding_attachment_views.xml",
|
||||||
|
"views/hr_employee_views.xml",
|
||||||
|
],
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"assets": {
|
||||||
|
"web.assets_frontend": [
|
||||||
|
"employee_bridge/static/src/js/post_onboarding_form.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"application": False,
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.addons.website.controllers.main import Website # <-- ADD THIS IMPORT
|
||||||
|
|
||||||
|
|
||||||
|
# CHANGE INHERITANCE: http.Controller -> Website
|
||||||
|
class EmployeeBridgeController(Website):
|
||||||
|
|
||||||
|
def _get_bridge(self, token):
|
||||||
|
return request.env["recruitment.employee.bridge"].sudo().search([
|
||||||
|
("access_token", "=", token),
|
||||||
|
("active", "=", True),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
def _render_form(self, bridge):
|
||||||
|
# Removed the 'message' variable as we will use a separate template for success
|
||||||
|
return request.render("employee_bridge.employee_bridge_jod_form_template", {
|
||||||
|
"bridge": bridge,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route("/employee_bridge/jod/<string:token>", type="http", auth="public", csrf=False, website=True)
|
||||||
|
def employee_bridge_jod(self, token, **post):
|
||||||
|
bridge = self._get_bridge(token)
|
||||||
|
if not bridge:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
if request.httprequest.method == "POST":
|
||||||
|
bridge.write_from_public_form(post)
|
||||||
|
|
||||||
|
# RENDER A SEPARATE THANK YOU TEMPLATE INSTEAD OF THE FORM
|
||||||
|
return request.render("employee_bridge.employee_bridge_thank_you_template", {
|
||||||
|
"bridge": bridge,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self._render_form(bridge)
|
||||||
|
|
||||||
|
@http.route("/employee_bridge/fetch_related_state_ids", type="json", auth="public", website=True)
|
||||||
|
def fetch_related_state_ids(self, country_id=None):
|
||||||
|
states = request.env["res.country.state"].sudo()
|
||||||
|
return {
|
||||||
|
state.id: state.name
|
||||||
|
for state in states.search([("country_id", "=?", country_id)])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="seq_recruitment_employee_bridge" model="ir.sequence">
|
||||||
|
<field name="name">Onboarding</field>
|
||||||
|
<field name="code">recruitment.employee.bridge</field>
|
||||||
|
<field name="prefix">EB/</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="email_template_jod_form" model="mail.template">
|
||||||
|
<field name="name">JOD Form Email</field>
|
||||||
|
<field name="model_id" ref="employee_bridge.model_recruitment_employee_bridge"/>
|
||||||
|
<field name="email_from">{{ user.email_formatted or object.company_id.email or '' }}</field>
|
||||||
|
<field name="email_to">{{ object.work_email or object.private_email or object.employee_id.work_email or '' }}</field>
|
||||||
|
<field name="subject">Joining Onboarding Form - {{ object.employee_name or object.employee_id.name or '' }}</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin:0; padding:0; font-size:13px; line-height:1.7;">
|
||||||
|
<p>Dear <t t-esc="object.employee_name or object.employee_id.name or 'Candidate'"/>,</p>
|
||||||
|
<p>Welcome to <t t-esc="object.employee_id.company_id.name"/>.</p>
|
||||||
|
<p>
|
||||||
|
Please submit your employee joining details using the link below.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a t-att-href="ctx.get('joining_form_link')" target="_blank"
|
||||||
|
style="background-color:#005580; color:#ffffff; padding:10px 18px; text-decoration:none; border-radius:4px; display:inline-block;">
|
||||||
|
Open Employee Joining Form
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
|
||||||
|
<p><strong>Attachments to Request</strong></p>
|
||||||
|
<t t-if="ctx.get('personal_docs')">
|
||||||
|
<p><strong>Personal Documents:</strong> <t t-esc="', '.join(ctx.get('personal_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="ctx.get('education_docs')">
|
||||||
|
<p><strong>Education Documents:</strong> <t t-esc="', '.join(ctx.get('education_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="ctx.get('previous_employer_docs')">
|
||||||
|
<p><strong>Previous Employer Documents:</strong> <t t-esc="', '.join(ctx.get('previous_employer_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="ctx.get('other_docs')">
|
||||||
|
<p><strong>Other Documents:</strong> <t t-esc="', '.join(ctx.get('other_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<p>Regards,<br/>HR Team</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="onboarding_attachment_aadhaar" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Aadhaar Card</field>
|
||||||
|
<field name="attachment_type">personal</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_pan" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">PAN Card</field>
|
||||||
|
<field name="attachment_type">personal</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_photo" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Passport Size Photo</field>
|
||||||
|
<field name="attachment_type">personal</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_education" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Education Certificates</field>
|
||||||
|
<field name="attachment_type">education</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_experience" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Experience / Relieving Letter</field>
|
||||||
|
<field name="attachment_type">previous_employer</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_payslip" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Last Three Months Payslips</field>
|
||||||
|
<field name="attachment_type">previous_employer</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_bank" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Cancelled Cheque / Bank Proof</field>
|
||||||
|
<field name="attachment_type">others</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import recruitment_employee_bridge
|
||||||
|
from . import hr_employee
|
||||||
|
from . import onboarding_documents
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
from odoo import _, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class HrEmployee(models.Model):
|
||||||
|
_inherit = "hr.employee"
|
||||||
|
|
||||||
|
recruitment_bridge_ids = fields.One2many(
|
||||||
|
"recruitment.employee.bridge",
|
||||||
|
"employee_id",
|
||||||
|
string="Onboarding Records",
|
||||||
|
)
|
||||||
|
recruitment_bridge_count = fields.Integer(compute="_compute_recruitment_bridge_count")
|
||||||
|
|
||||||
|
def _compute_recruitment_bridge_count(self):
|
||||||
|
data = self.env["recruitment.employee.bridge"].read_group(
|
||||||
|
[("employee_id", "in", self.ids), ("active", "=", True)],
|
||||||
|
["employee_id"],
|
||||||
|
["employee_id"],
|
||||||
|
)
|
||||||
|
counts = {item["employee_id"][0]: item["employee_id_count"] for item in data}
|
||||||
|
for employee in self:
|
||||||
|
employee.recruitment_bridge_count = counts.get(employee.id, 0)
|
||||||
|
|
||||||
|
def _get_or_create_employee_bridge(self):
|
||||||
|
self.ensure_one()
|
||||||
|
bridge = self.env["recruitment.employee.bridge"].search(
|
||||||
|
[("employee_id", "=", self.id), ("active", "=", True)],
|
||||||
|
limit=1,
|
||||||
|
order="id desc",
|
||||||
|
)
|
||||||
|
if not bridge:
|
||||||
|
bridge = self.env["recruitment.employee.bridge"].create({
|
||||||
|
"employee_id": self.id,
|
||||||
|
"company_id": self.company_id.id or self.env.company.id,
|
||||||
|
"source": "jod",
|
||||||
|
"state": "draft",
|
||||||
|
})
|
||||||
|
return bridge
|
||||||
|
|
||||||
|
def send_jod_form_to_employee(self):
|
||||||
|
self.ensure_one()
|
||||||
|
bridge = self._get_or_create_employee_bridge()
|
||||||
|
bridge.action_send_jod_form()
|
||||||
|
return {
|
||||||
|
"name": _("Onboarding"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "recruitment.employee.bridge",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": bridge.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_recruitment_bridge(self):
|
||||||
|
self.ensure_one()
|
||||||
|
bridge = self._get_or_create_employee_bridge()
|
||||||
|
if not bridge:
|
||||||
|
raise ValidationError(_("No onboarding record found."))
|
||||||
|
return {
|
||||||
|
"name": _("Onboarding"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "recruitment.employee.bridge",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": bridge.id,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBridgeRequestedAttachment(models.Model):
|
||||||
|
_name = "employee.bridge.requested.attachment"
|
||||||
|
_description = "Onboarding Requested Attachment"
|
||||||
|
|
||||||
|
name = fields.Char(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")
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBridgeAttachment(models.Model):
|
||||||
|
_name = "employee.bridge.attachment"
|
||||||
|
_description = "Onboarding Attachment"
|
||||||
|
_rec_name = "name"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, ondelete="cascade")
|
||||||
|
employee_id = fields.Many2one("hr.employee")
|
||||||
|
requested_attachment_id = fields.Many2one("employee.bridge.requested.attachment")
|
||||||
|
attachment_type = fields.Selection(related="requested_attachment_id.attachment_type")
|
||||||
|
file = fields.Binary(required=True)
|
||||||
|
file_name = fields.Char()
|
||||||
|
review_status = fields.Selection([
|
||||||
|
("draft", "Under Review"),
|
||||||
|
("pass", "PASS"),
|
||||||
|
("fail", "FAIL"),
|
||||||
|
], default="draft")
|
||||||
|
review_comments = fields.Char()
|
||||||
|
|
||||||
|
def action_preview_file(self):
|
||||||
|
self.ensure_one()
|
||||||
|
attachment = self.env["ir.attachment"].sudo().create({
|
||||||
|
"name": self.file_name or self.name,
|
||||||
|
"datas": self.file,
|
||||||
|
"res_model": self._name,
|
||||||
|
"res_id": self.id,
|
||||||
|
"type": "binary",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"name": "File Preview",
|
||||||
|
"type": "ir.actions.act_url",
|
||||||
|
"url": f"/web/content/{attachment.id}?download=false",
|
||||||
|
"target": "current",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyDetails(models.Model):
|
||||||
|
_inherit = "family.details"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||||
|
|
||||||
|
|
||||||
|
class EducationHistory(models.Model):
|
||||||
|
_inherit = "education.history"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||||
|
|
||||||
|
|
||||||
|
class EmployerHistory(models.Model):
|
||||||
|
_inherit = "employer.history"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||||
|
|
@ -0,0 +1,559 @@
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class RecruitmentEmployeeBridge(models.Model):
|
||||||
|
_name = "recruitment.employee.bridge"
|
||||||
|
_description = "Onboarding"
|
||||||
|
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||||
|
_rec_name = "name"
|
||||||
|
_order = "id desc"
|
||||||
|
|
||||||
|
@api.depends('name', 'employee_id', 'employee_name')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for record in self:
|
||||||
|
employee_name = record.employee_name or record.employee_id.name or ''
|
||||||
|
|
||||||
|
if employee_name:
|
||||||
|
record.display_name = f"{record.name} - {employee_name}"
|
||||||
|
else:
|
||||||
|
record.display_name = record.name
|
||||||
|
|
||||||
|
name = fields.Char(default="/", copy=False, readonly=True, tracking=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
"hr.employee",
|
||||||
|
string="Employee",
|
||||||
|
ondelete="restrict",
|
||||||
|
tracking=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company",
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
source = fields.Selection(
|
||||||
|
[
|
||||||
|
("jod", "JOD"),
|
||||||
|
("manual", "Manual"),
|
||||||
|
("recruitment", "Recruitment"),
|
||||||
|
],
|
||||||
|
default="jod",
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
state = fields.Selection(
|
||||||
|
[
|
||||||
|
("draft", "Draft"),
|
||||||
|
("jod_sent", "JOD Sent"),
|
||||||
|
("jod_received", "JOD Received"),
|
||||||
|
("validated", "Validated"),
|
||||||
|
],
|
||||||
|
default="draft",
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
employee_name = fields.Char(string="Employee Name", tracking=True)
|
||||||
|
employee_code = fields.Char(string="Employee Code", tracking=True)
|
||||||
|
candidate_image = fields.Image(string="Photo")
|
||||||
|
work_email = fields.Char(string="Work Email", tracking=True)
|
||||||
|
private_email = fields.Char(string="Private Email")
|
||||||
|
alternate_phone = fields.Char(string="Alternate Mobile")
|
||||||
|
work_phone = fields.Char(string="Work Phone")
|
||||||
|
mobile_phone = fields.Char(string="Mobile Phone")
|
||||||
|
department_id = fields.Many2one("hr.department", string="Department")
|
||||||
|
job_id = fields.Many2one("hr.job", string="Job Position")
|
||||||
|
emp_type = fields.Many2one("hr.contract.type", string="Employment Type")
|
||||||
|
doj = fields.Date(string="Date of Joining")
|
||||||
|
gender = fields.Selection([
|
||||||
|
("male", "Male"),
|
||||||
|
("female", "Female"),
|
||||||
|
("other", "Other"),
|
||||||
|
])
|
||||||
|
birthday = fields.Date()
|
||||||
|
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=[
|
||||||
|
("single", "Single"),
|
||||||
|
("married", "Married"),
|
||||||
|
("cohabitant", "Legal Cohabitant"),
|
||||||
|
("widower", "Widower"),
|
||||||
|
("divorced", "Divorced"),
|
||||||
|
],
|
||||||
|
default="single",
|
||||||
|
string="Marital Status",
|
||||||
|
)
|
||||||
|
marriage_anniversary_date = fields.Date(string="Anniversary Date")
|
||||||
|
private_street = fields.Char(string="Private Street")
|
||||||
|
private_street2 = fields.Char(string="Private Street2")
|
||||||
|
private_city = fields.Char(string="Private City")
|
||||||
|
private_state_id = fields.Many2one("res.country.state", string="Private State")
|
||||||
|
private_zip = fields.Char(string="Private Zip")
|
||||||
|
private_country_id = fields.Many2one("res.country", string="Private Country")
|
||||||
|
permanent_street = fields.Char(string="Permanent Street")
|
||||||
|
permanent_street2 = fields.Char(string="Permanent Street2")
|
||||||
|
permanent_city = fields.Char(string="Permanent City")
|
||||||
|
permanent_state_id = fields.Many2one("res.country.state", string="Permanent State")
|
||||||
|
permanent_zip = fields.Char(string="Permanent Zip")
|
||||||
|
permanent_country_id = fields.Many2one("res.country", string="Permanent Country")
|
||||||
|
pan_no = fields.Char(string="PAN No")
|
||||||
|
identification_id = fields.Char(string="Aadhar No")
|
||||||
|
previous_company_pf_no = fields.Char(string="Previous Company PF No")
|
||||||
|
previous_company_uan_no = fields.Char(string="Previous Company UAN No")
|
||||||
|
passport_no = fields.Char(string="Passport No")
|
||||||
|
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")
|
||||||
|
full_name_as_in_bank = fields.Char(string="Name as per Bank")
|
||||||
|
bank_name = fields.Char()
|
||||||
|
bank_branch = fields.Char()
|
||||||
|
bank_account_no = fields.Char()
|
||||||
|
bank_ifsc_code = fields.Char()
|
||||||
|
requested_attachment_ids = fields.Many2many(
|
||||||
|
"employee.bridge.requested.attachment",
|
||||||
|
"employee_bridge_requested_attachment_rel",
|
||||||
|
"bridge_id",
|
||||||
|
"attachment_id",
|
||||||
|
string="Attachments to Request",
|
||||||
|
)
|
||||||
|
joining_attachment_ids = fields.One2many(
|
||||||
|
"employee.bridge.attachment",
|
||||||
|
"bridge_id",
|
||||||
|
string="Attachments",
|
||||||
|
)
|
||||||
|
education_history = fields.One2many("education.history", "bridge_id", string="Education Details")
|
||||||
|
employer_history = fields.One2many("employer.history", "bridge_id", string="Employer History")
|
||||||
|
family_details = fields.One2many("family.details", "bridge_id", string="Family Details")
|
||||||
|
joining_form_link = fields.Char(string="Joining Form Link", copy=False, readonly=True)
|
||||||
|
access_token = fields.Char(copy=False, readonly=True)
|
||||||
|
total_exp = fields.Float(string="Total Exp")
|
||||||
|
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
"unique_bridge_employee",
|
||||||
|
"unique(employee_id)",
|
||||||
|
"This employee is already connected to an employee bridge.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_base_url(self):
|
||||||
|
return self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||||
|
|
||||||
|
def _ensure_access_token(self):
|
||||||
|
for bridge in self:
|
||||||
|
if not bridge.access_token:
|
||||||
|
bridge.access_token = token_urlsafe(32)
|
||||||
|
|
||||||
|
def _get_joining_form_link(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self._ensure_access_token()
|
||||||
|
return "%s/employee_bridge/jod/%s" % (self._get_base_url(), self.access_token)
|
||||||
|
|
||||||
|
def _group_requested_attachments(self, attachments):
|
||||||
|
return {
|
||||||
|
"personal_docs": attachments.filtered(lambda a: a.attachment_type == "personal").mapped("name"),
|
||||||
|
"education_docs": attachments.filtered(lambda a: a.attachment_type == "education").mapped("name"),
|
||||||
|
"previous_employer_docs": attachments.filtered(lambda a: a.attachment_type == "previous_employer").mapped("name"),
|
||||||
|
"other_docs": attachments.filtered(lambda a: a.attachment_type == "others").mapped("name"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _send_jod_mail(self, req_attachment_ids=False, email_values=None, email_body=False):
|
||||||
|
for bridge in self:
|
||||||
|
email_to = bridge.work_email or bridge.private_email or bridge.employee_id.work_email
|
||||||
|
if not email_to:
|
||||||
|
raise ValidationError(_("Add an email on the bridge before sending the JOD form."))
|
||||||
|
link = bridge._get_joining_form_link()
|
||||||
|
bridge.write({
|
||||||
|
"joining_form_link": link,
|
||||||
|
"state": "jod_sent",
|
||||||
|
})
|
||||||
|
attachments = req_attachment_ids or self.env["employee.bridge.requested.attachment"].browse()
|
||||||
|
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||||
|
context = {
|
||||||
|
"joining_form_link": link,
|
||||||
|
**bridge._group_requested_attachments(attachments),
|
||||||
|
}
|
||||||
|
values = dict(email_values or {})
|
||||||
|
values.setdefault("email_from", self.env.company.email or self.env.user.email)
|
||||||
|
values.setdefault("email_to", email_to)
|
||||||
|
if email_body:
|
||||||
|
values["body_html"] = email_body
|
||||||
|
if template:
|
||||||
|
rendered_subject = template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id]
|
||||||
|
rendered_body = template.with_context(**context)._render_field(
|
||||||
|
"body_html", [bridge.id], compute_lang=True
|
||||||
|
)[bridge.id]
|
||||||
|
values.setdefault("subject", rendered_subject)
|
||||||
|
values.setdefault("body_html", rendered_body)
|
||||||
|
template.sudo().with_context(**context).send_mail(bridge.id, email_values=values, force_send=True)
|
||||||
|
else:
|
||||||
|
values.setdefault("subject", _("Joining Onboarding Details"))
|
||||||
|
values.setdefault("body_html", _(
|
||||||
|
"<p>Hello %(name)s,</p>"
|
||||||
|
"<p>Please submit your joining onboarding details using this link:</p>"
|
||||||
|
"<p><a href='%(link)s'>Open Joining Form</a></p>"
|
||||||
|
) % {
|
||||||
|
"name": bridge.employee_name or bridge.employee_id.name or _("Employee"),
|
||||||
|
"link": link,
|
||||||
|
})
|
||||||
|
self.env["mail.mail"].sudo().create(values).send()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_send_jod_form(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"name": _("Select Attachments"),
|
||||||
|
"res_model": "employee.bridge.attachment.wizard",
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
"context": {
|
||||||
|
"default_bridge_id": self.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_employee(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.employee_id:
|
||||||
|
raise ValidationError(_("No employee is linked to this bridge."))
|
||||||
|
return {
|
||||||
|
"name": _("Employee"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "hr.employee",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": self.employee_id.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_validate_employee_details(self):
|
||||||
|
for bridge in self:
|
||||||
|
if not bridge.employee_id:
|
||||||
|
raise ValidationError(_("Link or create an employee before validating the JOD details."))
|
||||||
|
bridge.employee_id.write(bridge._get_employee_update_vals())
|
||||||
|
bridge.education_history.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.employer_history.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.family_details.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.joining_attachment_ids.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.state = "validated"
|
||||||
|
|
||||||
|
def _get_employee_create_vals(self):
|
||||||
|
self.ensure_one()
|
||||||
|
vals = {
|
||||||
|
"name": self.employee_name,
|
||||||
|
"employee_id": self.employee_code,
|
||||||
|
"work_email": self.work_email,
|
||||||
|
"private_email": self.private_email,
|
||||||
|
"work_phone": self.work_phone,
|
||||||
|
"mobile_phone": self.mobile_phone,
|
||||||
|
"image_1920": self.candidate_image,
|
||||||
|
"department_id": self.department_id.id,
|
||||||
|
"job_id": self.job_id.id,
|
||||||
|
"job_title": self.job_id.name,
|
||||||
|
"emp_type": self.emp_type.id,
|
||||||
|
"company_id": self.company_id.id,
|
||||||
|
}
|
||||||
|
vals = {key: value for key, value in vals.items() if value not in (False, "", 0)}
|
||||||
|
if not vals.get("name"):
|
||||||
|
raise ValidationError(_("Employee Name is required to create an employee."))
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def _get_employee_update_vals(self):
|
||||||
|
self.ensure_one()
|
||||||
|
vals = {
|
||||||
|
"name": self.employee_name,
|
||||||
|
"employee_id": self.employee_code,
|
||||||
|
"work_email": self.work_email,
|
||||||
|
"private_email": self.private_email,
|
||||||
|
"work_phone": self.work_phone,
|
||||||
|
"mobile_phone": self.mobile_phone,
|
||||||
|
"image_1920": self.candidate_image,
|
||||||
|
"department_id": self.department_id.id,
|
||||||
|
"job_id": self.job_id.id,
|
||||||
|
"job_title": self.job_id.name,
|
||||||
|
"emp_type": self.emp_type.id,
|
||||||
|
"doj": self.doj,
|
||||||
|
"gender": self.gender,
|
||||||
|
"birthday": self.birthday,
|
||||||
|
"blood_group": self.blood_group,
|
||||||
|
"marital": self.marital,
|
||||||
|
"marriage_anniversary_date": self.marriage_anniversary_date,
|
||||||
|
"private_street": self.private_street,
|
||||||
|
"private_street2": self.private_street2,
|
||||||
|
"private_city": self.private_city,
|
||||||
|
"private_state_id": self.private_state_id.id,
|
||||||
|
"private_zip": self.private_zip,
|
||||||
|
"private_country_id": self.private_country_id.id,
|
||||||
|
"permanent_street": self.permanent_street,
|
||||||
|
"permanent_street2": self.permanent_street2,
|
||||||
|
"permanent_city": self.permanent_city,
|
||||||
|
"permanent_state_id": self.permanent_state_id.id,
|
||||||
|
"permanent_zip": self.permanent_zip,
|
||||||
|
"permanent_country_id": self.permanent_country_id.id,
|
||||||
|
"pan_no": self.pan_no,
|
||||||
|
"identification_id": self.identification_id,
|
||||||
|
"previous_company_pf_no": self.previous_company_pf_no,
|
||||||
|
"previous_company_uan_no": self.previous_company_uan_no,
|
||||||
|
"passport_id": self.passport_no,
|
||||||
|
"passport_start_date": self.passport_start_date,
|
||||||
|
"passport_end_date": self.passport_end_date,
|
||||||
|
"passport_issued_location": self.passport_issued_location,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in vals.items()
|
||||||
|
if key in self.employee_id._fields and value not in (False, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_create_employee(self):
|
||||||
|
for bridge in self:
|
||||||
|
if bridge.employee_id:
|
||||||
|
continue
|
||||||
|
bridge.employee_id = self.env["hr.employee"].sudo().create(bridge._get_employee_create_vals())
|
||||||
|
return self.action_open_employee()
|
||||||
|
|
||||||
|
def _sync_from_employee(self):
|
||||||
|
for bridge in self.filtered("employee_id"):
|
||||||
|
employee = bridge.employee_id
|
||||||
|
bridge.write({
|
||||||
|
"employee_name": bridge.employee_name or employee.name,
|
||||||
|
"employee_code": bridge.employee_code or employee.employee_id,
|
||||||
|
"work_email": bridge.work_email or employee.work_email,
|
||||||
|
"private_email": bridge.private_email or employee.private_email,
|
||||||
|
"work_phone": bridge.work_phone or employee.work_phone,
|
||||||
|
"mobile_phone": bridge.mobile_phone or employee.mobile_phone,
|
||||||
|
"department_id": bridge.department_id.id or employee.department_id.id,
|
||||||
|
"job_id": bridge.job_id.id or employee.job_id.id,
|
||||||
|
"emp_type": bridge.emp_type.id or employee.emp_type.id,
|
||||||
|
"company_id": employee.company_id.id or bridge.company_id.id,
|
||||||
|
"doj": bridge.doj or employee.doj,
|
||||||
|
"gender": bridge.gender or employee.gender,
|
||||||
|
"birthday": bridge.birthday or employee.birthday,
|
||||||
|
"blood_group": bridge.blood_group or employee.blood_group,
|
||||||
|
"marital": bridge.marital or employee.marital,
|
||||||
|
"marriage_anniversary_date": bridge.marriage_anniversary_date or employee.marriage_anniversary_date,
|
||||||
|
"private_street": bridge.private_street or employee.private_street,
|
||||||
|
"private_street2": bridge.private_street2 or employee.private_street2,
|
||||||
|
"private_city": bridge.private_city or employee.private_city,
|
||||||
|
"private_state_id": bridge.private_state_id.id or employee.private_state_id.id,
|
||||||
|
"private_zip": bridge.private_zip or employee.private_zip,
|
||||||
|
"private_country_id": bridge.private_country_id.id or employee.private_country_id.id,
|
||||||
|
"permanent_street": bridge.permanent_street or employee.permanent_street,
|
||||||
|
"permanent_street2": bridge.permanent_street2 or employee.permanent_street2,
|
||||||
|
"permanent_city": bridge.permanent_city or employee.permanent_city,
|
||||||
|
"permanent_state_id": bridge.permanent_state_id.id or employee.permanent_state_id.id,
|
||||||
|
"permanent_zip": bridge.permanent_zip or employee.permanent_zip,
|
||||||
|
"permanent_country_id": bridge.permanent_country_id.id or employee.permanent_country_id.id,
|
||||||
|
"pan_no": bridge.pan_no or employee.pan_no,
|
||||||
|
"identification_id": bridge.identification_id or employee.identification_id,
|
||||||
|
"previous_company_pf_no": bridge.previous_company_pf_no or employee.previous_company_pf_no,
|
||||||
|
"previous_company_uan_no": bridge.previous_company_uan_no or employee.previous_company_uan_no,
|
||||||
|
"passport_no": bridge.passport_no or employee.passport_id,
|
||||||
|
"passport_start_date": bridge.passport_start_date or employee.passport_start_date,
|
||||||
|
"passport_end_date": bridge.passport_end_date or employee.passport_end_date,
|
||||||
|
"passport_issued_location": bridge.passport_issued_location or employee.passport_issued_location,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.onchange("employee_id")
|
||||||
|
def _onchange_employee_id(self):
|
||||||
|
for bridge in self:
|
||||||
|
employee = bridge.employee_id
|
||||||
|
if not employee:
|
||||||
|
continue
|
||||||
|
bridge.employee_name = bridge.employee_name or employee.name
|
||||||
|
bridge.employee_code = bridge.employee_code or employee.employee_id
|
||||||
|
bridge.work_email = bridge.work_email or employee.work_email
|
||||||
|
bridge.private_email = bridge.private_email or employee.private_email
|
||||||
|
bridge.work_phone = bridge.work_phone or employee.work_phone
|
||||||
|
bridge.mobile_phone = bridge.mobile_phone or employee.mobile_phone
|
||||||
|
bridge.department_id = bridge.department_id or employee.department_id
|
||||||
|
bridge.job_id = bridge.job_id or employee.job_id
|
||||||
|
bridge.emp_type = bridge.emp_type or employee.emp_type
|
||||||
|
|
||||||
|
def write_from_public_form(self, values):
|
||||||
|
self.ensure_one()
|
||||||
|
private_state = self.env["res.country.state"].sudo().browse(int(values.get("present_state") or 0))
|
||||||
|
permanent_state = self.env["res.country.state"].sudo().browse(int(values.get("permanent_state") or 0))
|
||||||
|
mapped_values = dict(values)
|
||||||
|
mapped_values.update({
|
||||||
|
"candidate_image": values.get("candidate_image_base64"),
|
||||||
|
"private_email": values.get("email_from"),
|
||||||
|
"mobile_phone": values.get("partner_phone"),
|
||||||
|
"alternate_phone": values.get("alternate_phone"),
|
||||||
|
"private_street": values.get("present_street"),
|
||||||
|
"private_street2": values.get("present_street2"),
|
||||||
|
"private_city": values.get("present_city"),
|
||||||
|
"private_zip": values.get("present_zip"),
|
||||||
|
"private_state_id": private_state.id if private_state else False,
|
||||||
|
"private_country_id": private_state.country_id.id if private_state else False,
|
||||||
|
"permanent_state_id": permanent_state.id if permanent_state else False,
|
||||||
|
"permanent_country_id": permanent_state.country_id.id if permanent_state else False,
|
||||||
|
})
|
||||||
|
allowed_fields = {
|
||||||
|
"employee_name",
|
||||||
|
"candidate_image",
|
||||||
|
"private_email",
|
||||||
|
"mobile_phone",
|
||||||
|
"alternate_phone",
|
||||||
|
"doj",
|
||||||
|
"gender",
|
||||||
|
"birthday",
|
||||||
|
"blood_group",
|
||||||
|
"marital",
|
||||||
|
"marriage_anniversary_date",
|
||||||
|
"private_street",
|
||||||
|
"private_street2",
|
||||||
|
"private_city",
|
||||||
|
"private_state_id",
|
||||||
|
"private_country_id",
|
||||||
|
"private_zip",
|
||||||
|
"permanent_street",
|
||||||
|
"permanent_street2",
|
||||||
|
"permanent_city",
|
||||||
|
"permanent_state_id",
|
||||||
|
"permanent_country_id",
|
||||||
|
"permanent_zip",
|
||||||
|
"full_name_as_in_bank",
|
||||||
|
"bank_name",
|
||||||
|
"bank_branch",
|
||||||
|
"bank_account_no",
|
||||||
|
"bank_ifsc_code",
|
||||||
|
"pan_no",
|
||||||
|
"identification_id",
|
||||||
|
"previous_company_pf_no",
|
||||||
|
"previous_company_uan_no",
|
||||||
|
"passport_no",
|
||||||
|
"passport_start_date",
|
||||||
|
"passport_end_date",
|
||||||
|
"passport_issued_location",
|
||||||
|
}
|
||||||
|
bridge_vals = {
|
||||||
|
field_name: value or False
|
||||||
|
for field_name, value in mapped_values.items()
|
||||||
|
if field_name in allowed_fields and field_name in self._fields
|
||||||
|
}
|
||||||
|
if bridge_vals:
|
||||||
|
self.sudo().write(bridge_vals)
|
||||||
|
self._write_history_from_public_form(values)
|
||||||
|
self._replace_joining_attachments_from_public_form(values)
|
||||||
|
self.state = "jod_received"
|
||||||
|
|
||||||
|
def _safe_date(self, value):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, "%Y-%m-%d").date() if value else False
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _safe_int(self, value):
|
||||||
|
try:
|
||||||
|
return int(value) if value else False
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _json_list(self, values, key):
|
||||||
|
raw = values.get(key, "[]")
|
||||||
|
try:
|
||||||
|
data = json.loads(raw) if raw else []
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
data = []
|
||||||
|
return data if isinstance(data, list) else []
|
||||||
|
|
||||||
|
def _write_history_from_public_form(self, values):
|
||||||
|
self.ensure_one()
|
||||||
|
family_values = []
|
||||||
|
for member in self._json_list(values, "family_data_json"):
|
||||||
|
if member.get("name") and member.get("relation"):
|
||||||
|
family_values.append((0, 0, {
|
||||||
|
"relation_type": member.get("relation"),
|
||||||
|
"name": member.get("name"),
|
||||||
|
"contact_no": member.get("contact"),
|
||||||
|
"dob": self._safe_date(member.get("dob")),
|
||||||
|
"location": member.get("location"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
education_values = []
|
||||||
|
for education in self._json_list(values, "education_data_json"):
|
||||||
|
if education.get("specialization"):
|
||||||
|
education_values.append((0, 0, {
|
||||||
|
"education_type": education.get("education_type") or "additional",
|
||||||
|
"name": education.get("specialization"),
|
||||||
|
"university": education.get("university"),
|
||||||
|
"start_year": self._safe_int(education.get("start_year")),
|
||||||
|
"end_year": self._safe_int(education.get("end_year")),
|
||||||
|
"marks_or_grade": education.get("marks_or_grade"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
employer_values = []
|
||||||
|
for employer in self._json_list(values, "employer_history_data_json"):
|
||||||
|
if employer.get("company_name"):
|
||||||
|
employer_values.append((0, 0, {
|
||||||
|
"company_name": employer.get("company_name"),
|
||||||
|
"designation": employer.get("designation"),
|
||||||
|
"date_of_joining": self._safe_date(employer.get("date_of_joining")),
|
||||||
|
"last_working_day": self._safe_date(employer.get("last_working_day")),
|
||||||
|
"ctc": employer.get("ctc"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
write_vals = {}
|
||||||
|
if family_values:
|
||||||
|
write_vals["family_details"] = [(5, 0, 0)] + family_values
|
||||||
|
if education_values:
|
||||||
|
write_vals["education_history"] = [(5, 0, 0)] + education_values
|
||||||
|
if employer_values:
|
||||||
|
write_vals["employer_history"] = [(5, 0, 0)] + employer_values
|
||||||
|
if write_vals:
|
||||||
|
self.sudo().write(write_vals)
|
||||||
|
|
||||||
|
def _replace_joining_attachments_from_public_form(self, values):
|
||||||
|
self.ensure_one()
|
||||||
|
attachment_values = []
|
||||||
|
seen_files = set()
|
||||||
|
for item in self._json_list(values, "attachments_data_json"):
|
||||||
|
attachment = self.env["employee.bridge.requested.attachment"].sudo().browse(
|
||||||
|
int(item.get("attachment_rec_id") or 0)
|
||||||
|
)
|
||||||
|
file_content = item.get("file_content")
|
||||||
|
file_name = item.get("file_name")
|
||||||
|
file_key = (attachment.id, file_name, file_content)
|
||||||
|
if attachment.exists() and file_content and file_key not in seen_files:
|
||||||
|
seen_files.add(file_key)
|
||||||
|
attachment_values.append((0, 0, {
|
||||||
|
"name": attachment.name,
|
||||||
|
"requested_attachment_id": attachment.id,
|
||||||
|
"file": file_content,
|
||||||
|
"file_name": file_name,
|
||||||
|
}))
|
||||||
|
self.sudo().write({"joining_attachment_ids": [(5, 0, 0)] + attachment_values})
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get("name", "/") == "/":
|
||||||
|
vals["name"] = self.env["ir.sequence"].next_by_code("recruitment.employee.bridge") or "/"
|
||||||
|
if vals.get("employee_id") and not vals.get("company_id"):
|
||||||
|
employee = self.env["hr.employee"].browse(vals["employee_id"])
|
||||||
|
vals["company_id"] = employee.company_id.id or self.env.company.id
|
||||||
|
bridges = super().create(vals_list)
|
||||||
|
bridges._sync_from_employee()
|
||||||
|
return bridges
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if "employee_id" in vals:
|
||||||
|
self._sync_from_employee()
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_employee_bridge_user,employee.bridge.user,model_recruitment_employee_bridge,base.group_user,1,1,1,0
|
||||||
|
access_employee_bridge_manager,employee.bridge.manager,model_recruitment_employee_bridge,hr.group_hr_manager,1,1,1,1
|
||||||
|
access_employee_bridge_requested_attachment_user,employee.bridge.requested.attachment.user,model_employee_bridge_requested_attachment,base.group_user,1,1,1,0
|
||||||
|
access_employee_bridge_requested_attachment_manager,employee.bridge.requested.attachment.manager,model_employee_bridge_requested_attachment,hr.group_hr_manager,1,1,1,1
|
||||||
|
access_employee_bridge_attachment_user,employee.bridge.attachment.user,model_employee_bridge_attachment,base.group_user,1,1,1,0
|
||||||
|
access_employee_bridge_attachment_manager,employee.bridge.attachment.manager,model_employee_bridge_attachment,hr.group_hr_manager,1,1,1,1
|
||||||
|
access_employee_bridge_attachment_wizard,employee.bridge.attachment.wizard.user,model_employee_bridge_attachment_wizard,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,754 @@
|
||||||
|
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.employeeBridgeJod = 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",
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.uploadedFiles = {}; // Store files per attachment ID for this form only.
|
||||||
|
this.isSubmitting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getFileSignature(file) {
|
||||||
|
return [file.name, file.size, file.lastModified, file.type].join("|");
|
||||||
|
},
|
||||||
|
|
||||||
|
addUploadedFileRow(attachmentId, file, base64String) {
|
||||||
|
console.log("addUploadedFileRow", attachmentId, file.name);
|
||||||
|
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
||||||
|
|
||||||
|
if (!this.uploadedFiles[attachmentId]) {
|
||||||
|
this.uploadedFiles[attachmentId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSignature = this._getFileSignature(file);
|
||||||
|
const isDuplicate = this.uploadedFiles[attachmentId].some(
|
||||||
|
uploadedFile => uploadedFile.signature === fileSignature
|
||||||
|
);
|
||||||
|
if (isDuplicate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique file ID using attachmentId and a timestamp
|
||||||
|
const fileId = `${attachmentId}-${Date.now()}-${this.uploadedFiles[attachmentId].length}`;
|
||||||
|
|
||||||
|
const fileRecord = {
|
||||||
|
attachment_rec_id : attachmentId,
|
||||||
|
id: fileId, // Unique file ID
|
||||||
|
name: file.name,
|
||||||
|
base64: base64String,
|
||||||
|
type: file.type,
|
||||||
|
signature: fileSignature,
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.log("handleAttachmentUpload");
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$(input).val('');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUploadNewFile(ev) {
|
||||||
|
const button = $(ev.currentTarget);
|
||||||
|
const attachmentId = button.data('attachment-id');
|
||||||
|
const index = button.data('index');
|
||||||
|
const hiddenInput = this.$(`.upload-new-file-input[data-attachment-id='${attachmentId}']`);
|
||||||
|
|
||||||
|
hiddenInput.off('change').on('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const base64String = event.target.result.split(',')[1];
|
||||||
|
this.uploadedFiles[attachmentId][index] = {
|
||||||
|
name: file.name,
|
||||||
|
base64: base64String,
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageId = `preview_image_${attachmentId}_${index}`;
|
||||||
|
const filePreviewWrapperId = `preview_wrapper_${attachmentId}_${index}`;
|
||||||
|
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>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNameInputId = `file_name_input_${attachmentId}_${index}`;
|
||||||
|
this.$(`#${fileNameInputId}`).val(file.name);
|
||||||
|
|
||||||
|
// CRITICAL FIX: Clear input after reading
|
||||||
|
$(e.target).val('');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
hiddenInput.trigger('click');
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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.isSubmitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateFamilyDetails()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSubmitting = true;
|
||||||
|
this.$("#submit-btn").prop("disabled", true);
|
||||||
|
|
||||||
|
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];
|
||||||
|
if (filesArray && Array.isArray(filesArray)) {
|
||||||
|
filesArray.forEach(file => {
|
||||||
|
// Only push if base64 actually exists (prevents ghost/empty data)
|
||||||
|
if (file && file.base64) {
|
||||||
|
attachments.push({
|
||||||
|
attachment_rec_id: file.attachment_rec_id,
|
||||||
|
file_name: file.name,
|
||||||
|
file_content: file.base64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
this.isSubmitting = false;
|
||||||
|
this.$("#submit-btn").prop("disabled", false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 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("/employee_bridge/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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_employee_view_form_bridge" model="ir.ui.view">
|
||||||
|
<field name="name">hr.employee.view.form.bridge</field>
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="action_open_recruitment_bridge"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-link"
|
||||||
|
groups="hr.group_hr_user"
|
||||||
|
invisible="not recruitment_bridge_count">
|
||||||
|
<field name="recruitment_bridge_count" widget="statinfo" string="Onboarding"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//label[@for='user_id']" position="before">
|
||||||
|
<button string="Send JOD Form"
|
||||||
|
name="send_jod_form_to_employee"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
groups="hr.group_hr_user"
|
||||||
|
invisible="not work_email"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_bridge_requested_attachment_list" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.requested.attachment.list</field>
|
||||||
|
<field name="model">employee.bridge.requested.attachment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attachment_type"/>
|
||||||
|
<field name="is_default"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_employee_bridge_requested_attachment_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.requested.attachment.form</field>
|
||||||
|
<field name="model">employee.bridge.requested.attachment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attachment_type"/>
|
||||||
|
<field name="is_default"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_employee_bridge_requested_attachment" model="ir.actions.act_window">
|
||||||
|
<field name="name">Onboarding Documents</field>
|
||||||
|
<field name="res_model">employee.bridge.requested.attachment</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_bridge_requested_attachment"
|
||||||
|
name="Onboarding Documents"
|
||||||
|
parent="menu_employee_bridge"
|
||||||
|
action="action_employee_bridge_requested_attachment"
|
||||||
|
sequence="20"
|
||||||
|
groups="hr.group_hr_user"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_bridge_list" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.list</field>
|
||||||
|
<field name="model">recruitment.employee.bridge</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="employee_code"/>
|
||||||
|
<field name="work_email"/>
|
||||||
|
<field name="joining_form_link" optional="show"/>
|
||||||
|
<field name="source"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_employee_bridge_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.form</field>
|
||||||
|
<field name="model">recruitment.employee.bridge</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<header>
|
||||||
|
<button string="Send JOD Form" name="action_send_jod_form" type="object" class="btn-primary" invisible="state == 'validated'"/>
|
||||||
|
<button string="Create Employee" name="action_create_employee" type="object" class="btn-secondary" invisible="employee_id"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,jod_sent,jod_received,validated"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_open_employee" type="object" class="oe_stat_button" icon="fa-id-card-o" invisible="not employee_id">
|
||||||
|
<div class="o_field_widget o_stat_info">
|
||||||
|
<span class="o_stat_value"><field name="employee_code" readonly="1"/></span>
|
||||||
|
<span class="o_stat_text">Employee</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<group string="Onboarding">
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="source"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="employee_code"/>
|
||||||
|
<field name="joining_form_link" widget="url"/>
|
||||||
|
<field name="requested_attachment_ids" widget="many2many_tags"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<button string="Validate Into Employee" name="action_validate_employee_details" type="object" class="btn-primary" invisible="state == 'validated' or not employee_id"/>
|
||||||
|
<notebook>
|
||||||
|
<page string="Basic Details" name="basic_details">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_name"/>
|
||||||
|
<field name="candidate_image" widget="image" class="oe_avatar"/>
|
||||||
|
<field name="employee_code"/>
|
||||||
|
<field name="work_email" widget="email"/>
|
||||||
|
<field name="private_email" widget="email"/>
|
||||||
|
<field name="work_phone" widget="phone"/>
|
||||||
|
<field name="mobile_phone" widget="phone"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="job_id"/>
|
||||||
|
<field name="emp_type"/>
|
||||||
|
<field name="total_exp"/>
|
||||||
|
<field name="doj"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="JOD Details" name="jod_details">
|
||||||
|
<group string="Personal Details">
|
||||||
|
<group>
|
||||||
|
<field name="gender"/>
|
||||||
|
<field name="birthday"/>
|
||||||
|
<field name="blood_group"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="marital"/>
|
||||||
|
<field name="marriage_anniversary_date" invisible="marital == 'single'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Current Address">
|
||||||
|
<group>
|
||||||
|
<field name="private_street"/>
|
||||||
|
<field name="private_street2"/>
|
||||||
|
<field name="private_city"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="private_state_id"/>
|
||||||
|
<field name="private_zip"/>
|
||||||
|
<field name="private_country_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Permanent Address">
|
||||||
|
<group>
|
||||||
|
<field name="permanent_street"/>
|
||||||
|
<field name="permanent_street2"/>
|
||||||
|
<field name="permanent_city"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="permanent_state_id"/>
|
||||||
|
<field name="permanent_zip"/>
|
||||||
|
<field name="permanent_country_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Authentication">
|
||||||
|
<group>
|
||||||
|
<field name="pan_no"/>
|
||||||
|
<field name="identification_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="previous_company_pf_no"/>
|
||||||
|
<field name="previous_company_uan_no"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Passport">
|
||||||
|
<group>
|
||||||
|
<field name="passport_no"/>
|
||||||
|
<field name="passport_start_date"/>
|
||||||
|
<field name="passport_end_date"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="passport_issued_location"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Family Details" name="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>
|
||||||
|
</page>
|
||||||
|
<page string="Employer History" name="employer_history">
|
||||||
|
<field name="employer_history" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="company_name"/>
|
||||||
|
<field name="designation"/>
|
||||||
|
<field name="date_of_joining"/>
|
||||||
|
<field name="last_working_day"/>
|
||||||
|
<field name="ctc"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Education Details" name="education_history">
|
||||||
|
<field name="education_history" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<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>
|
||||||
|
</page>
|
||||||
|
<page string="Attachments" name="attachments">
|
||||||
|
<field name="joining_attachment_ids" nolabel="1">
|
||||||
|
<list editable="bottom" default_group_by="requested_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
|
||||||
|
<field name="requested_attachment_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attachment_type"/>
|
||||||
|
<field name="file" widget="binary" filename="file_name"/>
|
||||||
|
<field name="file_name" optional="hide"/>
|
||||||
|
<button name="action_preview_file" type="object" string="Preview" class="oe_highlight" icon="fa-eye"/>
|
||||||
|
<field name="review_status"/>
|
||||||
|
<field name="review_comments" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_employee_bridge_search" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.search</field>
|
||||||
|
<field name="model">recruitment.employee.bridge</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<filter name="jod_sent" string="JOD Sent" domain="[('state', '=', 'jod_sent')]"/>
|
||||||
|
<filter name="jod_received" string="JOD Received" domain="[('state', '=', 'jod_received')]"/>
|
||||||
|
<filter name="validated" string="Validated" domain="[('state', '=', 'validated')]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
|
||||||
|
<filter name="group_source" string="Source" context="{'group_by': 'source'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_employee_bridge" model="ir.actions.act_window">
|
||||||
|
<field name="name">Onboarding</field>
|
||||||
|
<field name="res_model">recruitment.employee.bridge</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_bridge"
|
||||||
|
name="Onboarding"
|
||||||
|
parent="hr.menu_hr_root"
|
||||||
|
sequence="90"
|
||||||
|
groups="hr.group_hr_user"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_bridge_onboarding"
|
||||||
|
name="Onboarding"
|
||||||
|
parent="menu_employee_bridge"
|
||||||
|
action="action_employee_bridge"
|
||||||
|
sequence="10"
|
||||||
|
groups="hr.group_hr_user"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import employee_bridge_attachment_wizard
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBridgeAttachmentWizard(models.TransientModel):
|
||||||
|
_name = "employee.bridge.attachment.wizard"
|
||||||
|
_description = "Onboarding Attachment Request Wizard"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, readonly=True)
|
||||||
|
req_attachment_ids = fields.Many2many(
|
||||||
|
comodel_name="employee.bridge.requested.attachment",
|
||||||
|
relation="employee_bridge_req_attachment_rel", # relation table name
|
||||||
|
column1="request_id", # column referring to this model
|
||||||
|
column2="attachment_id", # column referring to the comodel
|
||||||
|
string="Attachments to Request",
|
||||||
|
)
|
||||||
|
attachment_ids = fields.Many2many("ir.attachment")
|
||||||
|
template_id = fields.Many2one(
|
||||||
|
"mail.template",
|
||||||
|
default=lambda self: self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False),
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
email_from = fields.Char("Email From")
|
||||||
|
email_to = fields.Char("Email To")
|
||||||
|
email_cc = fields.Text("Email CC")
|
||||||
|
email_subject = fields.Char()
|
||||||
|
email_body = fields.Html(
|
||||||
|
"Body",
|
||||||
|
render_engine="qweb",
|
||||||
|
render_options={"post_process": True},
|
||||||
|
prefetch=True,
|
||||||
|
translate=True,
|
||||||
|
sanitize="email_outgoing",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
defaults = super().default_get(fields_list)
|
||||||
|
bridge = self.env["recruitment.employee.bridge"].browse(
|
||||||
|
self.env.context.get("default_bridge_id") or self.env.context.get("bridge_id")
|
||||||
|
)
|
||||||
|
if bridge:
|
||||||
|
attachments = bridge.requested_attachment_ids or self.env["employee.bridge.requested.attachment"].search([
|
||||||
|
("is_default", "=", True),
|
||||||
|
])
|
||||||
|
defaults.update({
|
||||||
|
"bridge_id": bridge.id,
|
||||||
|
"req_attachment_ids": [(6, 0, attachments.ids)],
|
||||||
|
"email_from": self.env.company.email or self.env.user.email,
|
||||||
|
"email_to": bridge.work_email or bridge.private_email or bridge.employee_id.work_email,
|
||||||
|
})
|
||||||
|
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||||
|
if template:
|
||||||
|
context = {
|
||||||
|
"joining_form_link": bridge._get_joining_form_link(),
|
||||||
|
**bridge._group_requested_attachments(attachments),
|
||||||
|
}
|
||||||
|
defaults.update({
|
||||||
|
"template_id": template.id,
|
||||||
|
"email_subject": template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id],
|
||||||
|
"email_body": template.with_context(**context)._render_field(
|
||||||
|
"body_html", [bridge.id], compute_lang=True
|
||||||
|
)[bridge.id],
|
||||||
|
})
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
@api.onchange("req_attachment_ids")
|
||||||
|
def _onchange_req_attachment_ids(self):
|
||||||
|
if not self.bridge_id or not self.template_id:
|
||||||
|
return
|
||||||
|
context = {
|
||||||
|
"joining_form_link": self.bridge_id._get_joining_form_link(),
|
||||||
|
**self.bridge_id._group_requested_attachments(self.req_attachment_ids),
|
||||||
|
}
|
||||||
|
self.email_body = self.template_id.with_context(**context)._render_field(
|
||||||
|
"body_html", [self.bridge_id.id], compute_lang=True
|
||||||
|
)[self.bridge_id.id]
|
||||||
|
|
||||||
|
def action_confirm(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self.bridge_id.requested_attachment_ids = [(6, 0, self.req_attachment_ids.ids)]
|
||||||
|
self.bridge_id._send_jod_mail(
|
||||||
|
req_attachment_ids=self.req_attachment_ids,
|
||||||
|
email_values={
|
||||||
|
"email_from": self.email_from,
|
||||||
|
"email_to": self.email_to,
|
||||||
|
"email_cc": self.email_cc,
|
||||||
|
"subject": self.email_subject,
|
||||||
|
"attachment_ids": [(6, 0, self.attachment_ids.ids)],
|
||||||
|
},
|
||||||
|
email_body=self.email_body,
|
||||||
|
)
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_bridge_attachment_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.attachment.wizard.form</field>
|
||||||
|
<field name="model">employee.bridge.attachment.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Select Attachments">
|
||||||
|
<group>
|
||||||
|
<field name="bridge_id" invisible="1"/>
|
||||||
|
<field name="req_attachment_ids" widget="many2many_tags" force_save="1"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page name="attachment" string="Attachments (Binary)">
|
||||||
|
<field name="attachment_ids" widget="many2many_binary" domain="[('mimetype', 'not ilike', 'image')]"/>
|
||||||
|
</page>
|
||||||
|
<page name="email" string="Email">
|
||||||
|
<group>
|
||||||
|
<field name="template_id" options="{'no_create': True}" readonly="1" force_save="1"/>
|
||||||
|
<field name="email_from" force_save="1"/>
|
||||||
|
<field name="email_to" force_save="1"/>
|
||||||
|
<field name="email_cc"/>
|
||||||
|
<field name="email_subject" force_save="1"/>
|
||||||
|
<field name="email_body" widget="html_mail" class="oe-bordered-editor" options="{'codeview': true}" force_save="1"/>
|
||||||
|
<field name="attachment_ids" widget="many2many_tags" string="Attachments" force_save="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
<footer>
|
||||||
|
<button name="action_confirm" type="object" string="Send JOD Form" class="btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -10,12 +10,10 @@
|
||||||
""",
|
""",
|
||||||
'author': 'FTPROTECH',
|
'author': 'FTPROTECH',
|
||||||
'website': 'https://ftprotech.in',
|
'website': 'https://ftprotech.in',
|
||||||
'depends': ['hr', 'mail','hr_employee_extended','hr_recruitment_extended'],
|
'depends': ['hr', 'mail', 'hr_employee_extended', 'employee_bridge'],
|
||||||
'data': [
|
'data': [
|
||||||
'data/data.xml',
|
|
||||||
'data/actions.xml',
|
'data/actions.xml',
|
||||||
'data/template.xml',
|
'data/template.xml',
|
||||||
'views/emp_jod.xml',
|
|
||||||
],
|
],
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,22 @@
|
||||||
from odoo import http, _
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.hr_recruitment_extended.controllers.controllers import website_hr_recruitment_applications
|
|
||||||
from odoo.http import content_disposition
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from odoo.tools import misc
|
from odoo.tools import misc
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class website_hr_recruitment_applications_extended(website_hr_recruitment_applications):
|
class EmployeeJodController(http.Controller):
|
||||||
|
@http.route("/download/employee_jod/<int:employee_id>", type="http", auth="user")
|
||||||
@http.route(['/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
def download_employee_jod_form(self, employee_id, **kwargs):
|
||||||
website=True)
|
employee = request.env["hr.employee"].sudo().browse(employee_id)
|
||||||
def post_onboarding_form(self, applicant_id, **kwargs):
|
if not employee.exists():
|
||||||
"""Renders the website form for applicants to submit additional details."""
|
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
|
||||||
if not applicant.exists():
|
|
||||||
return request.not_found()
|
|
||||||
if applicant:
|
|
||||||
if applicant.post_onboarding_form_status == 'done':
|
|
||||||
return request.render("hr_recruitment_extended.thank_you_template", {
|
|
||||||
'applicant': applicant
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return request.render("hr_recruitment_extended.post_onboarding_form_template", {
|
|
||||||
'applicant': applicant
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
|
|
||||||
@http.route(['/download/jod/<int:applicant_id>'], type='http', auth="public", cors='*', website=True)
|
template = request.env.ref("employee_jod.emp_joining_form_template", raise_if_not_found=False)
|
||||||
def download_jod_form(self, applicant_id, **kwargs):
|
|
||||||
# Get the applicant record
|
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
|
||||||
if not applicant.exists():
|
|
||||||
return f"Error: Applicant with ID {applicant_id} not found"
|
|
||||||
|
|
||||||
# Business logic check
|
|
||||||
if applicant.post_onboarding_form_status != 'done':
|
|
||||||
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
|
|
||||||
|
|
||||||
# Get the template
|
|
||||||
template = request.env.ref('hr_recruitment_extended.employee_joining_form_template')
|
|
||||||
if not template:
|
if not template:
|
||||||
return "Error: Template not found"
|
return "Error: Template not found"
|
||||||
|
|
||||||
try:
|
return request.env["ir.qweb"]._render(template.id, {
|
||||||
# Render the template to HTML for debugging
|
"docs": employee,
|
||||||
html = request.env['ir.qweb']._render(
|
"doc": employee,
|
||||||
template.id,
|
"time": misc.datetime,
|
||||||
{
|
"user": request.env.user,
|
||||||
'docs': applicant,
|
})
|
||||||
'doc': applicant,
|
|
||||||
'time': misc.datetime,
|
|
||||||
'user': request.env.user,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return HTML for debugging
|
|
||||||
return html
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error rendering template: {str(e)}"
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
<field name="report_file">employee_jod.emp_joining_form_template</field>
|
<field name="report_file">employee_jod.emp_joining_form_template</field>
|
||||||
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
||||||
<field name="print_report_name">'JOD - %s' % (object.display_name)</field>
|
<field name="print_report_name">'JOD - %s' % (object.display_name)</field>
|
||||||
<field name="paperformat_id" ref="hr_recruitment_extended.custom_paper_format"/>
|
|
||||||
<field name="binding_type">report</field>
|
<field name="binding_type">report</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<data noupdate="1">
|
|
||||||
<!-- Original job record -->
|
|
||||||
<record model="hr.job" id="employee_jod_internal_job_id">
|
|
||||||
<field name="name">Internal Job</field>
|
|
||||||
<field name="active" eval="False"/>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- Recruitment stage -->
|
|
||||||
<record model="hr.recruitment.stage" id="hired_stage8">
|
|
||||||
<field name="name">IJ</field>
|
|
||||||
<field name="is_default_field" eval="False"/>
|
|
||||||
<field name="hired_stage" eval="True"/>
|
|
||||||
<!-- <field name="post_onboarding_form" eval="True"/>-->
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- Changed ID for the recruitment record -->
|
|
||||||
<record model="hr.job.recruitment" id="employee_jod_internal_job_recruitment_id">
|
|
||||||
<field name="recruitment_sequence">IJ001</field>
|
|
||||||
<field name="job_id" ref="employee_jod_internal_job_id"/>
|
|
||||||
<field name="recruitment_stage_ids" eval="[(4, ref('hired_stage8'))]"/>
|
|
||||||
<field name="active" eval="False"/>
|
|
||||||
</record>
|
|
||||||
</data>
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -313,13 +313,4 @@
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <template id="thank_you_template_inherit" inherit_id="hr_recruitment_exteded.thank_you_template" name="Thank You Template Extended">-->
|
|
||||||
<!-- <xpath expr="//div[@class='container mt-5 text-center']" position="inside">-->
|
|
||||||
<!-- <div t-if="applicant.post_onboarding_form_status == 'done'" style="margin-top: 20px;">-->
|
|
||||||
<!-- <a t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </xpath>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -1,165 +1,8 @@
|
||||||
from odoo import api, fields, models, _
|
from odoo import models
|
||||||
from odoo.exceptions import UserError
|
|
||||||
|
|
||||||
|
|
||||||
class HRApplicant(models.Model):
|
class HrEmployee(models.Model):
|
||||||
_inherit = 'hr.applicant'
|
_inherit = "hr.employee"
|
||||||
|
|
||||||
joining_form_link = fields.Char()
|
|
||||||
|
|
||||||
|
|
||||||
class HREmployee(models.Model):
|
|
||||||
_inherit = 'hr.employee'
|
|
||||||
|
|
||||||
applicant_id = fields.Many2one("hr.applicant")
|
|
||||||
|
|
||||||
def send_jod_form_to_employee(self):
|
def send_jod_form_to_employee(self):
|
||||||
for rec in self:
|
return super().send_jod_form_to_employee()
|
||||||
if not rec.applicant_id:
|
|
||||||
application = self.env['hr.applicant'].sudo().search(['|','|',('partner_phone','=',rec.work_phone),('email_from','=',rec.work_email),('employee_id', '=', rec.id),'|',('company_id','=',False),('company_id','=',self.env.company.id)], limit=1)
|
|
||||||
if application and self.env['hr.employee'].sudo().search([('applicant_id','=', application.id)]):
|
|
||||||
application = False
|
|
||||||
if not application:
|
|
||||||
candidate = self.env['hr.candidate'].sudo().create({
|
|
||||||
'partner_name': rec.name,
|
|
||||||
'email_from': rec.work_email,
|
|
||||||
'partner_phone': rec.work_phone,
|
|
||||||
'employee_id': rec.id,
|
|
||||||
'company_id': self.env.company.id
|
|
||||||
})
|
|
||||||
|
|
||||||
application = self.env['hr.applicant'].sudo().create({
|
|
||||||
'candidate_id': candidate.id,
|
|
||||||
'hr_job_recruitment': self.env.ref('employee_jod.employee_jod_internal_job_recruitment_id').id,
|
|
||||||
'recruitment_stage_id': self.env.ref('employee_jod.hired_stage8').id,
|
|
||||||
'company_id': self.env.company.id
|
|
||||||
})
|
|
||||||
rec.applicant_id = application.id
|
|
||||||
return rec.sudo().applicant_id.send_jod_form_to_employee()
|
|
||||||
|
|
||||||
|
|
||||||
class PostOnboardingAttachmentWizard(models.TransientModel):
|
|
||||||
_inherit = 'post.onboarding.attachment.wizard'
|
|
||||||
|
|
||||||
send_mail = fields.Boolean(default=False)
|
|
||||||
|
|
||||||
@api.onchange('template_id')
|
|
||||||
def _onchange_template_id(self):
|
|
||||||
""" Update the email body and recipients based on the selected template. """
|
|
||||||
if self.template_id:
|
|
||||||
record_id = self.env.context.get('active_id')
|
|
||||||
model = self.env.context.get('active_model')
|
|
||||||
if model == 'applicant.request.forms':
|
|
||||||
applicant = self.env['hr.applicant'].browse(record_id)
|
|
||||||
elif model == 'hr.applicant':
|
|
||||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
|
||||||
else:
|
|
||||||
if model == 'hr.employee':
|
|
||||||
applicant = self.env['hr.employee'].browse(record_id).applicant_id
|
|
||||||
|
|
||||||
if applicant:
|
|
||||||
record = self.env[self.template_id.model].sudo().browse(applicant.id)
|
|
||||||
|
|
||||||
if not record.exists():
|
|
||||||
raise UserError("The record does not exist or is not accessible.")
|
|
||||||
|
|
||||||
# Fetch email template
|
|
||||||
email_template = self.env['mail.template'].sudo().browse(self.template_id.id)
|
|
||||||
|
|
||||||
if not email_template:
|
|
||||||
raise UserError("Email template not found.")
|
|
||||||
|
|
||||||
self.email_from = self.env.company.email
|
|
||||||
self.email_to = applicant.email_from
|
|
||||||
self.email_body = email_template.body_html # Assign the rendered email bodyc
|
|
||||||
self.email_subject = email_template.subject
|
|
||||||
|
|
||||||
def action_confirm(self):
|
|
||||||
for rec in self:
|
|
||||||
self.ensure_one()
|
|
||||||
context = self.env.context
|
|
||||||
active_id = context.get('active_id')
|
|
||||||
model = context.get('active_model')
|
|
||||||
request_token = False
|
|
||||||
request_upload_url = False
|
|
||||||
|
|
||||||
if model == 'applicant.request.forms':
|
|
||||||
applicant = self.env['hr.applicant'].browse(active_id)
|
|
||||||
elif model == 'hr.applicant':
|
|
||||||
applicant = self.env['hr.applicant'].browse(context.get('active_id'))
|
|
||||||
elif model == 'hr.employee':
|
|
||||||
applicant = self.env['hr.employee'].browse(active_id).applicant_id
|
|
||||||
else:
|
|
||||||
applicant = self.env['hr.applicant'].browse(active_id)
|
|
||||||
|
|
||||||
if rec.is_pre_onboarding_attachment_request and not rec.request_form_id:
|
|
||||||
raise UserError("A document request form is required before sending this email.")
|
|
||||||
|
|
||||||
if rec.request_form_id:
|
|
||||||
request_token = rec.request_form_id._issue_new_access_token()
|
|
||||||
base_url = self.get_base_url()
|
|
||||||
request_upload_url = (
|
|
||||||
f"{base_url}/FTPROTECH/DocRequests/"
|
|
||||||
f"{applicant.id}/{rec.request_form_id.id}?token={request_token}"
|
|
||||||
)
|
|
||||||
|
|
||||||
applicant.recruitment_attachments = [(4, attachment.id) for attachment in rec.req_attachment_ids]
|
|
||||||
|
|
||||||
template = rec.template_id
|
|
||||||
|
|
||||||
personal_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'personal').mapped('name')
|
|
||||||
education_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'education').mapped('name')
|
|
||||||
previous_employer_docs = rec.req_attachment_ids.filtered(
|
|
||||||
lambda a: a.attachment_type == 'previous_employer').mapped('name')
|
|
||||||
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
|
||||||
|
|
||||||
email_context = {
|
|
||||||
'applicant_request_form_id': rec.request_form_id.id,
|
|
||||||
'applicant_request_form_token': request_token,
|
|
||||||
'applicant_request_form_url': request_upload_url,
|
|
||||||
'personal_docs': personal_docs,
|
|
||||||
'education_docs': education_docs,
|
|
||||||
'previous_employer_docs': previous_employer_docs,
|
|
||||||
'other_docs': other_docs,
|
|
||||||
}
|
|
||||||
rendered_subject = template.with_context(**email_context)._render_field(
|
|
||||||
'subject', [applicant.id]
|
|
||||||
)[applicant.id]
|
|
||||||
rendered_body_html = template.with_context(**email_context)._render_field(
|
|
||||||
'body_html', [applicant.id], compute_lang=True
|
|
||||||
)[applicant.id]
|
|
||||||
email_values = {
|
|
||||||
'email_from': rec.email_from,
|
|
||||||
'email_to': rec.email_to,
|
|
||||||
'email_cc': rec.email_cc,
|
|
||||||
'subject': rendered_subject or rec.email_subject,
|
|
||||||
'body_html': rendered_body_html,
|
|
||||||
'attachment_ids': [(6, 0, rec.attachment_ids.ids)],
|
|
||||||
}
|
|
||||||
if rec.send_mail:
|
|
||||||
template.sudo().with_context(default_body_html=rec.email_body,
|
|
||||||
**email_context).send_mail(applicant.id, email_values=email_values,
|
|
||||||
force_send=True)
|
|
||||||
base_url = self.get_base_url()
|
|
||||||
|
|
||||||
if rec.is_pre_onboarding_attachment_request:
|
|
||||||
rec.request_form_id.status = 'email_sent_to_candidate'
|
|
||||||
else:
|
|
||||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
|
||||||
applicant.joining_form_link = '%s/SRIVYNPLATFORMS/JoiningForm/%s'%(base_url,applicant.id)
|
|
||||||
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_url(self):
|
|
||||||
""" Return rooturl for a specific record.
|
|
||||||
|
|
||||||
By default, it returns the ir.config.parameter of base_url
|
|
||||||
but it can be overridden by model.
|
|
||||||
|
|
||||||
:return: the base url for this record
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
if len(self) > 1:
|
|
||||||
raise ValueError("Expected singleton or no record: %s" % self)
|
|
||||||
return self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<record id="hr_employee_view_form_applicant_id" model="ir.ui.view">
|
|
||||||
<field name="name">hr.employee.view.form.applicant.id</field>
|
|
||||||
<field name="model">hr.employee</field>
|
|
||||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//label[@for='user_id']" position="before">
|
|
||||||
<field name="applicant_id" string="Application ID" />
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
|
|
||||||
<record id="view_post_onboarding_attachment_wizard_form_inherit" model="ir.ui.view">
|
|
||||||
<field name="name">post.onboarding.attachment.wizard.form.inherit</field>
|
|
||||||
<field name="model">post.onboarding.attachment.wizard</field>
|
|
||||||
<field name="inherit_id" ref="hr_recruitment_extended.view_post_onboarding_attachment_wizard_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//field[@name='req_attachment_ids']" position="after">
|
|
||||||
<field name="send_mail"/>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//notebook" position="attributes">
|
|
||||||
<attribute name="invisible">not send_mail</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//button[@name='action_confirm']" position="attributes">
|
|
||||||
<attribute name="string">Send</attribute>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
|
|
||||||
<record id="hr_applicant_view_form_inherit_extend" model="ir.ui.view">
|
|
||||||
<field name="name">hr.applicant.view.form.extend</field>
|
|
||||||
<field name="model">hr.applicant</field>
|
|
||||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//field[@name='candidate_id']" position="after">
|
|
||||||
<field name="joining_form_link" force_save="1" readonly="1"/>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from odoo import api, fields, models, tools, _
|
from odoo import api, fields, models, tools, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
class AttendanceAnalytics(models.Model):
|
class AttendanceAnalytics(models.Model):
|
||||||
|
|
@ -64,6 +65,8 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
('absent', 'Absent'),
|
('absent', 'Absent'),
|
||||||
|
|
||||||
|
('invalid_attendance', 'Invalid Attendance'),
|
||||||
|
|
||||||
('half_day', 'Half Day'),
|
('half_day', 'Half Day'),
|
||||||
|
|
||||||
('late_in', 'Late In'),
|
('late_in', 'Late In'),
|
||||||
|
|
@ -192,7 +195,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.status == 'present':
|
if rec.status == 'present':
|
||||||
rec.color = 10
|
rec.color = 10
|
||||||
elif rec.status == 'absent':
|
elif rec.status in ('absent', 'invalid_attendance'):
|
||||||
rec.color = 1
|
rec.color = 1
|
||||||
elif rec.status == 'half_day':
|
elif rec.status == 'half_day':
|
||||||
rec.color = 2
|
rec.color = 2
|
||||||
|
|
@ -280,6 +283,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
def action_create_shiftswap_request(self):
|
def action_create_shiftswap_request(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
|
if self.date and self.date < fields.Date.context_today(self):
|
||||||
|
raise UserError(
|
||||||
|
_("Shift Swap cannot be requested for past dates.")
|
||||||
|
)
|
||||||
|
|
||||||
request = self.env['shift.swap.request'].search([
|
request = self.env['shift.swap.request'].search([
|
||||||
('employee_id', '=', self.employee_id.id),
|
('employee_id', '=', self.employee_id.id),
|
||||||
('roster_date', '=', self.date),
|
('roster_date', '=', self.date),
|
||||||
|
|
@ -324,6 +332,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
emp.department_id,
|
emp.department_id,
|
||||||
emp.resource_calendar_id,
|
emp.resource_calendar_id,
|
||||||
emp.attendance_mode,
|
emp.attendance_mode,
|
||||||
|
emp.work_mode,
|
||||||
generate_series(
|
generate_series(
|
||||||
DATE(emp.create_date),
|
DATE(emp.create_date),
|
||||||
CURRENT_DATE,
|
CURRENT_DATE,
|
||||||
|
|
@ -340,7 +349,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
att.employee_id,
|
att.employee_id,
|
||||||
|
|
||||||
DATE(att.check_in)
|
DATE(
|
||||||
|
att.check_in
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
)
|
||||||
AS attendance_date,
|
AS attendance_date,
|
||||||
|
|
||||||
MIN(att.check_in)
|
MIN(att.check_in)
|
||||||
|
|
@ -349,6 +362,18 @@ class AttendanceAnalytics(models.Model):
|
||||||
MAX(att.check_out)
|
MAX(att.check_out)
|
||||||
AS max_check_out,
|
AS max_check_out,
|
||||||
|
|
||||||
|
MIN(
|
||||||
|
att.check_in
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
) AS min_check_in_local,
|
||||||
|
|
||||||
|
MAX(
|
||||||
|
att.check_out
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
) AS max_check_out_local,
|
||||||
|
|
||||||
SUM(att.worked_hours)
|
SUM(att.worked_hours)
|
||||||
AS worked_hours
|
AS worked_hours
|
||||||
|
|
||||||
|
|
@ -358,7 +383,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
att.employee_id,
|
att.employee_id,
|
||||||
|
|
||||||
DATE(att.check_in)
|
DATE(
|
||||||
|
att.check_in
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
)
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -431,6 +460,32 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHERE rl.date_from IS NOT NULL
|
WHERE rl.date_from IS NOT NULL
|
||||||
AND rl.date_to IS NOT NULL
|
AND rl.date_to IS NOT NULL
|
||||||
AND rl.resource_id IS NULL
|
AND rl.resource_id IS NULL
|
||||||
|
),
|
||||||
|
|
||||||
|
calendar_day_schedule AS (
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
|
||||||
|
rca.calendar_id,
|
||||||
|
|
||||||
|
rca.dayofweek,
|
||||||
|
|
||||||
|
MIN(rca.hour_from) * 60
|
||||||
|
AS shift_start_minutes,
|
||||||
|
|
||||||
|
MAX(rca.hour_to) * 60
|
||||||
|
AS shift_end_minutes,
|
||||||
|
|
||||||
|
SUM(rca.hour_to - rca.hour_from)
|
||||||
|
AS hours_per_day
|
||||||
|
|
||||||
|
FROM resource_calendar_attendance rca
|
||||||
|
|
||||||
|
GROUP BY
|
||||||
|
|
||||||
|
rca.calendar_id,
|
||||||
|
|
||||||
|
rca.dayofweek
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -438,6 +493,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
ed.employee_id,
|
ed.employee_id,
|
||||||
ed.department_id,
|
ed.department_id,
|
||||||
ed.attendance_mode AS attendance_mode,
|
ed.attendance_mode AS attendance_mode,
|
||||||
|
ed.work_mode,
|
||||||
rc.id AS shift_id,
|
rc.id AS shift_id,
|
||||||
rc.name AS shift_name,
|
rc.name AS shift_name,
|
||||||
ed.date,
|
ed.date,
|
||||||
|
|
@ -448,20 +504,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
ats.worked_hours,
|
ats.worked_hours,
|
||||||
0
|
0
|
||||||
) AS worked_hours,
|
) AS worked_hours,
|
||||||
rc.hours_per_day AS hours_per_day,
|
es.hours_per_day AS hours_per_day,
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.hours_per_day
|
es.hours_per_day
|
||||||
+
|
+
|
||||||
COALESCE(rc.over_time_hrs, 0)
|
COALESCE(rc.over_time_hrs, 0)
|
||||||
) AS allowed_ot_limit,
|
) AS allowed_ot_limit,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
|
|
||||||
WHEN ats.worked_hours > rc.hours_per_day
|
WHEN ats.worked_hours > es.hours_per_day
|
||||||
|
|
||||||
THEN
|
THEN
|
||||||
ats.worked_hours - rc.hours_per_day
|
ats.worked_hours - es.hours_per_day
|
||||||
|
|
||||||
ELSE 0
|
ELSE 0
|
||||||
|
|
||||||
|
|
@ -472,7 +528,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHEN ats.worked_hours >
|
WHEN ats.worked_hours >
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.hours_per_day
|
es.hours_per_day
|
||||||
+
|
+
|
||||||
COALESCE(rc.over_time_hrs, 0)
|
COALESCE(rc.over_time_hrs, 0)
|
||||||
)
|
)
|
||||||
|
|
@ -525,19 +581,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
AS department_grace_period,
|
AS department_grace_period,
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
) AS expected_check_in,
|
) AS expected_check_in,
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
) AS expected_check_out,
|
) AS expected_check_out,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
|
|
@ -549,28 +597,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR FROM ats.min_check_in
|
HOUR FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -589,28 +629,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR FROM ats.min_check_in
|
HOUR FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
THEN TRUE
|
THEN TRUE
|
||||||
|
|
@ -622,7 +654,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -638,28 +670,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -669,7 +693,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
END
|
END
|
||||||
|
|
||||||
) AS required_checkout_time,
|
) / 60.0 AS required_checkout_time,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
|
|
||||||
|
|
@ -681,7 +705,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -697,29 +721,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time
|
|
||||||
* 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -737,13 +752,13 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -765,13 +780,13 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -780,7 +795,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -796,29 +811,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time
|
|
||||||
* 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -840,7 +846,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHEN
|
WHEN
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -856,29 +862,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time
|
|
||||||
* 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -895,13 +892,13 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -930,35 +927,30 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHEN ats.min_check_in IS NULL
|
WHEN ats.min_check_in IS NULL
|
||||||
THEN 'absent'
|
THEN 'absent'
|
||||||
|
|
||||||
WHEN ats.worked_hours < (rc.hours_per_day / 2.0)
|
WHEN ats.worked_hours < 1.0
|
||||||
|
THEN 'invalid_attendance'
|
||||||
|
|
||||||
|
WHEN ats.worked_hours < (es.hours_per_day / 2.0)
|
||||||
THEN 'half_day'
|
THEN 'half_day'
|
||||||
|
|
||||||
WHEN
|
WHEN
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR FROM ats.min_check_in
|
HOUR FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
THEN 'late_in'
|
THEN 'late_in'
|
||||||
|
|
@ -968,20 +960,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
<
|
<
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
THEN 'early_out'
|
THEN 'early_out'
|
||||||
|
|
@ -1024,6 +1016,36 @@ class AttendanceAnalytics(models.Model):
|
||||||
LEFT JOIN resource_calendar rc
|
LEFT JOIN resource_calendar rc
|
||||||
ON rc.id = ed.resource_calendar_id
|
ON rc.id = ed.resource_calendar_id
|
||||||
|
|
||||||
|
LEFT JOIN calendar_day_schedule cds
|
||||||
|
ON cds.calendar_id = ed.resource_calendar_id
|
||||||
|
AND cds.dayofweek = (
|
||||||
|
(
|
||||||
|
(
|
||||||
|
EXTRACT(DOW FROM ed.date)::integer
|
||||||
|
+ 6
|
||||||
|
) % 7
|
||||||
|
)::text
|
||||||
|
)
|
||||||
|
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(rc.shift_start_time * 60, 0),
|
||||||
|
cds.shift_start_minutes,
|
||||||
|
0
|
||||||
|
) AS shift_start_minutes,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(rc.shift_end_time * 60, 0),
|
||||||
|
cds.shift_end_minutes,
|
||||||
|
0
|
||||||
|
) AS shift_end_minutes,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(rc.hours_per_day, 0),
|
||||||
|
cds.hours_per_day,
|
||||||
|
0
|
||||||
|
) AS hours_per_day
|
||||||
|
) es ON TRUE
|
||||||
|
|
||||||
LEFT JOIN department_grace dg
|
LEFT JOIN department_grace dg
|
||||||
ON dg.calendar_id
|
ON dg.calendar_id
|
||||||
= ed.resource_calendar_id
|
= ed.resource_calendar_id
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
<field name="name">attendance.analytics.list</field>
|
<field name="name">attendance.analytics.list</field>
|
||||||
<field name="model">attendance.analytics</field>
|
<field name="model">attendance.analytics</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list string="Attendance Analytics">
|
<list string="Attendance Analytics"
|
||||||
|
decoration-danger="status == 'absent' or status == 'invalid_attendance'">
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="department_id"/>
|
<field name="department_id"/>
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
<field name="status"
|
<field name="status"
|
||||||
widget="badge"
|
widget="badge"
|
||||||
decoration-success="status == 'present'"
|
decoration-success="status == 'present'"
|
||||||
decoration-danger="status == 'absent'"
|
decoration-danger="status == 'absent' or status == 'invalid_attendance'"
|
||||||
decoration-warning="status == 'late_in'"
|
decoration-warning="status == 'late_in'"
|
||||||
decoration-info="status == 'half_day'"
|
decoration-info="status == 'half_day'"
|
||||||
decoration-primary="status == 'holiday'"/>
|
decoration-primary="status == 'holiday'"/>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,24 @@
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<template id="report_payslip">
|
<template id="report_payslip">
|
||||||
<t t-call="web.external_layout">
|
<t t-call="web.external_layout">
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h2 id="payslip_name"><span t-field="o.name">August 2023 Payslip</span></h2>
|
<div class="row mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<img t-if="o.company_id.logo"
|
||||||
|
t-att-src="image_data_uri(o.company_id.logo)"
|
||||||
|
style="max-height:80px; max-width:220px;"
|
||||||
|
alt="Company Logo"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 id="payslip_name">
|
||||||
|
<span t-field="o.name">August 2023 Payslip</span>
|
||||||
|
</h2>
|
||||||
<t t-set="is_invalid" t-value="o._is_invalid()"/>
|
<t t-set="is_invalid" t-value="o._is_invalid()"/>
|
||||||
<div t-if="is_invalid">
|
<div t-if="is_invalid">
|
||||||
<strong id="invalid_warning"><span t-out="is_invalid">This payslip is not validated. This is not a legal document.</span></strong>
|
<strong id="invalid_warning">
|
||||||
|
<span t-out="is_invalid">This payslip is not validated. This is not a legal document.</span>
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div t-else="">
|
<div t-else="">
|
||||||
<div class="oe_structure"></div>
|
<div class="oe_structure"></div>
|
||||||
|
|
@ -28,7 +40,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="employee_id">
|
<div id="employee_id">
|
||||||
<strong class="me-2">ID:</strong>
|
<strong class="me-2">ID:</strong>
|
||||||
<span t-if="o.employee_id.identification_id" t-field="o.employee_id.identification_id"/>
|
<span t-if="o.employee_id.employee_id" t-field="o.employee_id.identification_id"/>
|
||||||
<span t-else="" style="color:#875A7B" class="fw-bold">No ID number on the employee !!!</span>
|
<span t-else="" style="color:#875A7B" class="fw-bold">No ID number on the employee !!!</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="employee_email" t-if="o.employee_id.work_email">
|
<div id="employee_email" t-if="o.employee_id.work_email">
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<separator string="Employees Selection"/>
|
<separator string="Employees Selection"/>
|
||||||
<div class="o_row ms-2">
|
<div class="o_row ms-2">
|
||||||
<group>
|
<group>
|
||||||
<field name="department_id" class="w-75"
|
<field name="department_id" class="w-75" placeholder="All"
|
||||||
help="Set a specific department if you wish to select all the employees from this department (and subdepartments) at once."/>
|
help="Set a specific department if you wish to select all the employees from this department (and subdepartments) at once."/>
|
||||||
<field name="job_id" class="w-75" invisible="not department_id" domain="[('department_id', 'child_of', department_id)]"
|
<field name="job_id" class="w-75" invisible="not department_id" domain="[('department_id', 'child_of', department_id)]"
|
||||||
help="Set a specific job if you wish to select all the employees from this job at once."/>
|
help="Set a specific job if you wish to select all the employees from this job at once."/>
|
||||||
|
|
@ -95,4 +95,27 @@
|
||||||
action = env['hr.payslip.employees'].create({}).compute_sheet()
|
action = env['hr.payslip.employees'].create({}).compute_sheet()
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record id="hr_hr_employee_view_form3_inherit_salary_contract" model="ir.ui.view">
|
||||||
|
<field name="name">hr.hr.employee.view.form3.inherit.salary.contract</field>
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
<field name="inherit_id" ref="hr_contract.hr_hr_employee_view_form3"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<xpath expr="//button[@name='action_open_contract']" position="replace">
|
||||||
|
<button name="action_open_contract"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-book"
|
||||||
|
string="Employee Salary"
|
||||||
|
groups="hr_contract.group_hr_contract_manager"
|
||||||
|
context="{
|
||||||
|
'default_employee_id': id,
|
||||||
|
'default_resource_calendar_id': resource_calendar_id.id or False,
|
||||||
|
'from_action_open_contract': True
|
||||||
|
}"
|
||||||
|
invisible="employee_type not in ['employee', 'student', 'trainee']"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<menuitem
|
<menuitem
|
||||||
id="hr_payroll.menu_hr_payroll_employees_root"
|
id="hr_payroll.menu_hr_payroll_employees_root"
|
||||||
name="Contracts"
|
name="Employee Salary Contract"
|
||||||
parent="hr_payroll.menu_hr_payroll_root"
|
parent="hr_payroll.menu_hr_payroll_root"
|
||||||
sequence="1"
|
sequence="1"
|
||||||
action="hr_contract.action_hr_contract"
|
action="hr_contract.action_hr_contract"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_hr_recruitment_auto_doc_wizard,hr.recruitment.auto.doc.wizard,model_hr_recruitment_auto_doc_wizard,base.group_user,1,1,1,1
|
access_hr_recruitment_auto_doc_wizard,hr.recruitment.auto.doc.wizard,model_hr_recruitment_auto_doc_wizard,base.group_user,1,1,1,1
|
||||||
access_hr_recruitment_auto_doc_wizard_line,hr.recruitment.auto.doc.wizard.line,model_hr_recruitment_auto_doc_wizard_line,base.group_user,1,1,1,1
|
access_hr_recruitment_auto_doc_wizard_line,hr.recruitment.auto.doc.wizard.line,model_hr_recruitment_auto_doc_wizard_line,base.group_user,1,1,1,1
|
||||||
|
access_hr_recruitment_auto_doc_education_line,hr.recruitment.auto.doc.education.line,model_hr_recruitment_auto_doc_education_line,base.group_user,1,1,1,1
|
||||||
|
access_hr_recruitment_auto_doc_employer_line,hr.recruitment.auto.doc.employer.line,model_hr_recruitment_auto_doc_employer_line,base.group_user,1,1,1,1
|
||||||
|
access_hr_recruitment_auto_doc_family_line,hr.recruitment.auto.doc.family.line,model_hr_recruitment_auto_doc_family_line,base.group_user,1,1,1,1
|
||||||
|
access_hr_recruitment_auto_doc_certification_line,hr.recruitment.auto.doc.certification.line,model_hr_recruitment_auto_doc_certification_line,base.group_user,1,1,1,1
|
||||||
|
access_hr_recruitment_auto_doc_project_line,hr.recruitment.auto.doc.project.line,model_hr_recruitment_auto_doc_project_line,base.group_user,1,1,1,1
|
||||||
|
access_hr_recruitment_auto_doc_other_line,hr.recruitment.auto.doc.other.line,model_hr_recruitment_auto_doc_other_line,base.group_user,1,1,1,1
|
||||||
|
|
|
||||||
|
|
|
@ -152,7 +152,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
parsed_data = self._post_process_jd_data(parsed_data, parsed_payload["text"])
|
parsed_data = self._post_process_jd_data(parsed_data, parsed_payload["text"])
|
||||||
else:
|
else:
|
||||||
parsed_data = self._post_process_resume_data(parsed_data, parsed_payload["text"], line.file_name)
|
parsed_data = self._post_process_resume_data(parsed_data, parsed_payload["text"], line.file_name)
|
||||||
line.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
line.write(dict(
|
||||||
|
line._prepare_editable_vals(parsed_data),
|
||||||
|
extracted_payload=json.dumps(parsed_data, indent=2, ensure_ascii=False),
|
||||||
|
))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processed += 1
|
processed += 1
|
||||||
|
|
@ -255,6 +258,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed_data = json.loads(line.extracted_payload)
|
parsed_data = json.loads(line.extracted_payload)
|
||||||
|
parsed_data = line._get_edited_parsed_data(parsed_data)
|
||||||
|
|
||||||
with self.env.cr.savepoint():
|
with self.env.cr.savepoint():
|
||||||
|
|
||||||
|
|
@ -480,6 +484,17 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"relation_type (father/mother/spouse/kid1/kid2), name, contact_no, dob, and location."
|
"relation_type (father/mother/spouse/kid1/kid2), name, contact_no, dob, and location."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"certifications": {
|
||||||
|
"type": "list",
|
||||||
|
"description": "List of explicit certifications, courses, training, or licenses if present.",
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"type": "list",
|
||||||
|
"description": (
|
||||||
|
"List of explicit resume projects if present. Each item should include project_name, role, "
|
||||||
|
"technologies, duration, and description when available."
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_jd_required_fields(self):
|
def _get_jd_required_fields(self):
|
||||||
|
|
@ -510,6 +525,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"Normalize skills into clean individual names. "
|
"Normalize skills into clean individual names. "
|
||||||
"For experience values, return numeric years when clearly inferable. "
|
"For experience values, return numeric years when clearly inferable. "
|
||||||
"If the resume contains education details, previous employer details, or family details, return them as structured arrays of objects using the requested field names. "
|
"If the resume contains education details, previous employer details, or family details, return them as structured arrays of objects using the requested field names. "
|
||||||
|
"Read the complete resume including tables, side columns, and later pages before returning JSON. "
|
||||||
|
"education_history must include every school, intermediate, diploma, graduation, post graduation, or additional qualification row that is explicitly present. "
|
||||||
|
"employer_history must include internships, current employer, previous employers, project/company experience, and work periods when explicitly present. "
|
||||||
|
"family_details must include every explicitly mentioned family member and must be [] if the resume has no family section. "
|
||||||
"For each employer entry, extract the role-specific work description into work_description. "
|
"For each employer entry, extract the role-specific work description into work_description. "
|
||||||
"Only include entries that are explicitly present in the document. "
|
"Only include entries that are explicitly present in the document. "
|
||||||
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
|
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
|
||||||
|
|
@ -642,10 +661,13 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
data["email"] = email_match.group(1)
|
data["email"] = email_match.group(1)
|
||||||
|
|
||||||
phone_matches = re.findall(r"(\+?\d[\d\-\s()]{7,}\d)", extracted_text)
|
phone_matches = re.findall(r"(\+?\d[\d\-\s()]{7,}\d)", extracted_text)
|
||||||
if phone_matches and not data.get("phone"):
|
phone_values = []
|
||||||
data["phone"] = phone_matches[0].strip()
|
for phone_candidate in [data.get("phone"), data.get("alternate_phone")] + phone_matches:
|
||||||
if len(phone_matches) > 1 and not data.get("alternate_phone"):
|
phone_value = self._clean_resume_phone_value(phone_candidate)
|
||||||
data["alternate_phone"] = phone_matches[1].strip()
|
if phone_value and phone_value not in phone_values:
|
||||||
|
phone_values.append(phone_value)
|
||||||
|
data["phone"] = phone_values[0] if phone_values else False
|
||||||
|
data["alternate_phone"] = phone_values[1] if len(phone_values) > 1 else False
|
||||||
|
|
||||||
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", extracted_text, re.I)
|
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", extracted_text, re.I)
|
||||||
if linkedin_match:
|
if linkedin_match:
|
||||||
|
|
@ -654,11 +676,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
if not data.get("total_experience_years"):
|
if not data.get("total_experience_years"):
|
||||||
data["total_experience_years"] = self._guess_total_experience(extracted_text)
|
data["total_experience_years"] = self._guess_total_experience(extracted_text)
|
||||||
|
|
||||||
|
resume_skills = self._normalize_resume_skill_values(data.get("skills") or [], extracted_text)
|
||||||
data["skills"] = self.env[
|
data["skills"] = self.env[
|
||||||
"document.parser.service"
|
"document.parser.service"
|
||||||
].validate_explicit_skills(
|
].validate_explicit_skills(
|
||||||
extracted_text,
|
extracted_text,
|
||||||
data.get("skills") or []
|
resume_skills
|
||||||
)
|
)
|
||||||
data["education_history"] = self._normalize_resume_list(data.get("education_history"))
|
data["education_history"] = self._normalize_resume_list(data.get("education_history"))
|
||||||
data["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
|
data["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
|
||||||
|
|
@ -1431,8 +1454,57 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
normalized = re.sub(r"[^\d+]", "", value)
|
normalized = re.sub(r"[^\d+]", "", value)
|
||||||
return normalized or False
|
return normalized or False
|
||||||
|
|
||||||
|
def _clean_resume_phone_value(self, value):
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
raw_value = str(value).strip()
|
||||||
|
if re.fullmatch(r"(?:19|20)\d{2}\s*[-/]\s*(?:19|20)\d{2}", raw_value):
|
||||||
|
return False
|
||||||
|
normalized = self._normalize_phone(raw_value)
|
||||||
|
digits = re.sub(r"\D", "", normalized or "")
|
||||||
|
if len(digits) < 10 or len(digits) > 15:
|
||||||
|
return False
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
def _normalize_resume_skill_values(self, skills, extracted_text):
|
||||||
|
skill_parts = []
|
||||||
|
for skill in skills:
|
||||||
|
for skill_part in self._split_resume_skill_value(skill):
|
||||||
|
normalized = self._normalize_skill_name(skill_part)
|
||||||
|
if normalized and self._is_valid_resume_skill(normalized, extracted_text):
|
||||||
|
skill_parts.append(normalized)
|
||||||
|
normalized_skills = []
|
||||||
|
index = 0
|
||||||
|
while index < len(skill_parts):
|
||||||
|
current_skill = skill_parts[index]
|
||||||
|
next_skill = skill_parts[index + 1] if index + 1 < len(skill_parts) else False
|
||||||
|
combined_skill = "%s %s" % (current_skill, next_skill) if next_skill else False
|
||||||
|
if combined_skill and self._is_valid_resume_skill(combined_skill, extracted_text):
|
||||||
|
normalized_skills.append(combined_skill)
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
normalized_skills.append(current_skill)
|
||||||
|
index += 1
|
||||||
|
return self._deduplicate_skill_names(normalized_skills)
|
||||||
|
|
||||||
|
def _split_resume_skill_value(self, value):
|
||||||
|
value = re.sub(r"^[\s\-*\u2022•]+", "", str(value or "")).strip()
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
if ":" in value:
|
||||||
|
label, value = [item.strip() for item in value.split(":", 1)]
|
||||||
|
if not re.search(r"(programming|frameworks?|databases?|analytics?|cloud|devops|tools?|technologies|skills?)", label, re.I):
|
||||||
|
value = "%s %s" % (label, value)
|
||||||
|
value = re.sub(r"\((?:advanced|intermediate|beginner|expert)\)", "", value, flags=re.I)
|
||||||
|
return [item for item in re.split(r"[,;|/]", value) if item.strip()]
|
||||||
|
|
||||||
def _normalize_skill_name(self, value):
|
def _normalize_skill_name(self, value):
|
||||||
value = re.sub(r"\s+", " ", (value or "")).strip(" -,:;")
|
value = re.sub(r"^[\s\-*\u2022•]+", "", value or "")
|
||||||
|
value = re.sub(r"\s+", " ", value).strip(" -,:;()")
|
||||||
|
if ":" in value:
|
||||||
|
label, skill_value = [item.strip() for item in value.split(":", 1)]
|
||||||
|
if re.search(r"(programming|frameworks?|databases?|analytics?|cloud|devops|tools?|technologies|skills?)", label, re.I):
|
||||||
|
value = skill_value
|
||||||
value = re.sub(r"^[0-9.)\-(\s]+", "", value).strip()
|
value = re.sub(r"^[0-9.)\-(\s]+", "", value).strip()
|
||||||
if not value:
|
if not value:
|
||||||
return False
|
return False
|
||||||
|
|
@ -1834,8 +1906,9 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"target_to": self._parse_date_value(parsed_data.get("end_date")),
|
"target_to": self._parse_date_value(parsed_data.get("end_date")),
|
||||||
"job_category": int(job_category_id) if job_category_id and job_category_id.isdigit() else False,
|
"job_category": int(job_category_id) if job_category_id and job_category_id.isdigit() else False,
|
||||||
"address_id":False,
|
"address_id":False,
|
||||||
"recruitment_type": 'external'
|
"recruitment_type": 'external',
|
||||||
|
"requested_by": parsed_data.get("requested_by") or False,
|
||||||
|
"no_of_recruitment": parsed_data.get("no_of_positions") or 1,
|
||||||
}
|
}
|
||||||
if request_id:
|
if request_id:
|
||||||
create_vals["recruitment_sequence"] = request_id
|
create_vals["recruitment_sequence"] = request_id
|
||||||
|
|
@ -2034,13 +2107,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"<ul>",
|
"<ul>",
|
||||||
]
|
]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
html_parts.append(
|
html_parts.append("<li><strong>%s</strong>: <span class='text-%s'>%s</span>" % (
|
||||||
"<li><strong>%s</strong>: <span class='text-%s'>%s</span></li>" % (
|
|
||||||
escape(row["filename"]),
|
escape(row["filename"]),
|
||||||
row["level"],
|
row["level"],
|
||||||
escape(row["message"]),
|
escape(row["message"]),
|
||||||
)
|
))
|
||||||
)
|
html_parts.append("</li>")
|
||||||
html_parts.extend(["</ul>", "</div>"])
|
html_parts.extend(["</ul>", "</div>"])
|
||||||
return "".join(html_parts)
|
return "".join(html_parts)
|
||||||
|
|
||||||
|
|
@ -2072,3 +2144,543 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
|
||||||
extracted_payload = fields.Text(readonly=True)
|
extracted_payload = fields.Text(readonly=True)
|
||||||
candidate_id = fields.Many2one("hr.candidate", readonly=True)
|
candidate_id = fields.Many2one("hr.candidate", readonly=True)
|
||||||
applicant_id = fields.Many2one("hr.applicant", readonly=True)
|
applicant_id = fields.Many2one("hr.applicant", readonly=True)
|
||||||
|
target_model = fields.Selection(related="wizard_id.target_model", readonly=True)
|
||||||
|
|
||||||
|
full_name = fields.Char()
|
||||||
|
first_name = fields.Char()
|
||||||
|
last_name = fields.Char()
|
||||||
|
email = fields.Char()
|
||||||
|
phone = fields.Char()
|
||||||
|
alternate_phone = fields.Char()
|
||||||
|
linkedin_profile = fields.Char()
|
||||||
|
current_location = fields.Char()
|
||||||
|
current_organization = fields.Char()
|
||||||
|
total_experience_years = fields.Float()
|
||||||
|
relevant_experience_years = fields.Float()
|
||||||
|
notice_period = fields.Char()
|
||||||
|
degree = fields.Char()
|
||||||
|
skills_text = fields.Text(string="Skills")
|
||||||
|
summary = fields.Text()
|
||||||
|
education_history_json = fields.Text(string="Education History")
|
||||||
|
employer_history_json = fields.Text(string="Employer History")
|
||||||
|
family_details_json = fields.Text(string="Family Details")
|
||||||
|
other_payload_json = fields.Text(string="Other Parsed Data")
|
||||||
|
education_line_ids = fields.One2many(
|
||||||
|
"hr.recruitment.auto.doc.education.line",
|
||||||
|
"wizard_line_id",
|
||||||
|
string="Education History",
|
||||||
|
)
|
||||||
|
employer_line_ids = fields.One2many(
|
||||||
|
"hr.recruitment.auto.doc.employer.line",
|
||||||
|
"wizard_line_id",
|
||||||
|
string="Employer History",
|
||||||
|
)
|
||||||
|
family_line_ids = fields.One2many(
|
||||||
|
"hr.recruitment.auto.doc.family.line",
|
||||||
|
"wizard_line_id",
|
||||||
|
string="Family Details",
|
||||||
|
)
|
||||||
|
certification_line_ids = fields.One2many(
|
||||||
|
"hr.recruitment.auto.doc.certification.line",
|
||||||
|
"wizard_line_id",
|
||||||
|
string="Certifications",
|
||||||
|
)
|
||||||
|
project_line_ids = fields.One2many(
|
||||||
|
"hr.recruitment.auto.doc.project.line",
|
||||||
|
"wizard_line_id",
|
||||||
|
string="Projects",
|
||||||
|
)
|
||||||
|
other_line_ids = fields.One2many(
|
||||||
|
"hr.recruitment.auto.doc.other.line",
|
||||||
|
"wizard_line_id",
|
||||||
|
string="Other Parsed Data",
|
||||||
|
)
|
||||||
|
|
||||||
|
request_id = fields.Char(string="Request ID")
|
||||||
|
start_date = fields.Char()
|
||||||
|
end_date = fields.Char()
|
||||||
|
site_location = fields.Char()
|
||||||
|
job_title = fields.Char()
|
||||||
|
job_summary = fields.Text()
|
||||||
|
requirements = fields.Text()
|
||||||
|
primary_skills_text = fields.Text(string="Primary Skills")
|
||||||
|
secondary_skills_text = fields.Text(string="Secondary Skills")
|
||||||
|
budget = fields.Char()
|
||||||
|
experience_years = fields.Float()
|
||||||
|
job_category = fields.Char()
|
||||||
|
requested_by = fields.Many2one('res.partner',domain="[('contact_type', '=', 'external')]")
|
||||||
|
no_of_positions = fields.Integer('Number of Positions')
|
||||||
|
|
||||||
|
def action_save_line_changes(self):
|
||||||
|
self.ensure_one()
|
||||||
|
parsed_data = self._get_edited_parsed_data()
|
||||||
|
self.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
||||||
|
return self._action_open_parent_wizard()
|
||||||
|
|
||||||
|
def action_close_line(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return self._action_open_parent_wizard()
|
||||||
|
|
||||||
|
def _action_open_parent_wizard(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": self.wizard_id._name,
|
||||||
|
"res_id": self.wizard_id.id,
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _prepare_editable_vals(self, parsed_data):
|
||||||
|
parsed_data = parsed_data or {}
|
||||||
|
return {
|
||||||
|
"full_name": parsed_data.get("full_name"),
|
||||||
|
"first_name": parsed_data.get("first_name"),
|
||||||
|
"last_name": parsed_data.get("last_name"),
|
||||||
|
"email": parsed_data.get("email"),
|
||||||
|
"phone": parsed_data.get("phone"),
|
||||||
|
"alternate_phone": parsed_data.get("alternate_phone"),
|
||||||
|
"linkedin_profile": parsed_data.get("linkedin_profile"),
|
||||||
|
"current_location": parsed_data.get("current_location"),
|
||||||
|
"current_organization": parsed_data.get("current_organization"),
|
||||||
|
"total_experience_years": self._float_or_zero(parsed_data.get("total_experience_years")),
|
||||||
|
"relevant_experience_years": self._float_or_zero(parsed_data.get("relevant_experience_years")),
|
||||||
|
"notice_period": parsed_data.get("notice_period"),
|
||||||
|
"degree": parsed_data.get("degree"),
|
||||||
|
"skills_text": self._list_to_text(parsed_data.get("skills")),
|
||||||
|
"summary": parsed_data.get("summary"),
|
||||||
|
"education_history_json": self._json_to_text(parsed_data.get("education_history")),
|
||||||
|
"employer_history_json": self._json_to_text(parsed_data.get("employer_history")),
|
||||||
|
"family_details_json": self._json_to_text(parsed_data.get("family_details")),
|
||||||
|
"other_payload_json": self._json_to_text(self._get_other_parsed_data(parsed_data)),
|
||||||
|
"education_line_ids": self._prepare_education_line_commands(parsed_data.get("education_history")),
|
||||||
|
"employer_line_ids": self._prepare_employer_line_commands(parsed_data.get("employer_history"), parsed_data.get("projects")),
|
||||||
|
"family_line_ids": self._prepare_family_line_commands(parsed_data.get("family_details")),
|
||||||
|
"certification_line_ids": self._prepare_certification_line_commands(parsed_data.get("certifications")),
|
||||||
|
"project_line_ids": self._prepare_project_line_commands(parsed_data.get("projects")),
|
||||||
|
"other_line_ids": self._prepare_other_line_commands(self._get_other_parsed_data(parsed_data)),
|
||||||
|
"request_id": parsed_data.get("request_id"),
|
||||||
|
"start_date": parsed_data.get("start_date"),
|
||||||
|
"end_date": parsed_data.get("end_date"),
|
||||||
|
"site_location": parsed_data.get("site_location"),
|
||||||
|
"job_title": parsed_data.get("job_title"),
|
||||||
|
"job_summary": parsed_data.get("job_summary"),
|
||||||
|
"requirements": parsed_data.get("requirements"),
|
||||||
|
"primary_skills_text": self._list_to_text(parsed_data.get("primary_skills")),
|
||||||
|
"secondary_skills_text": self._list_to_text(parsed_data.get("secondary_skills")),
|
||||||
|
"budget": parsed_data.get("budget"),
|
||||||
|
"experience_years": self._float_or_zero(parsed_data.get("experience_years")),
|
||||||
|
"job_category": parsed_data.get("job_category"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_edited_parsed_data(self, base_data=None):
|
||||||
|
data = dict(base_data or {})
|
||||||
|
if self.target_model == "job_recruitment":
|
||||||
|
data.update({
|
||||||
|
"request_id": self.request_id,
|
||||||
|
"start_date": self.start_date,
|
||||||
|
"end_date": self.end_date,
|
||||||
|
"site_location": self.site_location,
|
||||||
|
"job_title": self.job_title,
|
||||||
|
"job_summary": self.job_summary,
|
||||||
|
"requirements": self.requirements,
|
||||||
|
"primary_skills": self._text_to_list(self.primary_skills_text),
|
||||||
|
"secondary_skills": self._text_to_list(self.secondary_skills_text),
|
||||||
|
"budget": self.budget,
|
||||||
|
"experience_years": self.experience_years,
|
||||||
|
"job_category": self.job_category,
|
||||||
|
"requested_by": self.requested_by.id if self.requested_by else False,
|
||||||
|
"no_of_positions": self.no_of_positions if self.no_of_positions else 1,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
data.update({
|
||||||
|
"full_name": self.full_name,
|
||||||
|
"first_name": self.first_name,
|
||||||
|
"last_name": self.last_name,
|
||||||
|
"email": self.email,
|
||||||
|
"phone": self.phone,
|
||||||
|
"alternate_phone": self.alternate_phone,
|
||||||
|
"linkedin_profile": self.linkedin_profile,
|
||||||
|
"current_location": self.current_location,
|
||||||
|
"current_organization": self.current_organization,
|
||||||
|
"total_experience_years": self.total_experience_years,
|
||||||
|
"relevant_experience_years": self.relevant_experience_years,
|
||||||
|
"notice_period": self.notice_period,
|
||||||
|
"degree": self.degree,
|
||||||
|
"skills": self._text_to_list(self.skills_text),
|
||||||
|
"summary": self.summary,
|
||||||
|
"education_history": self._get_education_history_from_lines(),
|
||||||
|
"employer_history": self._get_employer_history_from_lines(),
|
||||||
|
"family_details": self._get_family_details_from_lines(),
|
||||||
|
"certifications": self._get_certifications_from_lines(),
|
||||||
|
"projects": self._get_projects_from_lines(),
|
||||||
|
})
|
||||||
|
data.update(self._get_other_payload_from_lines())
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _prepare_education_line_commands(self, education_history):
|
||||||
|
commands = [(5, 0, 0)]
|
||||||
|
for education in self._normalize_dict_list(education_history):
|
||||||
|
commands.append((0, 0, {
|
||||||
|
"education_type": education.get("education_type"),
|
||||||
|
"specialization": education.get("specialization") or education.get("name") or education.get("degree"),
|
||||||
|
"university": education.get("university") or education.get("institution") or education.get("college"),
|
||||||
|
"start_year": education.get("start_year"),
|
||||||
|
"end_year": education.get("end_year"),
|
||||||
|
"marks_or_grade": education.get("marks_or_grade") or education.get("marks") or education.get("grade") or education.get("cgpa") or education.get("percentage"),
|
||||||
|
}))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _prepare_employer_line_commands(self, employer_history, projects=None):
|
||||||
|
commands = [(5, 0, 0)]
|
||||||
|
employers = self._normalize_shared_employer_descriptions(
|
||||||
|
self._normalize_dict_list(employer_history),
|
||||||
|
projects,
|
||||||
|
)
|
||||||
|
for employer in employers:
|
||||||
|
commands.append((0, 0, {
|
||||||
|
"company_name": employer.get("company_name") or employer.get("employer"),
|
||||||
|
"designation": employer.get("designation") or employer.get("role") or employer.get("job_title"),
|
||||||
|
"date_of_joining": employer.get("date_of_joining") or employer.get("start_date"),
|
||||||
|
"last_working_day": employer.get("last_working_day") or employer.get("end_date"),
|
||||||
|
"ctc": employer.get("ctc") or employer.get("salary"),
|
||||||
|
"work_description": employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities"),
|
||||||
|
}))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _prepare_family_line_commands(self, family_details):
|
||||||
|
commands = [(5, 0, 0)]
|
||||||
|
for member in self._normalize_dict_list(family_details):
|
||||||
|
commands.append((0, 0, {
|
||||||
|
"relation_type": member.get("relation_type") or member.get("relation"),
|
||||||
|
"name": member.get("name"),
|
||||||
|
"contact_no": member.get("contact_no") or member.get("contact") or member.get("phone"),
|
||||||
|
"dob": member.get("dob"),
|
||||||
|
"location": member.get("location") or member.get("address"),
|
||||||
|
}))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _prepare_certification_line_commands(self, certifications):
|
||||||
|
commands = [(5, 0, 0)]
|
||||||
|
for certification in self._normalize_value_list(certifications):
|
||||||
|
if isinstance(certification, dict):
|
||||||
|
commands.append((0, 0, {
|
||||||
|
"certification_name": certification.get("certification_name") or certification.get("name") or certification.get("title"),
|
||||||
|
"provider": certification.get("provider") or certification.get("issuer") or certification.get("organization"),
|
||||||
|
"completion_date": certification.get("completion_date") or certification.get("date") or certification.get("year"),
|
||||||
|
}))
|
||||||
|
else:
|
||||||
|
commands.append((0, 0, {"certification_name": str(certification)}))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _prepare_project_line_commands(self, projects):
|
||||||
|
commands = [(5, 0, 0)]
|
||||||
|
for project in self._normalize_dict_list(projects):
|
||||||
|
technologies = project.get("technologies")
|
||||||
|
commands.append((0, 0, {
|
||||||
|
"project_name": project.get("project_name") or project.get("name") or project.get("title"),
|
||||||
|
"role": project.get("role"),
|
||||||
|
"technologies": ", ".join(technologies) if isinstance(technologies, list) else technologies,
|
||||||
|
"duration": project.get("duration"),
|
||||||
|
"description": project.get("description") or project.get("summary") or project.get("work_description"),
|
||||||
|
}))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _prepare_other_line_commands(self, other_payload):
|
||||||
|
commands = [(5, 0, 0)]
|
||||||
|
for key, value in (other_payload or {}).items():
|
||||||
|
commands.append((0, 0, {
|
||||||
|
"key": key,
|
||||||
|
"value": self._json_to_text(value) or str(value),
|
||||||
|
}))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _get_education_history_from_lines(self):
|
||||||
|
return [line._to_payload() for line in self.education_line_ids if line._to_payload()]
|
||||||
|
|
||||||
|
def _get_employer_history_from_lines(self):
|
||||||
|
return [line._to_payload() for line in self.employer_line_ids if line._to_payload()]
|
||||||
|
|
||||||
|
def _get_family_details_from_lines(self):
|
||||||
|
return [line._to_payload() for line in self.family_line_ids if line._to_payload()]
|
||||||
|
|
||||||
|
def _get_certifications_from_lines(self):
|
||||||
|
return [line._to_payload() for line in self.certification_line_ids if line._to_payload()]
|
||||||
|
|
||||||
|
def _get_projects_from_lines(self):
|
||||||
|
return [line._to_payload() for line in self.project_line_ids if line._to_payload()]
|
||||||
|
|
||||||
|
def _get_other_payload_from_lines(self):
|
||||||
|
return {
|
||||||
|
line.key: line._json_value()
|
||||||
|
for line in self.other_line_ids
|
||||||
|
if line.key and line.value not in (False, None, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
def _normalize_dict_list(self, value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [item for item in value if isinstance(item, dict)]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return [value]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _normalize_value_list(self, value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [item for item in value if item not in (False, None, "", {})]
|
||||||
|
if value in (False, None, "", {}):
|
||||||
|
return []
|
||||||
|
return [value]
|
||||||
|
|
||||||
|
def _normalize_shared_employer_descriptions(self, employers, projects=None):
|
||||||
|
if len(employers) < 2:
|
||||||
|
return employers
|
||||||
|
|
||||||
|
descriptions = [
|
||||||
|
employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")
|
||||||
|
for employer in employers
|
||||||
|
]
|
||||||
|
descriptions = [description for description in descriptions if description]
|
||||||
|
if descriptions:
|
||||||
|
shared_description = max(descriptions, key=lambda item: len(str(item)))
|
||||||
|
else:
|
||||||
|
project_descriptions = []
|
||||||
|
for project in self._normalize_dict_list(projects):
|
||||||
|
description = project.get("description") or project.get("summary") or project.get("work_description")
|
||||||
|
if description:
|
||||||
|
project_descriptions.append("%s: %s" % (
|
||||||
|
project.get("project_name") or project.get("name") or project.get("title") or "Project",
|
||||||
|
description,
|
||||||
|
))
|
||||||
|
shared_description = "\n".join(project_descriptions) if project_descriptions else False
|
||||||
|
|
||||||
|
if not shared_description:
|
||||||
|
return employers
|
||||||
|
|
||||||
|
for employer in employers:
|
||||||
|
if not (employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")):
|
||||||
|
employer["work_description"] = shared_description
|
||||||
|
return employers
|
||||||
|
|
||||||
|
def _list_to_text(self, value):
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
if isinstance(value, list):
|
||||||
|
return "\n".join(str(item) for item in value if item not in (False, None, ""))
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def _text_to_list(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
return [item.strip(" -,\t") for item in str(value).splitlines() if item.strip(" -,\t")]
|
||||||
|
|
||||||
|
def _json_to_text(self, value):
|
||||||
|
if value in (False, None, "", [], {}):
|
||||||
|
return False
|
||||||
|
return json.dumps(value, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def _text_to_json_list(self, value):
|
||||||
|
parsed = self._text_to_json_value(value, [])
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
return [item for item in parsed if item not in (False, None, "", {})]
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
return [parsed]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _text_to_json_dict(self, value):
|
||||||
|
parsed = self._text_to_json_value(value, {})
|
||||||
|
return parsed if isinstance(parsed, dict) else {}
|
||||||
|
|
||||||
|
def _text_to_json_value(self, value, default):
|
||||||
|
if not value:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def _get_other_parsed_data(self, parsed_data):
|
||||||
|
known_keys = {
|
||||||
|
"full_name", "first_name", "last_name", "email", "phone", "alternate_phone",
|
||||||
|
"linkedin_profile", "current_location", "current_organization",
|
||||||
|
"total_experience_years", "relevant_experience_years", "notice_period",
|
||||||
|
"degree", "skills", "summary", "education_history", "employer_history",
|
||||||
|
"family_details", "certifications", "projects", "request_id", "start_date", "end_date", "site_location",
|
||||||
|
"job_title", "job_summary", "requirements", "primary_skills",
|
||||||
|
"secondary_skills", "budget", "experience_years", "job_category",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in (parsed_data or {}).items()
|
||||||
|
if key not in known_keys and value not in (False, None, "", [], {})
|
||||||
|
}
|
||||||
|
|
||||||
|
def _float_or_zero(self, value):
|
||||||
|
if value in (False, None, ""):
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class HrRecruitmentAutoDocEducationLine(models.TransientModel):
|
||||||
|
_name = "hr.recruitment.auto.doc.education.line"
|
||||||
|
_description = "HR Recruitment Auto Document Education Line"
|
||||||
|
_order = "id"
|
||||||
|
|
||||||
|
wizard_line_id = fields.Many2one(
|
||||||
|
"hr.recruitment.auto.doc.wizard.line",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
education_type = fields.Char()
|
||||||
|
specialization = fields.Char()
|
||||||
|
university = fields.Char()
|
||||||
|
start_year = fields.Char()
|
||||||
|
end_year = fields.Char()
|
||||||
|
marks_or_grade = fields.Char()
|
||||||
|
|
||||||
|
def _to_payload(self):
|
||||||
|
self.ensure_one()
|
||||||
|
payload = {
|
||||||
|
"education_type": self.education_type,
|
||||||
|
"specialization": self.specialization,
|
||||||
|
"university": self.university,
|
||||||
|
"start_year": self.start_year,
|
||||||
|
"end_year": self.end_year,
|
||||||
|
"marks_or_grade": self.marks_or_grade,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||||
|
|
||||||
|
|
||||||
|
class HrRecruitmentAutoDocEmployerLine(models.TransientModel):
|
||||||
|
_name = "hr.recruitment.auto.doc.employer.line"
|
||||||
|
_description = "HR Recruitment Auto Document Employer Line"
|
||||||
|
_order = "id"
|
||||||
|
|
||||||
|
wizard_line_id = fields.Many2one(
|
||||||
|
"hr.recruitment.auto.doc.wizard.line",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
company_name = fields.Char()
|
||||||
|
designation = fields.Char()
|
||||||
|
date_of_joining = fields.Char()
|
||||||
|
last_working_day = fields.Char()
|
||||||
|
ctc = fields.Char()
|
||||||
|
work_description = fields.Text()
|
||||||
|
|
||||||
|
def _to_payload(self):
|
||||||
|
self.ensure_one()
|
||||||
|
payload = {
|
||||||
|
"company_name": self.company_name,
|
||||||
|
"designation": self.designation,
|
||||||
|
"date_of_joining": self.date_of_joining,
|
||||||
|
"last_working_day": self.last_working_day,
|
||||||
|
"ctc": self.ctc,
|
||||||
|
"work_description": self.work_description,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||||
|
|
||||||
|
|
||||||
|
class HrRecruitmentAutoDocFamilyLine(models.TransientModel):
|
||||||
|
_name = "hr.recruitment.auto.doc.family.line"
|
||||||
|
_description = "HR Recruitment Auto Document Family Line"
|
||||||
|
_order = "id"
|
||||||
|
|
||||||
|
wizard_line_id = fields.Many2one(
|
||||||
|
"hr.recruitment.auto.doc.wizard.line",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
relation_type = fields.Char()
|
||||||
|
name = fields.Char()
|
||||||
|
contact_no = fields.Char()
|
||||||
|
dob = fields.Char()
|
||||||
|
location = fields.Char()
|
||||||
|
|
||||||
|
def _to_payload(self):
|
||||||
|
self.ensure_one()
|
||||||
|
payload = {
|
||||||
|
"relation_type": self.relation_type,
|
||||||
|
"name": self.name,
|
||||||
|
"contact_no": self.contact_no,
|
||||||
|
"dob": self.dob,
|
||||||
|
"location": self.location,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||||
|
|
||||||
|
|
||||||
|
class HrRecruitmentAutoDocCertificationLine(models.TransientModel):
|
||||||
|
_name = "hr.recruitment.auto.doc.certification.line"
|
||||||
|
_description = "HR Recruitment Auto Document Certification Line"
|
||||||
|
_order = "id"
|
||||||
|
|
||||||
|
wizard_line_id = fields.Many2one(
|
||||||
|
"hr.recruitment.auto.doc.wizard.line",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
certification_name = fields.Char()
|
||||||
|
provider = fields.Char()
|
||||||
|
completion_date = fields.Char()
|
||||||
|
|
||||||
|
def _to_payload(self):
|
||||||
|
self.ensure_one()
|
||||||
|
payload = {
|
||||||
|
"certification_name": self.certification_name,
|
||||||
|
"provider": self.provider,
|
||||||
|
"completion_date": self.completion_date,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||||
|
|
||||||
|
|
||||||
|
class HrRecruitmentAutoDocProjectLine(models.TransientModel):
|
||||||
|
_name = "hr.recruitment.auto.doc.project.line"
|
||||||
|
_description = "HR Recruitment Auto Document Project Line"
|
||||||
|
_order = "id"
|
||||||
|
|
||||||
|
wizard_line_id = fields.Many2one(
|
||||||
|
"hr.recruitment.auto.doc.wizard.line",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
project_name = fields.Char()
|
||||||
|
role = fields.Char()
|
||||||
|
technologies = fields.Char()
|
||||||
|
duration = fields.Char()
|
||||||
|
description = fields.Text()
|
||||||
|
|
||||||
|
def _to_payload(self):
|
||||||
|
self.ensure_one()
|
||||||
|
payload = {
|
||||||
|
"project_name": self.project_name,
|
||||||
|
"role": self.role,
|
||||||
|
"technologies": [item.strip() for item in (self.technologies or "").split(",") if item.strip()],
|
||||||
|
"duration": self.duration,
|
||||||
|
"description": self.description,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in payload.items() if value not in (False, None, "", [])}
|
||||||
|
|
||||||
|
|
||||||
|
class HrRecruitmentAutoDocOtherLine(models.TransientModel):
|
||||||
|
_name = "hr.recruitment.auto.doc.other.line"
|
||||||
|
_description = "HR Recruitment Auto Document Other Parsed Data Line"
|
||||||
|
_order = "id"
|
||||||
|
|
||||||
|
wizard_line_id = fields.Many2one(
|
||||||
|
"hr.recruitment.auto.doc.wizard.line",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
key = fields.Char(required=True)
|
||||||
|
value = fields.Text()
|
||||||
|
|
||||||
|
def _json_value(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.value:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return json.loads(self.value)
|
||||||
|
except Exception:
|
||||||
|
return self.value
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,147 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<group string="Uploaded Files" invisible="not line_ids">
|
<group string="Uploaded Files" invisible="not line_ids">
|
||||||
<field name="line_ids" nolabel="1" readonly="1">
|
<field name="line_ids" nolabel="1" create="0" delete="0">
|
||||||
|
<form string="Parsed Document" create="0" delete="0">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="file_name" readonly="1"/>
|
||||||
|
<field name="state" readonly="1"/>
|
||||||
|
<field name="message" readonly="1"/>
|
||||||
|
<field name="target_model" invisible="1"/>
|
||||||
|
<field name="extracted_payload" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="candidate_id" readonly="1" invisible="target_model == 'job_recruitment'"/>
|
||||||
|
<field name="applicant_id" readonly="1" invisible="target_model != 'applicant'"/>
|
||||||
|
<field name="attachment_id" readonly="1"/>
|
||||||
|
<field name="file" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook invisible="target_model == 'job_recruitment'">
|
||||||
|
<page string="Candidate Details">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="full_name"/>
|
||||||
|
<field name="first_name"/>
|
||||||
|
<field name="last_name"/>
|
||||||
|
<field name="email"/>
|
||||||
|
<field name="phone"/>
|
||||||
|
<field name="alternate_phone"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="linkedin_profile"/>
|
||||||
|
<field name="current_location"/>
|
||||||
|
<field name="current_organization"/>
|
||||||
|
<field name="total_experience_years"/>
|
||||||
|
<field name="relevant_experience_years"/>
|
||||||
|
<field name="notice_period"/>
|
||||||
|
<field name="degree"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="summary" nolabel="1" placeholder="Summary"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Skills">
|
||||||
|
<field name="skills_text" nolabel="1" placeholder="Enter one skill per line"/>
|
||||||
|
</page>
|
||||||
|
<page string="Education">
|
||||||
|
<field name="education_line_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="education_type"/>
|
||||||
|
<field name="specialization"/>
|
||||||
|
<field name="university"/>
|
||||||
|
<field name="start_year"/>
|
||||||
|
<field name="end_year"/>
|
||||||
|
<field name="marks_or_grade"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Experience">
|
||||||
|
<field name="employer_line_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="company_name"/>
|
||||||
|
<field name="designation"/>
|
||||||
|
<field name="date_of_joining"/>
|
||||||
|
<field name="last_working_day"/>
|
||||||
|
<field name="ctc"/>
|
||||||
|
<field name="work_description"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Family">
|
||||||
|
<field name="family_line_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="relation_type"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="contact_no"/>
|
||||||
|
<field name="dob"/>
|
||||||
|
<field name="location"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Certifications">
|
||||||
|
<field name="certification_line_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="certification_name"/>
|
||||||
|
<field name="provider"/>
|
||||||
|
<field name="completion_date"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Other">
|
||||||
|
<field name="other_line_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="key"/>
|
||||||
|
<field name="value"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
|
||||||
|
<notebook invisible="target_model != 'job_recruitment'">
|
||||||
|
<page string="Job Details">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="request_id"/>
|
||||||
|
<field name="job_title"/>
|
||||||
|
<field name="site_location"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="requested_by"/>
|
||||||
|
<field name="no_of_positions"/>
|
||||||
|
<field name="budget"/>
|
||||||
|
<field name="experience_years"/>
|
||||||
|
<field name="job_category"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="job_summary" nolabel="1" placeholder="Job Summary"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="requirements" nolabel="1" placeholder="Requirements"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Skills">
|
||||||
|
<group>
|
||||||
|
<field name="primary_skills_text" nolabel="1" placeholder="Enter one primary skill per line"/>
|
||||||
|
<field name="secondary_skills_text" nolabel="1" placeholder="Enter one secondary skill per line"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button name="action_save_line_changes" string="Save" type="object" class="btn-primary"/>
|
||||||
|
<button name="action_close_line" string="Close" type="object" class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
<kanban class="o_kanban_small_column">
|
<kanban class="o_kanban_small_column">
|
||||||
<templates>
|
<templates>
|
||||||
<t t-name="kanban-box">
|
<t t-name="kanban-box">
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@
|
||||||
'data/data.xml',
|
'data/data.xml',
|
||||||
'data/sequence.xml',
|
'data/sequence.xml',
|
||||||
'data/mail_template.xml',
|
'data/mail_template.xml',
|
||||||
'data/templates.xml',
|
# 'data/templates.xml',
|
||||||
|
'views/res_config_settings.xml',
|
||||||
|
'views/survey_survey.xml',
|
||||||
|
'views/hr_recruitment_category.xml',
|
||||||
'views/submission_share_history.xml',
|
'views/submission_share_history.xml',
|
||||||
'views/job_category.xml',
|
'views/job_category.xml',
|
||||||
'views/hr_location.xml',
|
'views/hr_location.xml',
|
||||||
|
|
@ -64,7 +67,7 @@
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
# 'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,10 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@http.route(['/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
@http.route([
|
||||||
|
'/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>',
|
||||||
|
'/FTPROTECH/JoiningForm/<int:applicant_id>',
|
||||||
|
], type='http', auth="public",
|
||||||
website=True)
|
website=True)
|
||||||
def post_onboarding_form(self, applicant_id, **kwargs):
|
def post_onboarding_form(self, applicant_id, **kwargs):
|
||||||
"""Renders the website form for applicants to submit additional details."""
|
"""Renders the website form for applicants to submit additional details."""
|
||||||
|
|
@ -174,7 +177,10 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
|
|
||||||
|
|
||||||
@http.route(['/SRIVYNPLATFORMS/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
@http.route([
|
||||||
|
'/SRIVYNPLATFORMS/submit/<int:applicant_id>/JoinForm',
|
||||||
|
'/FTPROTECH/submit/<int:applicant_id>/JoinForm',
|
||||||
|
], type='http', auth="public",
|
||||||
methods=['POST'], website=True, csrf=False)
|
methods=['POST'], website=True, csrf=False)
|
||||||
def process_employee_joining_form(self,applicant_id,**post):
|
def process_employee_joining_form(self,applicant_id,**post):
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
|
|
@ -188,10 +194,10 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0)))
|
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)))
|
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
|
||||||
|
employee_id = post.get('employee_id')
|
||||||
applicant_data = {
|
applicant_data = {
|
||||||
'applicant_id': int(post.get('applicant_id', 0)),
|
'applicant_id': int(post.get('applicant_id', 0)),
|
||||||
'employee_id': int(post.get('employee_id', 0)),
|
'employee_id': int(employee_id) if employee_id else False,
|
||||||
'candidate_image': post.get('candidate_image_base64', ''),
|
'candidate_image': post.get('candidate_image_base64', ''),
|
||||||
'doj': datetime.strptime(post.get('doj'), '%Y-%m-%d').date() if post.get('doj', None) else '',
|
'doj': datetime.strptime(post.get('doj'), '%Y-%m-%d').date() if post.get('doj', None) else '',
|
||||||
'email_from': post.get('email_from', ''),
|
'email_from': post.get('email_from', ''),
|
||||||
|
|
@ -278,6 +284,13 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
|
|
||||||
applicant.write(applicant_data)
|
applicant.write(applicant_data)
|
||||||
applicant.replace_joining_attachments(attachments_data)
|
applicant.replace_joining_attachments(attachments_data)
|
||||||
|
if 'recruitment.employee.bridge' in request.env.registry:
|
||||||
|
bridge = request.env['recruitment.employee.bridge'].sudo().search([
|
||||||
|
('applicant_id', '=', applicant.id),
|
||||||
|
('active', '=', True),
|
||||||
|
], limit=1, order='id desc')
|
||||||
|
if bridge:
|
||||||
|
bridge.state = 'jod_received'
|
||||||
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
||||||
raise_if_not_found=False)
|
raise_if_not_found=False)
|
||||||
group = request.env.ref('hr.group_hr_manager')
|
group = request.env.ref('hr.group_hr_manager')
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import survey_survey
|
||||||
|
from . import hr_recruitment_category
|
||||||
from . import submission_share_history
|
from . import submission_share_history
|
||||||
from . import hr_recruitment
|
from . import hr_recruitment
|
||||||
from . import hr_job_recruitment
|
from . import hr_job_recruitment
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class CandidateExperience(models.Model):
|
||||||
experience_code = fields.Char('Experience Code')
|
experience_code = fields.Char('Experience Code')
|
||||||
experience_from = fields.Integer(string="Experience From (Years)")
|
experience_from = fields.Integer(string="Experience From (Years)")
|
||||||
experience_to = fields.Integer(string="Experience To (Years)")
|
experience_to = fields.Integer(string="Experience To (Years)")
|
||||||
|
company_id = fields.Many2one('res.company','Company', default=lambda self: self.env.company)
|
||||||
# display_name = fields.Char(string="Display Name")
|
# display_name = fields.Char(string="Display Name")
|
||||||
# active = fields.Boolean()
|
# active = fields.Boolean()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,10 @@ class HRApplicant(models.Model):
|
||||||
string='Request Forms'
|
string='Request Forms'
|
||||||
)
|
)
|
||||||
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||||
|
doc_requests_form_status = fields.Selection(
|
||||||
|
[('draft', 'Draft'), ('email_sent_to_candidate', 'Email Sent to Candidate'), ('done', 'Done')],
|
||||||
|
default='draft',
|
||||||
|
)
|
||||||
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
||||||
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
||||||
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
||||||
|
|
@ -301,6 +305,12 @@ class HRApplicant(models.Model):
|
||||||
|
|
||||||
approval_required = fields.Boolean(related='recruitment_stage_id.require_approval')
|
approval_required = fields.Boolean(related='recruitment_stage_id.require_approval')
|
||||||
application_submitted = fields.Boolean(string="Application Submitted")
|
application_submitted = fields.Boolean(string="Application Submitted")
|
||||||
|
send_second_application_form = fields.Boolean(string="Send Second Application Form")
|
||||||
|
send_post_onboarding_form = fields.Boolean(string="Send Post Onboarding Form")
|
||||||
|
second_application_form_status = fields.Selection(
|
||||||
|
[('draft', 'Draft'), ('email_sent_to_candidate', 'Email Sent to Candidate'), ('done', 'Done')],
|
||||||
|
default='draft',
|
||||||
|
)
|
||||||
resume = fields.Binary(related='candidate_id.resume', readonly=False, compute_sudo=True)
|
resume = fields.Binary(related='candidate_id.resume', readonly=False, compute_sudo=True)
|
||||||
resume_type = fields.Char(related='candidate_id.resume_type', readonly=False, compute_sudo=True)
|
resume_type = fields.Char(related='candidate_id.resume_type', readonly=False, compute_sudo=True)
|
||||||
resume_name = fields.Char(related='candidate_id.resume_name', readonly=False, compute_sudo=True)
|
resume_name = fields.Char(related='candidate_id.resume_name', readonly=False, compute_sudo=True)
|
||||||
|
|
@ -366,6 +376,9 @@ class HRApplicant(models.Model):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def submit_to_client(self):
|
||||||
|
return self.action_share_applicant()
|
||||||
|
|
||||||
def submit_for_approval(self):
|
def submit_for_approval(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
|
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
|
||||||
|
|
@ -376,8 +389,7 @@ class HRApplicant(models.Model):
|
||||||
render_ctx = dict(recruitment_manager=manager_id)
|
render_ctx = dict(recruitment_manager=manager_id)
|
||||||
mail_template.with_context(render_ctx).send_mail(
|
mail_template.with_context(render_ctx).send_mail(
|
||||||
self.id,
|
self.id,
|
||||||
force_send=True,
|
force_send=True)
|
||||||
email_layout_xmlid='mail.mail_notification_light')
|
|
||||||
rec.application_submitted = True
|
rec.application_submitted = True
|
||||||
|
|
||||||
def approve_applicant(self):
|
def approve_applicant(self):
|
||||||
|
|
@ -391,8 +403,7 @@ class HRApplicant(models.Model):
|
||||||
render_ctx = dict(recruitment_manager=manager_id)
|
render_ctx = dict(recruitment_manager=manager_id)
|
||||||
mail_template.with_context(render_ctx).send_mail(
|
mail_template.with_context(render_ctx).send_mail(
|
||||||
self.id,
|
self.id,
|
||||||
force_send=True,
|
force_send=True,)
|
||||||
email_layout_xmlid='mail.mail_notification_light')
|
|
||||||
rec.application_submitted = False
|
rec.application_submitted = False
|
||||||
recruitment_stage_ids = rec.hr_job_recruitment.recruitment_stage_ids.ids
|
recruitment_stage_ids = rec.hr_job_recruitment.recruitment_stage_ids.ids
|
||||||
current_stage = self.env['hr.recruitment.stage'].browse(rec.recruitment_stage_id.id)
|
current_stage = self.env['hr.recruitment.stage'].browse(rec.recruitment_stage_id.id)
|
||||||
|
|
@ -436,6 +447,9 @@ class HRApplicant(models.Model):
|
||||||
'context': {'default_req_attachment_ids': []}
|
'context': {'default_req_attachment_ids': []}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def send_post_onboarding_form_to_candidate(self):
|
||||||
|
return self.send_jod_form_to_employee()
|
||||||
|
|
||||||
|
|
||||||
def send_pre_onboarding_doc_request_form_to_candidate(self):
|
def send_pre_onboarding_doc_request_form_to_candidate(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -161,11 +161,11 @@ class HRJobRecruitment(models.Model):
|
||||||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, tracking=True, exportable=False)
|
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, tracking=True, exportable=False)
|
||||||
contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type', tracking=True)
|
contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type', tracking=True)
|
||||||
user_id = fields.Many2one('res.users', "Recruiter",
|
user_id = fields.Many2one('res.users', "Recruiter",
|
||||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
domain=lambda self:[('share', '=', False), ('company_ids', 'in', self.env.company.id),('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)],
|
||||||
default=lambda self: self.env.user,
|
default=lambda self: self.env.user,
|
||||||
tracking=True, help="The Recruiter will be the default value for all Applicants in this job \
|
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.")
|
position. The Recruiter is automatically added to all meetings with the Applicant.")
|
||||||
interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]", tracking=True, help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
|
interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain=lambda self:[('share', '=', False), ('company_ids', 'in', self.env.company.id),('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)], tracking=True, help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
|
||||||
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills", tracking=True)
|
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills", tracking=True)
|
||||||
address_id = fields.Many2one(
|
address_id = fields.Many2one(
|
||||||
'res.partner', "Job Location", default=_default_address_id,
|
'res.partner', "Job Location", default=_default_address_id,
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,7 @@ class RecruitmentCategory(models.Model):
|
||||||
|
|
||||||
category_name = fields.Char(string="Category Name")
|
category_name = fields.Char(string="Category Name")
|
||||||
default_user = fields.Many2one('res.users')
|
default_user = fields.Many2one('res.users')
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.company ,string='Company', ondelete='cascade')
|
||||||
|
|
||||||
|
|
||||||
class ApplicationsStageStatus(models.Model):
|
class ApplicationsStageStatus(models.Model):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from odoo import fields, models, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicantCategory(models.Model):
|
||||||
|
_inherit = "hr.applicant.category"
|
||||||
|
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
allow_cross_company_candidates = fields.Boolean(
|
||||||
|
string="Allow Cross-Company Candidate Access",config_parameter='hr_recruitment_extended.allow_cross_company_candidates',
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_values(self):
|
||||||
|
res = super().set_values()
|
||||||
|
rule = self.env.ref("hr_recruitment.hr_candidate_comp_rule")
|
||||||
|
rule.write({
|
||||||
|
"active": not self.allow_cross_company_candidates
|
||||||
|
})
|
||||||
|
self.env.registry.clear_cache()
|
||||||
|
return res
|
||||||
|
|
@ -5,3 +5,46 @@ class ResPartner(models.Model):
|
||||||
_inherit = 'res.partner'
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
|
||||||
|
|
||||||
|
# Vendor Statistics Fields
|
||||||
|
total_positions_requested = fields.Integer(compute='_compute_vendor_statistics', string='Positions Requested')
|
||||||
|
total_submitted = fields.Integer(compute='_compute_vendor_statistics', string='Total Submitted')
|
||||||
|
total_hired = fields.Integer(compute='_compute_vendor_statistics', string='Total Hired')
|
||||||
|
total_rejected = fields.Integer(compute='_compute_vendor_statistics', string='Total Rejected')
|
||||||
|
job_recruitment_ids = fields.One2many('hr.job.recruitment', 'requested_by', string='Job Requests')
|
||||||
|
|
||||||
|
@api.depends('contact_type')
|
||||||
|
def _compute_vendor_statistics(self):
|
||||||
|
for partner in self:
|
||||||
|
if partner.contact_type == 'external':
|
||||||
|
# Get all job recruitments requested by this vendor
|
||||||
|
job_recruitments = self.env['hr.job.recruitment'].search([
|
||||||
|
('requested_by', '=', partner.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
partner.total_positions_requested = sum(job_recruitments.mapped('no_of_recruitment'))
|
||||||
|
partner.total_submitted = sum(job_recruitments.mapped('no_of_submissions'))
|
||||||
|
partner.total_hired = sum(job_recruitments.mapped('no_of_hired_employee'))
|
||||||
|
partner.total_rejected = sum(job_recruitments.mapped('no_of_refused_submissions'))
|
||||||
|
else:
|
||||||
|
partner.total_positions_requested = 0
|
||||||
|
partner.total_submitted = 0
|
||||||
|
partner.total_hired = 0
|
||||||
|
partner.total_rejected = 0
|
||||||
|
|
||||||
|
def action_create_job_request(self):
|
||||||
|
"""Action to create a new job request from vendor"""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _('Create Job Request'),
|
||||||
|
'res_model': 'hr.job.recruitment',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current',
|
||||||
|
'context': {
|
||||||
|
'default_requested_by': self.id,
|
||||||
|
'default_recruitment_type': 'external',
|
||||||
|
'default_address_id': self.id if self.is_company else (self.parent_id.id if self.parent_id else False),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
class SurveySurvey(models.Model):
|
||||||
|
_inherit = "survey.survey"
|
||||||
|
|
||||||
|
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||||
|
|
@ -36,6 +36,6 @@ access_applicant_stage_comment_wizard,applicant.stage.comment.wizard.user,model_
|
||||||
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
|
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
|
||||||
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
||||||
access_applicant_request_forms_hr_user,access.applicant.request.forms.hr.user,model_applicant_request_forms,hr.group_hr_user,1,1,1,1
|
access_applicant_request_forms_hr_user,access.applicant.request.forms.hr.user,model_applicant_request_forms,hr.group_hr_user,1,1,1,1
|
||||||
access_applicant_request_forms_user,access.applicant.request.forms.user,model_applicant_request_forms,base.group_user,1,1,0,0
|
access_applicant_request_forms_user,access.applicant.request.forms.user,model_applicant_request_forms,base.group_user,1,1,1,0
|
||||||
access_hr_skill,access.hr.skill.user,hr_skills.model_hr_skill,base.group_public,1,0,0,0
|
access_hr_skill,access.hr.skill.user,hr_skills.model_hr_skill,base.group_public,1,0,0,0
|
||||||
|
|
||||||
|
|
|
||||||
|
Gitea Version: 1.21.4 |
