srivyn_test #32

Merged
deepak merged 37 commits from srivyn_test into srivyn_uat 2026-08-05 12:39:29 +05:30
260 changed files with 19490 additions and 5417 deletions

View File

@ -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

View File

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

View File

@ -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',
}

View File

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

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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,
};
},
});

View File

@ -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>

View File

@ -0,0 +1,3 @@
from . import models
from . import controllers
from . import wizards

View File

@ -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,
}

View File

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

View File

@ -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)])
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,3 @@
from . import recruitment_employee_bridge
from . import hr_employee
from . import onboarding_documents

View File

@ -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,
}

View File

@ -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")

View File

@ -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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_employee_bridge_user employee.bridge.user model_recruitment_employee_bridge base.group_user 1 1 1 0
3 access_employee_bridge_manager employee.bridge.manager model_recruitment_employee_bridge hr.group_hr_manager 1 1 1 1
4 access_employee_bridge_requested_attachment_user employee.bridge.requested.attachment.user model_employee_bridge_requested_attachment base.group_user 1 1 1 0
5 access_employee_bridge_requested_attachment_manager employee.bridge.requested.attachment.manager model_employee_bridge_requested_attachment hr.group_hr_manager 1 1 1 1
6 access_employee_bridge_attachment_user employee.bridge.attachment.user model_employee_bridge_attachment base.group_user 1 1 1 0
7 access_employee_bridge_attachment_manager employee.bridge.attachment.manager model_employee_bridge_attachment hr.group_hr_manager 1 1 1 1
8 access_employee_bridge_attachment_wizard employee.bridge.attachment.wizard.user model_employee_bridge_attachment_wizard base.group_user 1 1 1 1

View File

@ -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;">
&times;
</button>
</div>
<!-- Modal Body -->
<div class="modal-body" style="padding: 15px;">
<ul class="attachment-list" style="list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto;">
${fileList.length ? fileList.map((file, index) => `
<li style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa;">
<span style="font-weight: 500; flex-grow: 1;">${file.name}</span>
<button type="button" class="remove-file" data-index="${index}"
style="background: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;">
<i style="margin-right: 5px;">🗑</i>
</button>
</li>
`).join("") : `<p style="color: #6c757d; text-align: center; font-size: 16px; padding: 10px;">No files uploaded.</p>`}
</ul>
</div>
<!-- Modal Footer -->
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 12px;">
<button type="button" class="btn close-modal-btn" data-dismiss="modal"
style="background: #6c757d; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;">
Close
</button>
</div>
</div>
</div>
</div>`;
// Remove old modal and append new one
$("#attachmentModal").remove();
$("body").append(modalHtml);
// Attach remove event to new modal content
$("#attachmentModal").on("click", ".remove-file", this.removeFile.bind(this));
$("#attachmentModal").on("click", ".close-modal-btn", this.closeAttachmentModal.bind(this));
},
/**
* Close attachment modal
*/
closeAttachmentModal() {
console.log("Closing modal");
$("#attachmentModal").remove(); // Remove the modal from the DOM
},
// Function to preview the uploaded image
previewApplicantPhoto(ev) {
const input = ev.currentTarget;
const preview = this.$("#photo_preview");
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
preview.attr("src", e.target.result);
// Store the base64 in a hidden input field
this.$("input[name='candidate_image_base64']").val(base64String);
};
reader.readAsDataURL(input.files[0]);
}
},
// Function to delete the uploaded image
deleteCandidatePhoto() {
const preview = this.$("#photo_preview");
const inputFile = this.$("input[name='candidate_image']");
preview.attr("src", "data:image/png;base64,"); // Reset preview
inputFile.val(""); // Reset file input
},
// Function to preview full image inside a modal
previewFullImage() {
const previewSrc = this.$("#photo_preview").attr("src");
if (previewSrc) {
this.$("#modal_photo_preview").attr("src", previewSrc);
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
}
},
// Function to add a new education row dynamically
addEducationRow(ev) {
ev.preventDefault(); // Prevent default behavior
let newRow = `
<tr class="predefined-row">
<td class="education-relation-col">
<select name="education_type" class="form-control">
<option value="10">10th</option>
<option value="inter">Inter</option>
<option value="graduation">Graduation</option>
<option value="post_graduation">Post Graduation</option>
<option value="additional">Additional Qualification</option>
</select>
</td>
<td><input type="text" name="specialization" class="form-control" placeholder="Enter Specialization"/></td>
<td><input type="text" name="university" class="form-control" placeholder="Enter University"/></td>
<td><input type="number" name="start_year" class="form-control" placeholder="Start Year"/></td>
<td><input type="number" name="end_year" class="form-control" placeholder="End Year"/></td>
<td><input type="text" name="marks_grade" class="form-control" placeholder="Marks/Grade"/></td>
</tr>
`;
this.$("#education_details_data tbody").append(newRow); // Append new row inside the table
console.log("New education row added!");
},
validateFamilyDetails() {
let familyDetailsFilled = false;
let educationDetailsFilled = false;
const family_rows = document.querySelectorAll('#family_details_data tbody tr');
const education_rows = document.querySelectorAll('#education_details_data tbody tr');
family_rows.forEach(row => {
const inputs = row.querySelectorAll('input[type="text"], input[type="date"]');
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
if (isRowFilled) {
familyDetailsFilled = true;
}
});
education_rows.forEach(row => {
const inputs = row.querySelectorAll('input[type="text"]');
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
if (isRowFilled) {
educationDetailsFilled = true;
}
});
if (!familyDetailsFilled) {
alert('Please fill at least one family member detail.');
return false;
}
if (!educationDetailsFilled) {
alert('Please fill at least one Education details')
return false;
}
return true;
},
handleFormSubmit(ev) {
ev.preventDefault();
if (this.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();
}
},
});

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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"}

View File

@ -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>

View File

@ -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',
} }

View File

@ -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)}"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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>

View File

@ -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

View File

@ -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'"/>

View File

@ -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">

View File

@ -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>

View File

@ -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"

View File

@ -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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_recruitment_auto_doc_wizard hr.recruitment.auto.doc.wizard model_hr_recruitment_auto_doc_wizard base.group_user 1 1 1 1
3 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
4 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
5 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
6 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
7 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
8 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
9 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

View File

@ -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

View File

@ -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">

View File

@ -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',
], ],
} }
} }

View File

@ -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')

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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,

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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),
}
}

View File

@ -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)

View File

@ -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

Internal Server Error - Gitea: Git with a cup of tea

Internal Server Error

Gitea Version: 1.21.4