srivyn_test #32
|
|
@ -107,7 +107,8 @@ RUN if [ -f requirements.txt ]; then \
|
|||
pypdf \
|
||||
phonenumbers \
|
||||
python-docx \
|
||||
pyzk
|
||||
pyzk \
|
||||
firebase-admin
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Create Required Directories
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Base Changes',
|
||||
'category': 'Base',
|
||||
'version': '1.1',
|
||||
'author': 'Seshi Kanth',
|
||||
'summary': 'Changes of the form',
|
||||
'description': 'This module contains form changes.',
|
||||
'depends': [
|
||||
'base',
|
||||
'web',
|
||||
],
|
||||
|
||||
'data': [
|
||||
# 'views/user_menu.xml',
|
||||
],
|
||||
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'base_custom/static/src/css/backend.css',
|
||||
'base_custom/static/src/js/user_menu_patch.js',
|
||||
'base_custom/static/src/js/hide_user_menu.js',
|
||||
'base_custom/static/src/xml/user_menu.xml',
|
||||
],
|
||||
},
|
||||
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import ir_http
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = "ir.http"
|
||||
|
||||
def session_info(self):
|
||||
session_info = super().session_info()
|
||||
|
||||
employee = self.env.user.employee_id
|
||||
|
||||
session_info.update({
|
||||
"employee_name": employee.name or self.env.user.name,
|
||||
"employee_designation": employee.job_id.name if employee.job_id else "",
|
||||
})
|
||||
|
||||
return session_info
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
.o_switch_company_menu .oe_topbar_name{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.oe_topbar_name{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
line-height:1.2;
|
||||
}
|
||||
|
||||
.oe_topbar_name .fw-bold{
|
||||
font-size:13px;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.oe_topbar_name .designation{
|
||||
font-size:11px;
|
||||
color:#7b7b7b;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
|
||||
const itemsToHide = [
|
||||
"documentation",
|
||||
"support",
|
||||
"shortcuts",
|
||||
"odoo_account",
|
||||
];
|
||||
|
||||
for (const item of itemsToHide) {
|
||||
if (userMenuRegistry.contains(item)) {
|
||||
userMenuRegistry.remove(item);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { UserMenu } from "@web/webclient/user_menu/user_menu";
|
||||
import { session } from "@web/session";
|
||||
|
||||
patch(UserMenu.prototype, {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.employeeName = session.employee_name;
|
||||
this.employeeDesignation = session.employee_designation;
|
||||
},
|
||||
|
||||
get employeeInfo() {
|
||||
return {
|
||||
employeeName: this.employeeName,
|
||||
employeeDesignation: this.employeeDesignation,
|
||||
};
|
||||
},
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.UserMenu"
|
||||
t-inherit-mode="extension">
|
||||
|
||||
<xpath expr="//small[contains(@class,'oe_topbar_name')]"
|
||||
position="replace">
|
||||
|
||||
<small class="oe_topbar_name d-none d-lg-inline-block ms-2 text-start"
|
||||
style="max-width:220px">
|
||||
|
||||
<div class="fw-bold text-truncate">
|
||||
<t t-esc="employeeInfo.employeeName"/>
|
||||
</div>
|
||||
|
||||
<div class="text-muted text-truncate designation">
|
||||
<t t-esc="employeeInfo.employeeDesignation"/>
|
||||
</div>
|
||||
|
||||
</small>
|
||||
|
||||
</xpath>
|
||||
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
from . import wizards
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Onboarding",
|
||||
"summary": "Employee-side onboarding bridge for JOD and onboarding links",
|
||||
"description": """
|
||||
Provides an employee-owned bridge record that can be used without
|
||||
installing recruitment or ATS modules.
|
||||
""",
|
||||
"author": "FTPROTECH",
|
||||
"website": "https://www.ftprotech.com",
|
||||
"category": "Human Resources",
|
||||
"version": "1.0",
|
||||
"depends": [
|
||||
"hr_employee_extended",
|
||||
"mail",
|
||||
"website",
|
||||
],
|
||||
"data": [
|
||||
"data/ir_sequence_data.xml",
|
||||
"data/onboarding_attachment_data.xml",
|
||||
"data/mail_template.xml",
|
||||
"data/template.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"wizards/employee_bridge_attachment_wizard.xml",
|
||||
"views/recruitment_employee_bridge_views.xml",
|
||||
"views/onboarding_attachment_views.xml",
|
||||
"views/hr_employee_views.xml",
|
||||
],
|
||||
"license": "LGPL-3",
|
||||
"assets": {
|
||||
"web.assets_frontend": [
|
||||
"employee_bridge/static/src/js/post_onboarding_form.js",
|
||||
],
|
||||
},
|
||||
"application": False,
|
||||
"installable": True,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.website.controllers.main import Website # <-- ADD THIS IMPORT
|
||||
|
||||
|
||||
# CHANGE INHERITANCE: http.Controller -> Website
|
||||
class EmployeeBridgeController(Website):
|
||||
|
||||
def _get_bridge(self, token):
|
||||
return request.env["recruitment.employee.bridge"].sudo().search([
|
||||
("access_token", "=", token),
|
||||
("active", "=", True),
|
||||
], limit=1)
|
||||
|
||||
def _render_form(self, bridge):
|
||||
# Removed the 'message' variable as we will use a separate template for success
|
||||
return request.render("employee_bridge.employee_bridge_jod_form_template", {
|
||||
"bridge": bridge,
|
||||
})
|
||||
|
||||
@http.route("/employee_bridge/jod/<string:token>", type="http", auth="public", csrf=False, website=True)
|
||||
def employee_bridge_jod(self, token, **post):
|
||||
bridge = self._get_bridge(token)
|
||||
if not bridge:
|
||||
return request.not_found()
|
||||
|
||||
if request.httprequest.method == "POST":
|
||||
bridge.write_from_public_form(post)
|
||||
|
||||
# RENDER A SEPARATE THANK YOU TEMPLATE INSTEAD OF THE FORM
|
||||
return request.render("employee_bridge.employee_bridge_thank_you_template", {
|
||||
"bridge": bridge,
|
||||
})
|
||||
|
||||
return self._render_form(bridge)
|
||||
|
||||
@http.route("/employee_bridge/fetch_related_state_ids", type="json", auth="public", website=True)
|
||||
def fetch_related_state_ids(self, country_id=None):
|
||||
states = request.env["res.country.state"].sudo()
|
||||
return {
|
||||
state.id: state.name
|
||||
for state in states.search([("country_id", "=?", country_id)])
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="seq_recruitment_employee_bridge" model="ir.sequence">
|
||||
<field name="name">Onboarding</field>
|
||||
<field name="code">recruitment.employee.bridge</field>
|
||||
<field name="prefix">EB/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="email_template_jod_form" model="mail.template">
|
||||
<field name="name">JOD Form Email</field>
|
||||
<field name="model_id" ref="employee_bridge.model_recruitment_employee_bridge"/>
|
||||
<field name="email_from">{{ user.email_formatted or object.company_id.email or '' }}</field>
|
||||
<field name="email_to">{{ object.work_email or object.private_email or object.employee_id.work_email or '' }}</field>
|
||||
<field name="subject">Joining Onboarding Form - {{ object.employee_name or object.employee_id.name or '' }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin:0; padding:0; font-size:13px; line-height:1.7;">
|
||||
<p>Dear <t t-esc="object.employee_name or object.employee_id.name or 'Candidate'"/>,</p>
|
||||
<p>Welcome to <t t-esc="object.employee_id.company_id.name"/>.</p>
|
||||
<p>
|
||||
Please submit your employee joining details using the link below.
|
||||
</p>
|
||||
<p>
|
||||
<a t-att-href="ctx.get('joining_form_link')" target="_blank"
|
||||
style="background-color:#005580; color:#ffffff; padding:10px 18px; text-decoration:none; border-radius:4px; display:inline-block;">
|
||||
Open Employee Joining Form
|
||||
</a>
|
||||
</p>
|
||||
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
|
||||
<p><strong>Attachments to Request</strong></p>
|
||||
<t t-if="ctx.get('personal_docs')">
|
||||
<p><strong>Personal Documents:</strong> <t t-esc="', '.join(ctx.get('personal_docs'))"/></p>
|
||||
</t>
|
||||
<t t-if="ctx.get('education_docs')">
|
||||
<p><strong>Education Documents:</strong> <t t-esc="', '.join(ctx.get('education_docs'))"/></p>
|
||||
</t>
|
||||
<t t-if="ctx.get('previous_employer_docs')">
|
||||
<p><strong>Previous Employer Documents:</strong> <t t-esc="', '.join(ctx.get('previous_employer_docs'))"/></p>
|
||||
</t>
|
||||
<t t-if="ctx.get('other_docs')">
|
||||
<p><strong>Other Documents:</strong> <t t-esc="', '.join(ctx.get('other_docs'))"/></p>
|
||||
</t>
|
||||
</t>
|
||||
<p>Regards,<br/>HR Team</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="onboarding_attachment_aadhaar" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Aadhaar Card</field>
|
||||
<field name="attachment_type">personal</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_pan" model="employee.bridge.requested.attachment">
|
||||
<field name="name">PAN Card</field>
|
||||
<field name="attachment_type">personal</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_photo" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Passport Size Photo</field>
|
||||
<field name="attachment_type">personal</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_education" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Education Certificates</field>
|
||||
<field name="attachment_type">education</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_experience" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Experience / Relieving Letter</field>
|
||||
<field name="attachment_type">previous_employer</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_payslip" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Last Three Months Payslips</field>
|
||||
<field name="attachment_type">previous_employer</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_bank" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Cancelled Cheque / Bank Proof</field>
|
||||
<field name="attachment_type">others</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,3 @@
|
|||
from . import recruitment_employee_bridge
|
||||
from . import hr_employee
|
||||
from . import onboarding_documents
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
from odoo import _, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
recruitment_bridge_ids = fields.One2many(
|
||||
"recruitment.employee.bridge",
|
||||
"employee_id",
|
||||
string="Onboarding Records",
|
||||
)
|
||||
recruitment_bridge_count = fields.Integer(compute="_compute_recruitment_bridge_count")
|
||||
|
||||
def _compute_recruitment_bridge_count(self):
|
||||
data = self.env["recruitment.employee.bridge"].read_group(
|
||||
[("employee_id", "in", self.ids), ("active", "=", True)],
|
||||
["employee_id"],
|
||||
["employee_id"],
|
||||
)
|
||||
counts = {item["employee_id"][0]: item["employee_id_count"] for item in data}
|
||||
for employee in self:
|
||||
employee.recruitment_bridge_count = counts.get(employee.id, 0)
|
||||
|
||||
def _get_or_create_employee_bridge(self):
|
||||
self.ensure_one()
|
||||
bridge = self.env["recruitment.employee.bridge"].search(
|
||||
[("employee_id", "=", self.id), ("active", "=", True)],
|
||||
limit=1,
|
||||
order="id desc",
|
||||
)
|
||||
if not bridge:
|
||||
bridge = self.env["recruitment.employee.bridge"].create({
|
||||
"employee_id": self.id,
|
||||
"company_id": self.company_id.id or self.env.company.id,
|
||||
"source": "jod",
|
||||
"state": "draft",
|
||||
})
|
||||
return bridge
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_or_create_employee_bridge()
|
||||
bridge.action_send_jod_form()
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
||||
def action_open_recruitment_bridge(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_or_create_employee_bridge()
|
||||
if not bridge:
|
||||
raise ValidationError(_("No onboarding record found."))
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class EmployeeBridgeRequestedAttachment(models.Model):
|
||||
_name = "employee.bridge.requested.attachment"
|
||||
_description = "Onboarding Requested Attachment"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
attachment_type = fields.Selection([
|
||||
("personal", "Personal Documents"),
|
||||
("education", "Education Documents"),
|
||||
("previous_employer", "Previous Employer"),
|
||||
("others", "Others"),
|
||||
], default="others", required=True)
|
||||
is_default = fields.Boolean(string="Is Default")
|
||||
|
||||
|
||||
class EmployeeBridgeAttachment(models.Model):
|
||||
_name = "employee.bridge.attachment"
|
||||
_description = "Onboarding Attachment"
|
||||
_rec_name = "name"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, ondelete="cascade")
|
||||
employee_id = fields.Many2one("hr.employee")
|
||||
requested_attachment_id = fields.Many2one("employee.bridge.requested.attachment")
|
||||
attachment_type = fields.Selection(related="requested_attachment_id.attachment_type")
|
||||
file = fields.Binary(required=True)
|
||||
file_name = fields.Char()
|
||||
review_status = fields.Selection([
|
||||
("draft", "Under Review"),
|
||||
("pass", "PASS"),
|
||||
("fail", "FAIL"),
|
||||
], default="draft")
|
||||
review_comments = fields.Char()
|
||||
|
||||
def action_preview_file(self):
|
||||
self.ensure_one()
|
||||
attachment = self.env["ir.attachment"].sudo().create({
|
||||
"name": self.file_name or self.name,
|
||||
"datas": self.file,
|
||||
"res_model": self._name,
|
||||
"res_id": self.id,
|
||||
"type": "binary",
|
||||
})
|
||||
return {
|
||||
"name": "File Preview",
|
||||
"type": "ir.actions.act_url",
|
||||
"url": f"/web/content/{attachment.id}?download=false",
|
||||
"target": "current",
|
||||
}
|
||||
|
||||
|
||||
class FamilyDetails(models.Model):
|
||||
_inherit = "family.details"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||
|
||||
|
||||
class EducationHistory(models.Model):
|
||||
_inherit = "education.history"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||
|
||||
|
||||
class EmployerHistory(models.Model):
|
||||
_inherit = "employer.history"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||
|
|
@ -0,0 +1,559 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class RecruitmentEmployeeBridge(models.Model):
|
||||
_name = "recruitment.employee.bridge"
|
||||
_description = "Onboarding"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_rec_name = "name"
|
||||
_order = "id desc"
|
||||
|
||||
@api.depends('name', 'employee_id', 'employee_name')
|
||||
def _compute_display_name(self):
|
||||
for record in self:
|
||||
employee_name = record.employee_name or record.employee_id.name or ''
|
||||
|
||||
if employee_name:
|
||||
record.display_name = f"{record.name} - {employee_name}"
|
||||
else:
|
||||
record.display_name = record.name
|
||||
|
||||
name = fields.Char(default="/", copy=False, readonly=True, tracking=True)
|
||||
active = fields.Boolean(default=True)
|
||||
employee_id = fields.Many2one(
|
||||
"hr.employee",
|
||||
string="Employee",
|
||||
ondelete="restrict",
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
source = fields.Selection(
|
||||
[
|
||||
("jod", "JOD"),
|
||||
("manual", "Manual"),
|
||||
("recruitment", "Recruitment"),
|
||||
],
|
||||
default="jod",
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "Draft"),
|
||||
("jod_sent", "JOD Sent"),
|
||||
("jod_received", "JOD Received"),
|
||||
("validated", "Validated"),
|
||||
],
|
||||
default="draft",
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
employee_name = fields.Char(string="Employee Name", tracking=True)
|
||||
employee_code = fields.Char(string="Employee Code", tracking=True)
|
||||
candidate_image = fields.Image(string="Photo")
|
||||
work_email = fields.Char(string="Work Email", tracking=True)
|
||||
private_email = fields.Char(string="Private Email")
|
||||
alternate_phone = fields.Char(string="Alternate Mobile")
|
||||
work_phone = fields.Char(string="Work Phone")
|
||||
mobile_phone = fields.Char(string="Mobile Phone")
|
||||
department_id = fields.Many2one("hr.department", string="Department")
|
||||
job_id = fields.Many2one("hr.job", string="Job Position")
|
||||
emp_type = fields.Many2one("hr.contract.type", string="Employment Type")
|
||||
doj = fields.Date(string="Date of Joining")
|
||||
gender = fields.Selection([
|
||||
("male", "Male"),
|
||||
("female", "Female"),
|
||||
("other", "Other"),
|
||||
])
|
||||
birthday = fields.Date()
|
||||
blood_group = fields.Selection([
|
||||
("A+", "A+"),
|
||||
("A-", "A-"),
|
||||
("B+", "B+"),
|
||||
("B-", "B-"),
|
||||
("O+", "O+"),
|
||||
("O-", "O-"),
|
||||
("AB+", "AB+"),
|
||||
("AB-", "AB-"),
|
||||
], string="Blood Group")
|
||||
marital = fields.Selection(
|
||||
selection=[
|
||||
("single", "Single"),
|
||||
("married", "Married"),
|
||||
("cohabitant", "Legal Cohabitant"),
|
||||
("widower", "Widower"),
|
||||
("divorced", "Divorced"),
|
||||
],
|
||||
default="single",
|
||||
string="Marital Status",
|
||||
)
|
||||
marriage_anniversary_date = fields.Date(string="Anniversary Date")
|
||||
private_street = fields.Char(string="Private Street")
|
||||
private_street2 = fields.Char(string="Private Street2")
|
||||
private_city = fields.Char(string="Private City")
|
||||
private_state_id = fields.Many2one("res.country.state", string="Private State")
|
||||
private_zip = fields.Char(string="Private Zip")
|
||||
private_country_id = fields.Many2one("res.country", string="Private Country")
|
||||
permanent_street = fields.Char(string="Permanent Street")
|
||||
permanent_street2 = fields.Char(string="Permanent Street2")
|
||||
permanent_city = fields.Char(string="Permanent City")
|
||||
permanent_state_id = fields.Many2one("res.country.state", string="Permanent State")
|
||||
permanent_zip = fields.Char(string="Permanent Zip")
|
||||
permanent_country_id = fields.Many2one("res.country", string="Permanent Country")
|
||||
pan_no = fields.Char(string="PAN No")
|
||||
identification_id = fields.Char(string="Aadhar No")
|
||||
previous_company_pf_no = fields.Char(string="Previous Company PF No")
|
||||
previous_company_uan_no = fields.Char(string="Previous Company UAN No")
|
||||
passport_no = fields.Char(string="Passport No")
|
||||
passport_start_date = fields.Date(string="Passport Issued Date")
|
||||
passport_end_date = fields.Date(string="Passport End Date")
|
||||
passport_issued_location = fields.Char(string="Passport Issued Location")
|
||||
full_name_as_in_bank = fields.Char(string="Name as per Bank")
|
||||
bank_name = fields.Char()
|
||||
bank_branch = fields.Char()
|
||||
bank_account_no = fields.Char()
|
||||
bank_ifsc_code = fields.Char()
|
||||
requested_attachment_ids = fields.Many2many(
|
||||
"employee.bridge.requested.attachment",
|
||||
"employee_bridge_requested_attachment_rel",
|
||||
"bridge_id",
|
||||
"attachment_id",
|
||||
string="Attachments to Request",
|
||||
)
|
||||
joining_attachment_ids = fields.One2many(
|
||||
"employee.bridge.attachment",
|
||||
"bridge_id",
|
||||
string="Attachments",
|
||||
)
|
||||
education_history = fields.One2many("education.history", "bridge_id", string="Education Details")
|
||||
employer_history = fields.One2many("employer.history", "bridge_id", string="Employer History")
|
||||
family_details = fields.One2many("family.details", "bridge_id", string="Family Details")
|
||||
joining_form_link = fields.Char(string="Joining Form Link", copy=False, readonly=True)
|
||||
access_token = fields.Char(copy=False, readonly=True)
|
||||
total_exp = fields.Float(string="Total Exp")
|
||||
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"unique_bridge_employee",
|
||||
"unique(employee_id)",
|
||||
"This employee is already connected to an employee bridge.",
|
||||
),
|
||||
]
|
||||
|
||||
def _get_base_url(self):
|
||||
return self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||
|
||||
def _ensure_access_token(self):
|
||||
for bridge in self:
|
||||
if not bridge.access_token:
|
||||
bridge.access_token = token_urlsafe(32)
|
||||
|
||||
def _get_joining_form_link(self):
|
||||
self.ensure_one()
|
||||
self._ensure_access_token()
|
||||
return "%s/employee_bridge/jod/%s" % (self._get_base_url(), self.access_token)
|
||||
|
||||
def _group_requested_attachments(self, attachments):
|
||||
return {
|
||||
"personal_docs": attachments.filtered(lambda a: a.attachment_type == "personal").mapped("name"),
|
||||
"education_docs": attachments.filtered(lambda a: a.attachment_type == "education").mapped("name"),
|
||||
"previous_employer_docs": attachments.filtered(lambda a: a.attachment_type == "previous_employer").mapped("name"),
|
||||
"other_docs": attachments.filtered(lambda a: a.attachment_type == "others").mapped("name"),
|
||||
}
|
||||
|
||||
def _send_jod_mail(self, req_attachment_ids=False, email_values=None, email_body=False):
|
||||
for bridge in self:
|
||||
email_to = bridge.work_email or bridge.private_email or bridge.employee_id.work_email
|
||||
if not email_to:
|
||||
raise ValidationError(_("Add an email on the bridge before sending the JOD form."))
|
||||
link = bridge._get_joining_form_link()
|
||||
bridge.write({
|
||||
"joining_form_link": link,
|
||||
"state": "jod_sent",
|
||||
})
|
||||
attachments = req_attachment_ids or self.env["employee.bridge.requested.attachment"].browse()
|
||||
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||
context = {
|
||||
"joining_form_link": link,
|
||||
**bridge._group_requested_attachments(attachments),
|
||||
}
|
||||
values = dict(email_values or {})
|
||||
values.setdefault("email_from", self.env.company.email or self.env.user.email)
|
||||
values.setdefault("email_to", email_to)
|
||||
if email_body:
|
||||
values["body_html"] = email_body
|
||||
if template:
|
||||
rendered_subject = template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id]
|
||||
rendered_body = template.with_context(**context)._render_field(
|
||||
"body_html", [bridge.id], compute_lang=True
|
||||
)[bridge.id]
|
||||
values.setdefault("subject", rendered_subject)
|
||||
values.setdefault("body_html", rendered_body)
|
||||
template.sudo().with_context(**context).send_mail(bridge.id, email_values=values, force_send=True)
|
||||
else:
|
||||
values.setdefault("subject", _("Joining Onboarding Details"))
|
||||
values.setdefault("body_html", _(
|
||||
"<p>Hello %(name)s,</p>"
|
||||
"<p>Please submit your joining onboarding details using this link:</p>"
|
||||
"<p><a href='%(link)s'>Open Joining Form</a></p>"
|
||||
) % {
|
||||
"name": bridge.employee_name or bridge.employee_id.name or _("Employee"),
|
||||
"link": link,
|
||||
})
|
||||
self.env["mail.mail"].sudo().create(values).send()
|
||||
return True
|
||||
|
||||
def action_send_jod_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Select Attachments"),
|
||||
"res_model": "employee.bridge.attachment.wizard",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_bridge_id": self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_open_employee(self):
|
||||
self.ensure_one()
|
||||
if not self.employee_id:
|
||||
raise ValidationError(_("No employee is linked to this bridge."))
|
||||
return {
|
||||
"name": _("Employee"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.employee",
|
||||
"view_mode": "form",
|
||||
"res_id": self.employee_id.id,
|
||||
}
|
||||
|
||||
def action_validate_employee_details(self):
|
||||
for bridge in self:
|
||||
if not bridge.employee_id:
|
||||
raise ValidationError(_("Link or create an employee before validating the JOD details."))
|
||||
bridge.employee_id.write(bridge._get_employee_update_vals())
|
||||
bridge.education_history.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.employer_history.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.family_details.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.joining_attachment_ids.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.state = "validated"
|
||||
|
||||
def _get_employee_create_vals(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
"name": self.employee_name,
|
||||
"employee_id": self.employee_code,
|
||||
"work_email": self.work_email,
|
||||
"private_email": self.private_email,
|
||||
"work_phone": self.work_phone,
|
||||
"mobile_phone": self.mobile_phone,
|
||||
"image_1920": self.candidate_image,
|
||||
"department_id": self.department_id.id,
|
||||
"job_id": self.job_id.id,
|
||||
"job_title": self.job_id.name,
|
||||
"emp_type": self.emp_type.id,
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
vals = {key: value for key, value in vals.items() if value not in (False, "", 0)}
|
||||
if not vals.get("name"):
|
||||
raise ValidationError(_("Employee Name is required to create an employee."))
|
||||
return vals
|
||||
|
||||
def _get_employee_update_vals(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
"name": self.employee_name,
|
||||
"employee_id": self.employee_code,
|
||||
"work_email": self.work_email,
|
||||
"private_email": self.private_email,
|
||||
"work_phone": self.work_phone,
|
||||
"mobile_phone": self.mobile_phone,
|
||||
"image_1920": self.candidate_image,
|
||||
"department_id": self.department_id.id,
|
||||
"job_id": self.job_id.id,
|
||||
"job_title": self.job_id.name,
|
||||
"emp_type": self.emp_type.id,
|
||||
"doj": self.doj,
|
||||
"gender": self.gender,
|
||||
"birthday": self.birthday,
|
||||
"blood_group": self.blood_group,
|
||||
"marital": self.marital,
|
||||
"marriage_anniversary_date": self.marriage_anniversary_date,
|
||||
"private_street": self.private_street,
|
||||
"private_street2": self.private_street2,
|
||||
"private_city": self.private_city,
|
||||
"private_state_id": self.private_state_id.id,
|
||||
"private_zip": self.private_zip,
|
||||
"private_country_id": self.private_country_id.id,
|
||||
"permanent_street": self.permanent_street,
|
||||
"permanent_street2": self.permanent_street2,
|
||||
"permanent_city": self.permanent_city,
|
||||
"permanent_state_id": self.permanent_state_id.id,
|
||||
"permanent_zip": self.permanent_zip,
|
||||
"permanent_country_id": self.permanent_country_id.id,
|
||||
"pan_no": self.pan_no,
|
||||
"identification_id": self.identification_id,
|
||||
"previous_company_pf_no": self.previous_company_pf_no,
|
||||
"previous_company_uan_no": self.previous_company_uan_no,
|
||||
"passport_id": self.passport_no,
|
||||
"passport_start_date": self.passport_start_date,
|
||||
"passport_end_date": self.passport_end_date,
|
||||
"passport_issued_location": self.passport_issued_location,
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
for key, value in vals.items()
|
||||
if key in self.employee_id._fields and value not in (False, "", 0)
|
||||
}
|
||||
|
||||
def action_create_employee(self):
|
||||
for bridge in self:
|
||||
if bridge.employee_id:
|
||||
continue
|
||||
bridge.employee_id = self.env["hr.employee"].sudo().create(bridge._get_employee_create_vals())
|
||||
return self.action_open_employee()
|
||||
|
||||
def _sync_from_employee(self):
|
||||
for bridge in self.filtered("employee_id"):
|
||||
employee = bridge.employee_id
|
||||
bridge.write({
|
||||
"employee_name": bridge.employee_name or employee.name,
|
||||
"employee_code": bridge.employee_code or employee.employee_id,
|
||||
"work_email": bridge.work_email or employee.work_email,
|
||||
"private_email": bridge.private_email or employee.private_email,
|
||||
"work_phone": bridge.work_phone or employee.work_phone,
|
||||
"mobile_phone": bridge.mobile_phone or employee.mobile_phone,
|
||||
"department_id": bridge.department_id.id or employee.department_id.id,
|
||||
"job_id": bridge.job_id.id or employee.job_id.id,
|
||||
"emp_type": bridge.emp_type.id or employee.emp_type.id,
|
||||
"company_id": employee.company_id.id or bridge.company_id.id,
|
||||
"doj": bridge.doj or employee.doj,
|
||||
"gender": bridge.gender or employee.gender,
|
||||
"birthday": bridge.birthday or employee.birthday,
|
||||
"blood_group": bridge.blood_group or employee.blood_group,
|
||||
"marital": bridge.marital or employee.marital,
|
||||
"marriage_anniversary_date": bridge.marriage_anniversary_date or employee.marriage_anniversary_date,
|
||||
"private_street": bridge.private_street or employee.private_street,
|
||||
"private_street2": bridge.private_street2 or employee.private_street2,
|
||||
"private_city": bridge.private_city or employee.private_city,
|
||||
"private_state_id": bridge.private_state_id.id or employee.private_state_id.id,
|
||||
"private_zip": bridge.private_zip or employee.private_zip,
|
||||
"private_country_id": bridge.private_country_id.id or employee.private_country_id.id,
|
||||
"permanent_street": bridge.permanent_street or employee.permanent_street,
|
||||
"permanent_street2": bridge.permanent_street2 or employee.permanent_street2,
|
||||
"permanent_city": bridge.permanent_city or employee.permanent_city,
|
||||
"permanent_state_id": bridge.permanent_state_id.id or employee.permanent_state_id.id,
|
||||
"permanent_zip": bridge.permanent_zip or employee.permanent_zip,
|
||||
"permanent_country_id": bridge.permanent_country_id.id or employee.permanent_country_id.id,
|
||||
"pan_no": bridge.pan_no or employee.pan_no,
|
||||
"identification_id": bridge.identification_id or employee.identification_id,
|
||||
"previous_company_pf_no": bridge.previous_company_pf_no or employee.previous_company_pf_no,
|
||||
"previous_company_uan_no": bridge.previous_company_uan_no or employee.previous_company_uan_no,
|
||||
"passport_no": bridge.passport_no or employee.passport_id,
|
||||
"passport_start_date": bridge.passport_start_date or employee.passport_start_date,
|
||||
"passport_end_date": bridge.passport_end_date or employee.passport_end_date,
|
||||
"passport_issued_location": bridge.passport_issued_location or employee.passport_issued_location,
|
||||
})
|
||||
|
||||
@api.onchange("employee_id")
|
||||
def _onchange_employee_id(self):
|
||||
for bridge in self:
|
||||
employee = bridge.employee_id
|
||||
if not employee:
|
||||
continue
|
||||
bridge.employee_name = bridge.employee_name or employee.name
|
||||
bridge.employee_code = bridge.employee_code or employee.employee_id
|
||||
bridge.work_email = bridge.work_email or employee.work_email
|
||||
bridge.private_email = bridge.private_email or employee.private_email
|
||||
bridge.work_phone = bridge.work_phone or employee.work_phone
|
||||
bridge.mobile_phone = bridge.mobile_phone or employee.mobile_phone
|
||||
bridge.department_id = bridge.department_id or employee.department_id
|
||||
bridge.job_id = bridge.job_id or employee.job_id
|
||||
bridge.emp_type = bridge.emp_type or employee.emp_type
|
||||
|
||||
def write_from_public_form(self, values):
|
||||
self.ensure_one()
|
||||
private_state = self.env["res.country.state"].sudo().browse(int(values.get("present_state") or 0))
|
||||
permanent_state = self.env["res.country.state"].sudo().browse(int(values.get("permanent_state") or 0))
|
||||
mapped_values = dict(values)
|
||||
mapped_values.update({
|
||||
"candidate_image": values.get("candidate_image_base64"),
|
||||
"private_email": values.get("email_from"),
|
||||
"mobile_phone": values.get("partner_phone"),
|
||||
"alternate_phone": values.get("alternate_phone"),
|
||||
"private_street": values.get("present_street"),
|
||||
"private_street2": values.get("present_street2"),
|
||||
"private_city": values.get("present_city"),
|
||||
"private_zip": values.get("present_zip"),
|
||||
"private_state_id": private_state.id if private_state else False,
|
||||
"private_country_id": private_state.country_id.id if private_state else False,
|
||||
"permanent_state_id": permanent_state.id if permanent_state else False,
|
||||
"permanent_country_id": permanent_state.country_id.id if permanent_state else False,
|
||||
})
|
||||
allowed_fields = {
|
||||
"employee_name",
|
||||
"candidate_image",
|
||||
"private_email",
|
||||
"mobile_phone",
|
||||
"alternate_phone",
|
||||
"doj",
|
||||
"gender",
|
||||
"birthday",
|
||||
"blood_group",
|
||||
"marital",
|
||||
"marriage_anniversary_date",
|
||||
"private_street",
|
||||
"private_street2",
|
||||
"private_city",
|
||||
"private_state_id",
|
||||
"private_country_id",
|
||||
"private_zip",
|
||||
"permanent_street",
|
||||
"permanent_street2",
|
||||
"permanent_city",
|
||||
"permanent_state_id",
|
||||
"permanent_country_id",
|
||||
"permanent_zip",
|
||||
"full_name_as_in_bank",
|
||||
"bank_name",
|
||||
"bank_branch",
|
||||
"bank_account_no",
|
||||
"bank_ifsc_code",
|
||||
"pan_no",
|
||||
"identification_id",
|
||||
"previous_company_pf_no",
|
||||
"previous_company_uan_no",
|
||||
"passport_no",
|
||||
"passport_start_date",
|
||||
"passport_end_date",
|
||||
"passport_issued_location",
|
||||
}
|
||||
bridge_vals = {
|
||||
field_name: value or False
|
||||
for field_name, value in mapped_values.items()
|
||||
if field_name in allowed_fields and field_name in self._fields
|
||||
}
|
||||
if bridge_vals:
|
||||
self.sudo().write(bridge_vals)
|
||||
self._write_history_from_public_form(values)
|
||||
self._replace_joining_attachments_from_public_form(values)
|
||||
self.state = "jod_received"
|
||||
|
||||
def _safe_date(self, value):
|
||||
try:
|
||||
return datetime.strptime(value, "%Y-%m-%d").date() if value else False
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def _safe_int(self, value):
|
||||
try:
|
||||
return int(value) if value else False
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def _json_list(self, values, key):
|
||||
raw = values.get(key, "[]")
|
||||
try:
|
||||
data = json.loads(raw) if raw else []
|
||||
except (TypeError, ValueError):
|
||||
data = []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
def _write_history_from_public_form(self, values):
|
||||
self.ensure_one()
|
||||
family_values = []
|
||||
for member in self._json_list(values, "family_data_json"):
|
||||
if member.get("name") and member.get("relation"):
|
||||
family_values.append((0, 0, {
|
||||
"relation_type": member.get("relation"),
|
||||
"name": member.get("name"),
|
||||
"contact_no": member.get("contact"),
|
||||
"dob": self._safe_date(member.get("dob")),
|
||||
"location": member.get("location"),
|
||||
}))
|
||||
|
||||
education_values = []
|
||||
for education in self._json_list(values, "education_data_json"):
|
||||
if education.get("specialization"):
|
||||
education_values.append((0, 0, {
|
||||
"education_type": education.get("education_type") or "additional",
|
||||
"name": education.get("specialization"),
|
||||
"university": education.get("university"),
|
||||
"start_year": self._safe_int(education.get("start_year")),
|
||||
"end_year": self._safe_int(education.get("end_year")),
|
||||
"marks_or_grade": education.get("marks_or_grade"),
|
||||
}))
|
||||
|
||||
employer_values = []
|
||||
for employer in self._json_list(values, "employer_history_data_json"):
|
||||
if employer.get("company_name"):
|
||||
employer_values.append((0, 0, {
|
||||
"company_name": employer.get("company_name"),
|
||||
"designation": employer.get("designation"),
|
||||
"date_of_joining": self._safe_date(employer.get("date_of_joining")),
|
||||
"last_working_day": self._safe_date(employer.get("last_working_day")),
|
||||
"ctc": employer.get("ctc"),
|
||||
}))
|
||||
|
||||
write_vals = {}
|
||||
if family_values:
|
||||
write_vals["family_details"] = [(5, 0, 0)] + family_values
|
||||
if education_values:
|
||||
write_vals["education_history"] = [(5, 0, 0)] + education_values
|
||||
if employer_values:
|
||||
write_vals["employer_history"] = [(5, 0, 0)] + employer_values
|
||||
if write_vals:
|
||||
self.sudo().write(write_vals)
|
||||
|
||||
def _replace_joining_attachments_from_public_form(self, values):
|
||||
self.ensure_one()
|
||||
attachment_values = []
|
||||
seen_files = set()
|
||||
for item in self._json_list(values, "attachments_data_json"):
|
||||
attachment = self.env["employee.bridge.requested.attachment"].sudo().browse(
|
||||
int(item.get("attachment_rec_id") or 0)
|
||||
)
|
||||
file_content = item.get("file_content")
|
||||
file_name = item.get("file_name")
|
||||
file_key = (attachment.id, file_name, file_content)
|
||||
if attachment.exists() and file_content and file_key not in seen_files:
|
||||
seen_files.add(file_key)
|
||||
attachment_values.append((0, 0, {
|
||||
"name": attachment.name,
|
||||
"requested_attachment_id": attachment.id,
|
||||
"file": file_content,
|
||||
"file_name": file_name,
|
||||
}))
|
||||
self.sudo().write({"joining_attachment_ids": [(5, 0, 0)] + attachment_values})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("name", "/") == "/":
|
||||
vals["name"] = self.env["ir.sequence"].next_by_code("recruitment.employee.bridge") or "/"
|
||||
if vals.get("employee_id") and not vals.get("company_id"):
|
||||
employee = self.env["hr.employee"].browse(vals["employee_id"])
|
||||
vals["company_id"] = employee.company_id.id or self.env.company.id
|
||||
bridges = super().create(vals_list)
|
||||
bridges._sync_from_employee()
|
||||
return bridges
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if "employee_id" in vals:
|
||||
self._sync_from_employee()
|
||||
return res
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_employee_bridge_user,employee.bridge.user,model_recruitment_employee_bridge,base.group_user,1,1,1,0
|
||||
access_employee_bridge_manager,employee.bridge.manager,model_recruitment_employee_bridge,hr.group_hr_manager,1,1,1,1
|
||||
access_employee_bridge_requested_attachment_user,employee.bridge.requested.attachment.user,model_employee_bridge_requested_attachment,base.group_user,1,1,1,0
|
||||
access_employee_bridge_requested_attachment_manager,employee.bridge.requested.attachment.manager,model_employee_bridge_requested_attachment,hr.group_hr_manager,1,1,1,1
|
||||
access_employee_bridge_attachment_user,employee.bridge.attachment.user,model_employee_bridge_attachment,base.group_user,1,1,1,0
|
||||
access_employee_bridge_attachment_manager,employee.bridge.attachment.manager,model_employee_bridge_attachment,hr.group_hr_manager,1,1,1,1
|
||||
access_employee_bridge_attachment_wizard,employee.bridge.attachment.wizard.user,model_employee_bridge_attachment_wizard,base.group_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,754 @@
|
|||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { assets, loadCSS, loadJS } from "@web/core/assets";
|
||||
|
||||
publicWidget.registry.employeeBridgeJod = publicWidget.Widget.extend({
|
||||
selector: "#post_onboarding_form",
|
||||
events: {
|
||||
"change [name='candidate_image']": "previewApplicantPhoto",
|
||||
"click #delete-photo-btn": "deleteCandidatePhoto",
|
||||
"click #preview-photo-btn": "previewFullImage",
|
||||
"click #add-education-row": "addEducationRow", // Ensure button click event is correctly bound
|
||||
|
||||
|
||||
|
||||
'change .attachment-input': 'handleAttachmentUpload',
|
||||
'click .upload-new-btn': 'handleUploadNewFile',
|
||||
'click .delete-file-btn': 'handleDeleteUploadedFile',
|
||||
'input .file-name-input': 'handleFileNameChange',
|
||||
"click .remove-file": "removeFile",
|
||||
"click .preview-file": "previewFile",
|
||||
"click .view-attachments-btn": "openAttachmentModal", // Opens modal with files
|
||||
"click .close-modal-btn": "closeAttachmentModal", // Close modal
|
||||
"submit": "handleFormSubmit",
|
||||
"change [name='experience']": "onChangeExperience",
|
||||
"change [name='marital']": "onChangeMarital",
|
||||
},
|
||||
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.uploadedFiles = {}; // Store files per attachment ID for this form only.
|
||||
this.isSubmitting = false;
|
||||
},
|
||||
|
||||
_getFileSignature(file) {
|
||||
return [file.name, file.size, file.lastModified, file.type].join("|");
|
||||
},
|
||||
|
||||
addUploadedFileRow(attachmentId, file, base64String) {
|
||||
console.log("addUploadedFileRow", attachmentId, file.name);
|
||||
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
||||
|
||||
if (!this.uploadedFiles[attachmentId]) {
|
||||
this.uploadedFiles[attachmentId] = [];
|
||||
}
|
||||
|
||||
const fileSignature = this._getFileSignature(file);
|
||||
const isDuplicate = this.uploadedFiles[attachmentId].some(
|
||||
uploadedFile => uploadedFile.signature === fileSignature
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a unique file ID using attachmentId and a timestamp
|
||||
const fileId = `${attachmentId}-${Date.now()}-${this.uploadedFiles[attachmentId].length}`;
|
||||
|
||||
const fileRecord = {
|
||||
attachment_rec_id : attachmentId,
|
||||
id: fileId, // Unique file ID
|
||||
name: file.name,
|
||||
base64: base64String,
|
||||
type: file.type,
|
||||
signature: fileSignature,
|
||||
};
|
||||
|
||||
this.uploadedFiles[attachmentId].push(fileRecord);
|
||||
|
||||
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
|
||||
const previewImageId = `preview_image_${fileId}`;
|
||||
const fileNameInputId = `file_name_input_${fileId}`;
|
||||
let previewContent = '';
|
||||
let previewClickHandler = '';
|
||||
|
||||
// Check if the file is an image or PDF and set preview content accordingly
|
||||
if (file.type.startsWith('image/')) {
|
||||
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
|
||||
</div>`;
|
||||
previewClickHandler = () => {
|
||||
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
||||
this.$('#modal_attachment_photo_preview').show();
|
||||
this.$('#modal_attachment_pdf_preview').hide(); // Hide PDF preview
|
||||
this.$('#attachmentPreviewModal').modal('show');
|
||||
};
|
||||
} else if (file.type === 'application/pdf') {
|
||||
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
|
||||
</div>`;
|
||||
previewClickHandler = () => {
|
||||
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
||||
this.$('#modal_attachment_pdf_preview').show();
|
||||
this.$('#modal_attachment_photo_preview').hide(); // Hide image preview
|
||||
this.$('#attachmentPreviewModal').modal('show');
|
||||
};
|
||||
}
|
||||
|
||||
// Append new row to the table with a preview and buttons
|
||||
tableBody.append(`
|
||||
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<td>
|
||||
<input type="text" class="form-control file-name-input" id="${fileNameInputId}" value="${file.name}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column align-items-center justify-content-center gap-2">
|
||||
${previewContent}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-danger btn-sm delete-file-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-info btn-sm preview-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
||||
|
||||
// Attach click handler for preview (image or PDF)
|
||||
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
|
||||
|
||||
// Attach click handler for the preview button (to trigger modal)
|
||||
this.$(`.preview-btn[data-attachment-id="${attachmentId}"][data-file-id="${fileId}"]`).on('click', previewClickHandler);
|
||||
},
|
||||
|
||||
handleDeleteUploadedFile(ev) {
|
||||
const button = ev.currentTarget;
|
||||
const attachmentId = $(button).data('attachment-id');
|
||||
const fileId = $(button).data('file-id');
|
||||
|
||||
// Find the index of the file to delete based on unique file ID
|
||||
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
this.uploadedFiles[attachmentId].splice(fileIndex, 1); // Remove from array
|
||||
}
|
||||
|
||||
// Remove the row from DOM
|
||||
this.$(`tr[data-file-id="${fileId}"]`).remove();
|
||||
|
||||
// Hide table if no files left
|
||||
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
handleAttachmentUpload(ev) {
|
||||
console.log("handleAttachmentUpload");
|
||||
const input = ev.target;
|
||||
const attachmentId = $(input).data('attachment-id');
|
||||
|
||||
if (input.files.length > 0) {
|
||||
Array.from(input.files).forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
$(input).val('');
|
||||
},
|
||||
|
||||
handleUploadNewFile(ev) {
|
||||
const button = $(ev.currentTarget);
|
||||
const attachmentId = button.data('attachment-id');
|
||||
const index = button.data('index');
|
||||
const hiddenInput = this.$(`.upload-new-file-input[data-attachment-id='${attachmentId}']`);
|
||||
|
||||
hiddenInput.off('change').on('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64String = event.target.result.split(',')[1];
|
||||
this.uploadedFiles[attachmentId][index] = {
|
||||
name: file.name,
|
||||
base64: base64String,
|
||||
};
|
||||
|
||||
const imageId = `preview_image_${attachmentId}_${index}`;
|
||||
const filePreviewWrapperId = `preview_wrapper_${attachmentId}_${index}`;
|
||||
const fileType = file.type;
|
||||
|
||||
if (fileType.startsWith('image/')) {
|
||||
this.$(`#${filePreviewWrapperId}`).html(`
|
||||
<img id="${imageId}" src="data:image/png;base64,${base64String}" class="img-thumbnail"
|
||||
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;" />
|
||||
`);
|
||||
} else if (fileType === 'application/pdf') {
|
||||
this.$(`#${filePreviewWrapperId}`).html(`
|
||||
<iframe src="data:application/pdf;base64,${base64String}" width="80" height="80" class="img-thumbnail" style="border: none; cursor: pointer;"></iframe>
|
||||
`);
|
||||
}
|
||||
|
||||
const fileNameInputId = `file_name_input_${attachmentId}_${index}`;
|
||||
this.$(`#${fileNameInputId}`).val(file.name);
|
||||
|
||||
// CRITICAL FIX: Clear input after reading
|
||||
$(e.target).val('');
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
hiddenInput.trigger('click');
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleFileNameChange(event) {
|
||||
const attachmentId = $(event.target).closest('tr').data('attachment-id');
|
||||
const fileId = $(event.target).closest('tr').data('file-id');
|
||||
const newFileName = event.target.value;
|
||||
|
||||
if (!attachmentId || !fileId) {
|
||||
console.error('Missing attachmentId or fileId');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileList = this.uploadedFiles[attachmentId];
|
||||
|
||||
if (!fileList) {
|
||||
console.error(`No files found for attachmentId: ${attachmentId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileRecord = fileList.find(file => file.id === fileId);
|
||||
|
||||
if (!fileRecord) {
|
||||
console.error(`File with ID ${fileId} not found under attachment ${attachmentId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
fileRecord.name = newFileName;
|
||||
},
|
||||
|
||||
|
||||
|
||||
renderFilePreview(attachmentId) {
|
||||
const container = this.$(`#preview_container_${attachmentId}`);
|
||||
container.empty();
|
||||
|
||||
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||
container.addClass("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
container.removeClass("d-none");
|
||||
|
||||
this.uploadedFiles[attachmentId].forEach((file, index) => {
|
||||
const fileHtml = $(`
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<div class="position-relative">
|
||||
<img src="${file.base64}" class="rounded-circle shadow-sm" style="width: 80px; height: 80px; object-fit: cover; border: 1px solid #ddd; cursor: pointer;" data-index="${index}" data-attachment-id="${attachmentId}" />
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 remove-file" data-attachment-id="${attachmentId}" data-index="${index}" style="transform: translate(50%, -50%);"><i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
<small>${file.name}</small>
|
||||
</div>
|
||||
`);
|
||||
|
||||
fileHtml.find("img").on("click", this.previewAttachmentImage.bind(this));
|
||||
fileHtml.find(".remove-file").on("click", this.removeFile.bind(this));
|
||||
|
||||
container.append(fileHtml);
|
||||
});
|
||||
},
|
||||
|
||||
previewAttachmentImage(ev) {
|
||||
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||
const index = $(ev.currentTarget).data("index");
|
||||
const fileData = this.uploadedFiles[attachmentId][index];
|
||||
|
||||
this.$("#attachment_modal_preview").attr("src", fileData.base64);
|
||||
this.$("#attachmentPreviewModal").modal("show");
|
||||
},
|
||||
|
||||
removeFile(ev) {
|
||||
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||
const index = $(ev.currentTarget).data("index");
|
||||
|
||||
this.uploadedFiles[attachmentId].splice(index, 1);
|
||||
this.renderFilePreview(attachmentId);
|
||||
},
|
||||
|
||||
|
||||
previewFile(ev) {
|
||||
const fileUrl = ev.currentTarget.dataset.fileUrl;
|
||||
const modal = this.$("#photoPreviewModal");
|
||||
|
||||
this.$("#modal_photo_preview").attr("src", fileUrl);
|
||||
modal.modal("show");
|
||||
},
|
||||
|
||||
onChangeExperience(event) {
|
||||
const selectedValue = $(event.currentTarget).val();
|
||||
const employerHistorySection = $("#employer_history_data");
|
||||
|
||||
if (selectedValue === "experienced") {
|
||||
employerHistorySection.show();
|
||||
} else {
|
||||
employerHistorySection.hide();
|
||||
}
|
||||
},
|
||||
|
||||
onChangeMarital(event) {
|
||||
const selectedValue = $(event.currentTarget).val();
|
||||
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
|
||||
const family_details_data_spouse = this.$('#family_details_data_spouse')
|
||||
const family_details_data_kid1 = this.$('#family_details_data_kid1')
|
||||
const family_details_data_kid2 = this.$('#family_details_data_kid2')
|
||||
|
||||
|
||||
if (selectedValue === "married") {
|
||||
marriageAnniversarySection.show();
|
||||
// Show rows for spouse, kid1, and kid2
|
||||
family_details_data_spouse.show();
|
||||
family_details_data_kid1.show();
|
||||
family_details_data_kid2.show();
|
||||
} else {
|
||||
marriageAnniversarySection.hide();
|
||||
// Hide rows for spouse, kid1, and kid2
|
||||
family_details_data_spouse.hide();
|
||||
family_details_data_kid1.hide();
|
||||
family_details_data_kid2.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open modal and display uploaded files
|
||||
*/
|
||||
openAttachmentModal(event) {
|
||||
console.log("openAttachmentModal");
|
||||
const rowId = $(event.currentTarget).closest("tr").index(); // Get the row index
|
||||
this.currentRowId = rowId; // Store rowId for reference
|
||||
this.renderAttachmentModal(rowId); // Render the modal for the row
|
||||
},
|
||||
|
||||
renderAttachmentModal(rowId) {
|
||||
const fileList = this.uploadedFiles && this.uploadedFiles[rowId] ? this.uploadedFiles[rowId] : [];
|
||||
|
||||
let modalHtml = `
|
||||
<div id="attachmentModal" class="modal fade show" tabindex="-1" style="display: block; background: rgba(0,0,0,0.5);" aria-modal="true">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 600px;">
|
||||
<div class="modal-content" style="border-radius: 8px; box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header" style="background: #143d5d; color: white; border-bottom: 2px solid #dee2e6; font-weight: bold; padding: 12px 15px;">
|
||||
<h5 class="modal-title" style="margin: 0;">Uploaded Attachments</h5>
|
||||
<button type="button" class="close close-modal-btn" data-dismiss="modal" aria-label="Close" style="background: none; border: none; font-size: 20px; color: white; cursor: pointer;">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body" style="padding: 15px;">
|
||||
<ul class="attachment-list" style="list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto;">
|
||||
${fileList.length ? fileList.map((file, index) => `
|
||||
<li style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa;">
|
||||
<span style="font-weight: 500; flex-grow: 1;">${file.name}</span>
|
||||
<button type="button" class="remove-file" data-index="${index}"
|
||||
style="background: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;">
|
||||
<i style="margin-right: 5px;">🗑️</i>
|
||||
</button>
|
||||
</li>
|
||||
`).join("") : `<p style="color: #6c757d; text-align: center; font-size: 16px; padding: 10px;">No files uploaded.</p>`}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 12px;">
|
||||
<button type="button" class="btn close-modal-btn" data-dismiss="modal"
|
||||
style="background: #6c757d; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Remove old modal and append new one
|
||||
$("#attachmentModal").remove();
|
||||
$("body").append(modalHtml);
|
||||
|
||||
// Attach remove event to new modal content
|
||||
$("#attachmentModal").on("click", ".remove-file", this.removeFile.bind(this));
|
||||
$("#attachmentModal").on("click", ".close-modal-btn", this.closeAttachmentModal.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Close attachment modal
|
||||
*/
|
||||
closeAttachmentModal() {
|
||||
console.log("Closing modal");
|
||||
$("#attachmentModal").remove(); // Remove the modal from the DOM
|
||||
},
|
||||
|
||||
|
||||
// Function to preview the uploaded image
|
||||
previewApplicantPhoto(ev) {
|
||||
const input = ev.currentTarget;
|
||||
const preview = this.$("#photo_preview");
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
|
||||
preview.attr("src", e.target.result);
|
||||
|
||||
// Store the base64 in a hidden input field
|
||||
this.$("input[name='candidate_image_base64']").val(base64String);
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
},
|
||||
|
||||
// Function to delete the uploaded image
|
||||
deleteCandidatePhoto() {
|
||||
const preview = this.$("#photo_preview");
|
||||
const inputFile = this.$("input[name='candidate_image']");
|
||||
|
||||
preview.attr("src", "data:image/png;base64,"); // Reset preview
|
||||
inputFile.val(""); // Reset file input
|
||||
},
|
||||
|
||||
// Function to preview full image inside a modal
|
||||
previewFullImage() {
|
||||
const previewSrc = this.$("#photo_preview").attr("src");
|
||||
if (previewSrc) {
|
||||
this.$("#modal_photo_preview").attr("src", previewSrc);
|
||||
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
|
||||
}
|
||||
},
|
||||
|
||||
// Function to add a new education row dynamically
|
||||
addEducationRow(ev) {
|
||||
ev.preventDefault(); // Prevent default behavior
|
||||
|
||||
let newRow = `
|
||||
<tr class="predefined-row">
|
||||
<td class="education-relation-col">
|
||||
<select name="education_type" class="form-control">
|
||||
<option value="10">10th</option>
|
||||
<option value="inter">Inter</option>
|
||||
<option value="graduation">Graduation</option>
|
||||
<option value="post_graduation">Post Graduation</option>
|
||||
<option value="additional">Additional Qualification</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="specialization" class="form-control" placeholder="Enter Specialization"/></td>
|
||||
<td><input type="text" name="university" class="form-control" placeholder="Enter University"/></td>
|
||||
<td><input type="number" name="start_year" class="form-control" placeholder="Start Year"/></td>
|
||||
<td><input type="number" name="end_year" class="form-control" placeholder="End Year"/></td>
|
||||
<td><input type="text" name="marks_grade" class="form-control" placeholder="Marks/Grade"/></td>
|
||||
</tr>
|
||||
|
||||
`;
|
||||
|
||||
this.$("#education_details_data tbody").append(newRow); // Append new row inside the table
|
||||
console.log("New education row added!");
|
||||
},
|
||||
|
||||
|
||||
validateFamilyDetails() {
|
||||
let familyDetailsFilled = false;
|
||||
let educationDetailsFilled = false;
|
||||
const family_rows = document.querySelectorAll('#family_details_data tbody tr');
|
||||
const education_rows = document.querySelectorAll('#education_details_data tbody tr');
|
||||
|
||||
|
||||
family_rows.forEach(row => {
|
||||
const inputs = row.querySelectorAll('input[type="text"], input[type="date"]');
|
||||
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
|
||||
if (isRowFilled) {
|
||||
familyDetailsFilled = true;
|
||||
}
|
||||
});
|
||||
|
||||
education_rows.forEach(row => {
|
||||
const inputs = row.querySelectorAll('input[type="text"]');
|
||||
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
|
||||
if (isRowFilled) {
|
||||
educationDetailsFilled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!familyDetailsFilled) {
|
||||
alert('Please fill at least one family member detail.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!educationDetailsFilled) {
|
||||
alert('Please fill at least one Education details')
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
handleFormSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.validateFamilyDetails()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
this.$("#submit-btn").prop("disabled", true);
|
||||
|
||||
let employerHistoryData = [];
|
||||
this.$("#employer_history_data tbody tr").each((index, row) => {
|
||||
let rowData = {
|
||||
company_name: this.$(row).find("[name$='company_name']").val()?.trim() || "",
|
||||
designation: this.$(row).find("[name$='designation']").val()?.trim() || "",
|
||||
date_of_joining: this.$(row).find("[name$='doj']").val()?.trim() || "",
|
||||
last_working_day: this.$(row).find("[name$='lwd']").val()?.trim() || "",
|
||||
ctc: this.$(row).find("[name$='ctc']").val()?.trim() || "",
|
||||
};
|
||||
if (Object.values(rowData).some(value => value)) {
|
||||
employerHistoryData.push(rowData);
|
||||
}
|
||||
});
|
||||
|
||||
let familyData = [];
|
||||
this.$("#family_details_data tbody tr").each((index, row) => {
|
||||
let rowData = {
|
||||
relation: this.$(row).find(".relation-col").attr("value")?.trim(),
|
||||
name: this.$(row).find("[name$='_name']").val()?.trim() || "",
|
||||
contact: this.$(row).find("[name$='_contact']").val()?.trim() || "",
|
||||
dob: this.$(row).find("[name$='_dob']").val()?.trim() || "",
|
||||
location: this.$(row).find("[name$='_location']").val()?.trim() || "",
|
||||
};
|
||||
if (Object.entries(rowData).some(([key, value]) => key !== 'relation' && value)) {
|
||||
familyData.push(rowData);
|
||||
}
|
||||
});
|
||||
|
||||
let educationData = [];
|
||||
this.$("#education_details_data tbody tr").each((index, row) => {
|
||||
let educationCell = this.$(row).find(".education-relation-col");
|
||||
let educationType = educationCell.find("select").val()?.trim() || educationCell.attr("value")?.trim();
|
||||
|
||||
let rowData = {
|
||||
education_type: educationType,
|
||||
specialization: this.$(row).find("[name='specialization']").val()?.trim() || "",
|
||||
university: this.$(row).find("[name='university']").val()?.trim() || "",
|
||||
start_year: this.$(row).find("[name='start_year']").val()?.trim() || "",
|
||||
end_year: this.$(row).find("[name='end_year']").val()?.trim() || "",
|
||||
marks_or_grade: this.$(row).find("[name='marks_grade']").val()?.trim() || "",
|
||||
};
|
||||
if (Object.entries(rowData).some(([key, value]) => key !== 'education_type' && value)) {
|
||||
educationData.push(rowData);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let attachments = [];
|
||||
let fileReadPromises = [];
|
||||
let attachmentInputs = this.uploadedFiles; // your object {1: Array(1), 2: Array(2)}
|
||||
|
||||
Object.keys(attachmentInputs).forEach(key => {
|
||||
let filesArray = attachmentInputs[key];
|
||||
if (filesArray && Array.isArray(filesArray)) {
|
||||
filesArray.forEach(file => {
|
||||
// Only push if base64 actually exists (prevents ghost/empty data)
|
||||
if (file && file.base64) {
|
||||
attachments.push({
|
||||
attachment_rec_id: file.attachment_rec_id,
|
||||
file_name: file.name,
|
||||
file_content: file.base64,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Promise.all(fileReadPromises).then(() => {
|
||||
this.$("#family_data_json").val(JSON.stringify(familyData));
|
||||
this.$("#education_data_json").val(JSON.stringify(educationData));
|
||||
this.$("#employer_history_data_json").val(JSON.stringify(employerHistoryData));
|
||||
this.$("#attachments_data_json").val(JSON.stringify(attachments));
|
||||
|
||||
let formElement = this.$el.is("form") ? this.$el[0] : this.$el.find("form")[0];
|
||||
if (formElement) {
|
||||
formElement.submit();
|
||||
} else {
|
||||
console.error("Form element not found.");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Error reading files:", error);
|
||||
this.isSubmitting = false;
|
||||
this.$("#submit-btn").prop("disabled", false);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Enforce required fields when any field in a row is filled
|
||||
enforceRowValidation() {
|
||||
// Education Details Validation - Make fields required when typing
|
||||
this.$("#education_details_data").on("input", "tbody tr input", function () {
|
||||
let row = $(this).closest("tr");
|
||||
let inputs = row.find("input");
|
||||
|
||||
// Check if at least one field has a value
|
||||
let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
|
||||
|
||||
if (isFilled) {
|
||||
inputs.attr("required", true);
|
||||
} else {
|
||||
inputs.removeAttr("required"); // Remove required if all fields are empty
|
||||
}
|
||||
});
|
||||
|
||||
// Remove empty rows only when the user leaves the last input field
|
||||
this.$("#education_details_data").on("blur", "tbody tr input", function () {
|
||||
let row = $(this).closest("tr");
|
||||
let inputs = row.find("input");
|
||||
|
||||
if (row.hasClass("predefined-row")) {
|
||||
let isEmpty = inputs.toArray().every(input => $(input).val().trim() === "");
|
||||
if (isEmpty) {
|
||||
row.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Family Details Validation
|
||||
// this.$("#family_details_data").on("input", "tbody tr input", function () {
|
||||
// let row = $(this).closest("tr");
|
||||
// let inputs = row.find("input");
|
||||
//
|
||||
// let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
|
||||
//
|
||||
// if (isFilled) {
|
||||
// inputs.attr("required", true);
|
||||
// } else {
|
||||
// inputs.removeAttr("required");
|
||||
// }
|
||||
// });
|
||||
|
||||
// Remove row functionality (Manual delete using button)
|
||||
this.$("#education_details_data").on("click", ".remove-edu-row", function () {
|
||||
$(this).closest("tr").remove();
|
||||
});
|
||||
|
||||
this.$("#family_details_data").on("click", ".remove-family-row", function () {
|
||||
$(this).closest("tr").remove();
|
||||
});
|
||||
},
|
||||
|
||||
async _renderStateIds() {
|
||||
console.log("Fetching States...");
|
||||
|
||||
const country_id = $('#present_state_ids_container').data('country_id');
|
||||
|
||||
const state_ids = await rpc("/employee_bridge/fetch_related_state_ids", {
|
||||
country_id: country_id,
|
||||
});
|
||||
|
||||
const present_state_ids_container = $("#present_state_ids_container");
|
||||
const permanent_state_ids_container = $("#permanent_state_ids_container");
|
||||
|
||||
present_state_ids_container.empty();
|
||||
permanent_state_ids_container.empty();
|
||||
|
||||
console.log(state_ids);
|
||||
|
||||
if (typeof state_ids === 'object' && !Array.isArray(state_ids)) {
|
||||
const stateOptions = Object.entries(state_ids).map(([id, name]) => `
|
||||
<option value="${id}">${name}</option>
|
||||
`).join('');
|
||||
|
||||
const stateHtml = `
|
||||
<select name='permanent_state' id="permanent_state" class="form-control" required>
|
||||
<option value="" disabled selected>Select State</option>
|
||||
${stateOptions}
|
||||
</select>
|
||||
`;
|
||||
|
||||
const presentStateHtml = `
|
||||
<select name='present_state' id="present_state" class="form-control" required>
|
||||
<option value="" disabled selected>Select State</option>
|
||||
${stateOptions}
|
||||
</select>
|
||||
`;
|
||||
|
||||
permanent_state_ids_container.append(stateHtml);
|
||||
present_state_ids_container.append(presentStateHtml);
|
||||
} else {
|
||||
console.error("Expected an object like {id: name}, but got:", state_ids);
|
||||
}
|
||||
|
||||
console.log("Hello World");
|
||||
},
|
||||
|
||||
|
||||
|
||||
async start() {
|
||||
this._super(...arguments);
|
||||
this._renderStateIds();
|
||||
|
||||
// Ensure form submit event is properly bound
|
||||
// this.$el.on("submit", this.handleFormSubmit.bind(this));
|
||||
|
||||
// Bind validation enforcement
|
||||
this.enforceRowValidation();
|
||||
const selectedExperience = this.$("[name='experience']:checked").val();
|
||||
const selectedMarital = this.$("[name='marital']:checked").val();
|
||||
|
||||
const employerHistorySection = this.$("#employer_history_data");
|
||||
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
|
||||
|
||||
const family_details_data_spouse = this.$('#family_details_data_spouse')
|
||||
const family_details_data_kid1 = this.$('#family_details_data_kid1')
|
||||
const family_details_data_kid2 = this.$('#family_details_data_kid2')
|
||||
|
||||
|
||||
if (selectedExperience === "experienced") {
|
||||
employerHistorySection.show();
|
||||
} else {
|
||||
employerHistorySection.hide();
|
||||
}
|
||||
|
||||
if (selectedMarital === 'married') {
|
||||
marriageAnniversarySection.show();
|
||||
// Show rows for spouse, kid1, and kid2 if married
|
||||
family_details_data_spouse.show();
|
||||
family_details_data_kid1.show();
|
||||
family_details_data_kid2.show();
|
||||
} else {
|
||||
marriageAnniversarySection.hide();
|
||||
// Show rows for spouse, kid1, and kid2 if married
|
||||
family_details_data_spouse.hide();
|
||||
family_details_data_kid1.hide();
|
||||
family_details_data_kid2.hide();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="hr_employee_view_form_bridge" model="ir.ui.view">
|
||||
<field name="name">hr.employee.view.form.bridge</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_recruitment_bridge"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-link"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not recruitment_bridge_count">
|
||||
<field name="recruitment_bridge_count" widget="statinfo" string="Onboarding"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//label[@for='user_id']" position="before">
|
||||
<button string="Send JOD Form"
|
||||
name="send_jod_form_to_employee"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not work_email"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_bridge_requested_attachment_list" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.requested.attachment.list</field>
|
||||
<field name="model">employee.bridge.requested.attachment</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="attachment_type"/>
|
||||
<field name="is_default"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_bridge_requested_attachment_form" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.requested.attachment.form</field>
|
||||
<field name="model">employee.bridge.requested.attachment</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="attachment_type"/>
|
||||
<field name="is_default"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_employee_bridge_requested_attachment" model="ir.actions.act_window">
|
||||
<field name="name">Onboarding Documents</field>
|
||||
<field name="res_model">employee.bridge.requested.attachment</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_employee_bridge_requested_attachment"
|
||||
name="Onboarding Documents"
|
||||
parent="menu_employee_bridge"
|
||||
action="action_employee_bridge_requested_attachment"
|
||||
sequence="20"
|
||||
groups="hr.group_hr_user"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_bridge_list" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.list</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="employee_name"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="work_email"/>
|
||||
<field name="joining_form_link" optional="show"/>
|
||||
<field name="source"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_bridge_form" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.form</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button string="Send JOD Form" name="action_send_jod_form" type="object" class="btn-primary" invisible="state == 'validated'"/>
|
||||
<button string="Create Employee" name="action_create_employee" type="object" class="btn-secondary" invisible="employee_id"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,jod_sent,jod_received,validated"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_open_employee" type="object" class="oe_stat_button" icon="fa-id-card-o" invisible="not employee_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="employee_code" readonly="1"/></span>
|
||||
<span class="o_stat_text">Employee</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group string="Onboarding">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="source"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="joining_form_link" widget="url"/>
|
||||
<field name="requested_attachment_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</group>
|
||||
<button string="Validate Into Employee" name="action_validate_employee_details" type="object" class="btn-primary" invisible="state == 'validated' or not employee_id"/>
|
||||
<notebook>
|
||||
<page string="Basic Details" name="basic_details">
|
||||
<group>
|
||||
<group>
|
||||
<field name="employee_name"/>
|
||||
<field name="candidate_image" widget="image" class="oe_avatar"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="work_email" widget="email"/>
|
||||
<field name="private_email" widget="email"/>
|
||||
<field name="work_phone" widget="phone"/>
|
||||
<field name="mobile_phone" widget="phone"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="emp_type"/>
|
||||
<field name="total_exp"/>
|
||||
<field name="doj"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="JOD Details" name="jod_details">
|
||||
<group string="Personal Details">
|
||||
<group>
|
||||
<field name="gender"/>
|
||||
<field name="birthday"/>
|
||||
<field name="blood_group"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="marital"/>
|
||||
<field name="marriage_anniversary_date" invisible="marital == 'single'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Current Address">
|
||||
<group>
|
||||
<field name="private_street"/>
|
||||
<field name="private_street2"/>
|
||||
<field name="private_city"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="private_state_id"/>
|
||||
<field name="private_zip"/>
|
||||
<field name="private_country_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Permanent Address">
|
||||
<group>
|
||||
<field name="permanent_street"/>
|
||||
<field name="permanent_street2"/>
|
||||
<field name="permanent_city"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="permanent_state_id"/>
|
||||
<field name="permanent_zip"/>
|
||||
<field name="permanent_country_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Authentication">
|
||||
<group>
|
||||
<field name="pan_no"/>
|
||||
<field name="identification_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="previous_company_pf_no"/>
|
||||
<field name="previous_company_uan_no"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Passport">
|
||||
<group>
|
||||
<field name="passport_no"/>
|
||||
<field name="passport_start_date"/>
|
||||
<field name="passport_end_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="passport_issued_location"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Family Details" name="family_details">
|
||||
<field name="family_details" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="relation_type"/>
|
||||
<field name="name"/>
|
||||
<field name="contact_no"/>
|
||||
<field name="dob"/>
|
||||
<field name="location"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Employer History" name="employer_history">
|
||||
<field name="employer_history" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="company_name"/>
|
||||
<field name="designation"/>
|
||||
<field name="date_of_joining"/>
|
||||
<field name="last_working_day"/>
|
||||
<field name="ctc"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Education Details" name="education_history">
|
||||
<field name="education_history" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="education_type"/>
|
||||
<field name="name"/>
|
||||
<field name="university"/>
|
||||
<field name="start_year"/>
|
||||
<field name="end_year"/>
|
||||
<field name="marks_or_grade"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Attachments" name="attachments">
|
||||
<field name="joining_attachment_ids" nolabel="1">
|
||||
<list editable="bottom" default_group_by="requested_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
|
||||
<field name="requested_attachment_id"/>
|
||||
<field name="name"/>
|
||||
<field name="attachment_type"/>
|
||||
<field name="file" widget="binary" filename="file_name"/>
|
||||
<field name="file_name" optional="hide"/>
|
||||
<button name="action_preview_file" type="object" string="Preview" class="oe_highlight" icon="fa-eye"/>
|
||||
<field name="review_status"/>
|
||||
<field name="review_comments" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_bridge_search" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.search</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="employee_id"/>
|
||||
<filter name="jod_sent" string="JOD Sent" domain="[('state', '=', 'jod_sent')]"/>
|
||||
<filter name="jod_received" string="JOD Received" domain="[('state', '=', 'jod_received')]"/>
|
||||
<filter name="validated" string="Validated" domain="[('state', '=', 'validated')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
|
||||
<filter name="group_source" string="Source" context="{'group_by': 'source'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_employee_bridge" model="ir.actions.act_window">
|
||||
<field name="name">Onboarding</field>
|
||||
<field name="res_model">recruitment.employee.bridge</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_employee_bridge"
|
||||
name="Onboarding"
|
||||
parent="hr.menu_hr_root"
|
||||
sequence="90"
|
||||
groups="hr.group_hr_user"/>
|
||||
|
||||
<menuitem id="menu_employee_bridge_onboarding"
|
||||
name="Onboarding"
|
||||
parent="menu_employee_bridge"
|
||||
action="action_employee_bridge"
|
||||
sequence="10"
|
||||
groups="hr.group_hr_user"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import employee_bridge_attachment_wizard
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EmployeeBridgeAttachmentWizard(models.TransientModel):
|
||||
_name = "employee.bridge.attachment.wizard"
|
||||
_description = "Onboarding Attachment Request Wizard"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, readonly=True)
|
||||
req_attachment_ids = fields.Many2many(
|
||||
comodel_name="employee.bridge.requested.attachment",
|
||||
relation="employee_bridge_req_attachment_rel", # relation table name
|
||||
column1="request_id", # column referring to this model
|
||||
column2="attachment_id", # column referring to the comodel
|
||||
string="Attachments to Request",
|
||||
)
|
||||
attachment_ids = fields.Many2many("ir.attachment")
|
||||
template_id = fields.Many2one(
|
||||
"mail.template",
|
||||
default=lambda self: self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False),
|
||||
readonly=True,
|
||||
)
|
||||
email_from = fields.Char("Email From")
|
||||
email_to = fields.Char("Email To")
|
||||
email_cc = fields.Text("Email CC")
|
||||
email_subject = fields.Char()
|
||||
email_body = fields.Html(
|
||||
"Body",
|
||||
render_engine="qweb",
|
||||
render_options={"post_process": True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize="email_outgoing",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
bridge = self.env["recruitment.employee.bridge"].browse(
|
||||
self.env.context.get("default_bridge_id") or self.env.context.get("bridge_id")
|
||||
)
|
||||
if bridge:
|
||||
attachments = bridge.requested_attachment_ids or self.env["employee.bridge.requested.attachment"].search([
|
||||
("is_default", "=", True),
|
||||
])
|
||||
defaults.update({
|
||||
"bridge_id": bridge.id,
|
||||
"req_attachment_ids": [(6, 0, attachments.ids)],
|
||||
"email_from": self.env.company.email or self.env.user.email,
|
||||
"email_to": bridge.work_email or bridge.private_email or bridge.employee_id.work_email,
|
||||
})
|
||||
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||
if template:
|
||||
context = {
|
||||
"joining_form_link": bridge._get_joining_form_link(),
|
||||
**bridge._group_requested_attachments(attachments),
|
||||
}
|
||||
defaults.update({
|
||||
"template_id": template.id,
|
||||
"email_subject": template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id],
|
||||
"email_body": template.with_context(**context)._render_field(
|
||||
"body_html", [bridge.id], compute_lang=True
|
||||
)[bridge.id],
|
||||
})
|
||||
return defaults
|
||||
|
||||
@api.onchange("req_attachment_ids")
|
||||
def _onchange_req_attachment_ids(self):
|
||||
if not self.bridge_id or not self.template_id:
|
||||
return
|
||||
context = {
|
||||
"joining_form_link": self.bridge_id._get_joining_form_link(),
|
||||
**self.bridge_id._group_requested_attachments(self.req_attachment_ids),
|
||||
}
|
||||
self.email_body = self.template_id.with_context(**context)._render_field(
|
||||
"body_html", [self.bridge_id.id], compute_lang=True
|
||||
)[self.bridge_id.id]
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
self.bridge_id.requested_attachment_ids = [(6, 0, self.req_attachment_ids.ids)]
|
||||
self.bridge_id._send_jod_mail(
|
||||
req_attachment_ids=self.req_attachment_ids,
|
||||
email_values={
|
||||
"email_from": self.email_from,
|
||||
"email_to": self.email_to,
|
||||
"email_cc": self.email_cc,
|
||||
"subject": self.email_subject,
|
||||
"attachment_ids": [(6, 0, self.attachment_ids.ids)],
|
||||
},
|
||||
email_body=self.email_body,
|
||||
)
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_bridge_attachment_wizard_form" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.attachment.wizard.form</field>
|
||||
<field name="model">employee.bridge.attachment.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Select Attachments">
|
||||
<group>
|
||||
<field name="bridge_id" invisible="1"/>
|
||||
<field name="req_attachment_ids" widget="many2many_tags" force_save="1"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="attachment" string="Attachments (Binary)">
|
||||
<field name="attachment_ids" widget="many2many_binary" domain="[('mimetype', 'not ilike', 'image')]"/>
|
||||
</page>
|
||||
<page name="email" string="Email">
|
||||
<group>
|
||||
<field name="template_id" options="{'no_create': True}" readonly="1" force_save="1"/>
|
||||
<field name="email_from" force_save="1"/>
|
||||
<field name="email_to" force_save="1"/>
|
||||
<field name="email_cc"/>
|
||||
<field name="email_subject" force_save="1"/>
|
||||
<field name="email_body" widget="html_mail" class="oe-bordered-editor" options="{'codeview': true}" force_save="1"/>
|
||||
<field name="attachment_ids" widget="many2many_tags" string="Attachments" force_save="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object" string="Send JOD Form" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -10,12 +10,10 @@
|
|||
""",
|
||||
'author': 'FTPROTECH',
|
||||
'website': 'https://ftprotech.in',
|
||||
'depends': ['hr', 'mail','hr_employee_extended','hr_recruitment_extended'],
|
||||
'depends': ['hr', 'mail', 'hr_employee_extended', 'employee_bridge'],
|
||||
'data': [
|
||||
'data/data.xml',
|
||||
'data/actions.xml',
|
||||
'data/template.xml',
|
||||
'views/emp_jod.xml',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +1,22 @@
|
|||
from odoo import http, _
|
||||
from odoo import http
|
||||
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
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class website_hr_recruitment_applications_extended(website_hr_recruitment_applications):
|
||||
|
||||
@http.route(['/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
||||
website=True)
|
||||
def post_onboarding_form(self, applicant_id, **kwargs):
|
||||
"""Renders the website form for applicants to submit additional details."""
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
return request.not_found()
|
||||
if applicant:
|
||||
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:
|
||||
class EmployeeJodController(http.Controller):
|
||||
@http.route("/download/employee_jod/<int:employee_id>", type="http", auth="user")
|
||||
def download_employee_jod_form(self, employee_id, **kwargs):
|
||||
employee = request.env["hr.employee"].sudo().browse(employee_id)
|
||||
if not employee.exists():
|
||||
return request.not_found()
|
||||
|
||||
@http.route(['/download/jod/<int:applicant_id>'], type='http', auth="public", cors='*', website=True)
|
||||
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')
|
||||
template = request.env.ref("employee_jod.emp_joining_form_template", raise_if_not_found=False)
|
||||
if not template:
|
||||
return "Error: Template not found"
|
||||
|
||||
try:
|
||||
# Render the template to HTML for debugging
|
||||
html = request.env['ir.qweb']._render(
|
||||
template.id,
|
||||
{
|
||||
'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)}"
|
||||
return request.env["ir.qweb"]._render(template.id, {
|
||||
"docs": employee,
|
||||
"doc": employee,
|
||||
"time": misc.datetime,
|
||||
"user": request.env.user,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
<field name="report_file">employee_jod.emp_joining_form_template</field>
|
||||
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
||||
<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>
|
||||
</record>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Original job record -->
|
||||
<record model="hr.job" id="employee_jod_internal_job_id">
|
||||
<field name="name">Internal Job</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Recruitment stage -->
|
||||
<record model="hr.recruitment.stage" id="hired_stage8">
|
||||
<field name="name">IJ</field>
|
||||
<field name="is_default_field" eval="False"/>
|
||||
<field name="hired_stage" eval="True"/>
|
||||
<!-- <field name="post_onboarding_form" eval="True"/>-->
|
||||
</record>
|
||||
|
||||
<!-- Changed ID for the recruitment record -->
|
||||
<record model="hr.job.recruitment" id="employee_jod_internal_job_recruitment_id">
|
||||
<field name="recruitment_sequence">IJ001</field>
|
||||
<field name="job_id" ref="employee_jod_internal_job_id"/>
|
||||
<field name="recruitment_stage_ids" eval="[(4, ref('hired_stage8'))]"/>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -313,13 +313,4 @@
|
|||
</t>
|
||||
</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>
|
||||
|
|
@ -1,165 +1,8 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import models
|
||||
|
||||
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
|
||||
joining_form_link = fields.Char()
|
||||
|
||||
|
||||
class HREmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
applicant_id = fields.Many2one("hr.applicant")
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
for rec in self:
|
||||
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')
|
||||
return super().send_jod_form_to_employee()
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="hr_employee_view_form_applicant_id" model="ir.ui.view">
|
||||
<field name="name">hr.employee.view.form.applicant.id</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//label[@for='user_id']" position="before">
|
||||
<field name="applicant_id" string="Application ID" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_post_onboarding_attachment_wizard_form_inherit" model="ir.ui.view">
|
||||
<field name="name">post.onboarding.attachment.wizard.form.inherit</field>
|
||||
<field name="model">post.onboarding.attachment.wizard</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.view_post_onboarding_attachment_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='req_attachment_ids']" position="after">
|
||||
<field name="send_mail"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="attributes">
|
||||
<attribute name="invisible">not send_mail</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_confirm']" position="attributes">
|
||||
<attribute name="string">Send</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="hr_applicant_view_form_inherit_extend" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.extend</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='candidate_id']" position="after">
|
||||
<field name="joining_form_link" force_save="1" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AttendanceAnalytics(models.Model):
|
||||
|
|
@ -64,6 +65,8 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
('absent', 'Absent'),
|
||||
|
||||
('invalid_attendance', 'Invalid Attendance'),
|
||||
|
||||
('half_day', 'Half Day'),
|
||||
|
||||
('late_in', 'Late In'),
|
||||
|
|
@ -192,7 +195,7 @@ class AttendanceAnalytics(models.Model):
|
|||
for rec in self:
|
||||
if rec.status == 'present':
|
||||
rec.color = 10
|
||||
elif rec.status == 'absent':
|
||||
elif rec.status in ('absent', 'invalid_attendance'):
|
||||
rec.color = 1
|
||||
elif rec.status == 'half_day':
|
||||
rec.color = 2
|
||||
|
|
@ -280,6 +283,11 @@ class AttendanceAnalytics(models.Model):
|
|||
def action_create_shiftswap_request(self):
|
||||
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([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('roster_date', '=', self.date),
|
||||
|
|
@ -324,6 +332,7 @@ class AttendanceAnalytics(models.Model):
|
|||
emp.department_id,
|
||||
emp.resource_calendar_id,
|
||||
emp.attendance_mode,
|
||||
emp.work_mode,
|
||||
generate_series(
|
||||
DATE(emp.create_date),
|
||||
CURRENT_DATE,
|
||||
|
|
@ -340,7 +349,11 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
att.employee_id,
|
||||
|
||||
DATE(att.check_in)
|
||||
DATE(
|
||||
att.check_in
|
||||
AT TIME ZONE 'UTC'
|
||||
AT TIME ZONE 'Asia/Kolkata'
|
||||
)
|
||||
AS attendance_date,
|
||||
|
||||
MIN(att.check_in)
|
||||
|
|
@ -349,6 +362,18 @@ class AttendanceAnalytics(models.Model):
|
|||
MAX(att.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)
|
||||
AS worked_hours
|
||||
|
||||
|
|
@ -358,7 +383,11 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
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
|
||||
AND rl.date_to IS NOT 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
|
||||
|
|
@ -438,6 +493,7 @@ class AttendanceAnalytics(models.Model):
|
|||
ed.employee_id,
|
||||
ed.department_id,
|
||||
ed.attendance_mode AS attendance_mode,
|
||||
ed.work_mode,
|
||||
rc.id AS shift_id,
|
||||
rc.name AS shift_name,
|
||||
ed.date,
|
||||
|
|
@ -448,20 +504,20 @@ class AttendanceAnalytics(models.Model):
|
|||
ats.worked_hours,
|
||||
0
|
||||
) 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)
|
||||
) AS allowed_ot_limit,
|
||||
|
||||
CASE
|
||||
|
||||
WHEN ats.worked_hours > rc.hours_per_day
|
||||
WHEN ats.worked_hours > es.hours_per_day
|
||||
|
||||
THEN
|
||||
ats.worked_hours - rc.hours_per_day
|
||||
ats.worked_hours - es.hours_per_day
|
||||
|
||||
ELSE 0
|
||||
|
||||
|
|
@ -472,7 +528,7 @@ class AttendanceAnalytics(models.Model):
|
|||
WHEN ats.worked_hours >
|
||||
|
||||
(
|
||||
rc.hours_per_day
|
||||
es.hours_per_day
|
||||
+
|
||||
COALESCE(rc.over_time_hrs, 0)
|
||||
)
|
||||
|
|
@ -525,19 +581,11 @@ class AttendanceAnalytics(models.Model):
|
|||
AS department_grace_period,
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
) AS expected_check_in,
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
) AS expected_check_out,
|
||||
|
||||
CASE
|
||||
|
|
@ -549,28 +597,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -589,28 +629,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
>
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
)
|
||||
|
||||
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(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -669,7 +693,7 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
END
|
||||
|
||||
) AS required_checkout_time,
|
||||
) / 60.0 AS required_checkout_time,
|
||||
|
||||
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(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -737,13 +752,13 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
)
|
||||
),
|
||||
|
||||
|
|
@ -765,13 +780,13 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
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(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -840,7 +846,7 @@ class AttendanceAnalytics(models.Model):
|
|||
WHEN
|
||||
(
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
+
|
||||
|
|
@ -856,29 +862,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -895,13 +892,13 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
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
|
||||
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'
|
||||
|
||||
WHEN
|
||||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
>
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
)
|
||||
|
||||
THEN 'late_in'
|
||||
|
|
@ -968,20 +960,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
)
|
||||
)
|
||||
|
||||
<
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
THEN 'early_out'
|
||||
|
|
@ -1024,6 +1016,36 @@ class AttendanceAnalytics(models.Model):
|
|||
LEFT JOIN resource_calendar rc
|
||||
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
|
||||
ON dg.calendar_id
|
||||
= ed.resource_calendar_id
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
<field name="name">attendance.analytics.list</field>
|
||||
<field name="model">attendance.analytics</field>
|
||||
<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="department_id"/>
|
||||
<field name="date"/>
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
<field name="status"
|
||||
widget="badge"
|
||||
decoration-success="status == 'present'"
|
||||
decoration-danger="status == 'absent'"
|
||||
decoration-danger="status == 'absent' or status == 'invalid_attendance'"
|
||||
decoration-warning="status == 'late_in'"
|
||||
decoration-info="status == 'half_day'"
|
||||
decoration-primary="status == 'holiday'"/>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,32 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<template id="report_payslip">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2 id="payslip_name"><span t-field="o.name">August 2023 Payslip</span></h2>
|
||||
<t t-set="is_invalid" t-value="o._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>
|
||||
</div>
|
||||
<div t-else="">
|
||||
<div class="oe_structure"></div>
|
||||
</div>
|
||||
<div id="infos_table">
|
||||
<table class="table table-sm table-borderless">
|
||||
<thead class="o_black_border">
|
||||
<tr>
|
||||
<template id="report_payslip">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<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()"/>
|
||||
<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>
|
||||
</div>
|
||||
<div t-else="">
|
||||
<div class="oe_structure"></div>
|
||||
</div>
|
||||
<div id="infos_table">
|
||||
<table class="table table-sm table-borderless">
|
||||
<thead class="o_black_border">
|
||||
<tr>
|
||||
<th>Employee Information</th>
|
||||
<th>Other Information</th>
|
||||
</tr>
|
||||
|
|
@ -28,7 +40,7 @@
|
|||
</div>
|
||||
<div id="employee_id">
|
||||
<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>
|
||||
</div>
|
||||
<div id="employee_email" t-if="o.employee_id.work_email">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<separator string="Employees Selection"/>
|
||||
<div class="o_row ms-2">
|
||||
<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."/>
|
||||
<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."/>
|
||||
|
|
@ -95,4 +95,27 @@
|
|||
action = env['hr.payslip.employees'].create({}).compute_sheet()
|
||||
</field>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<odoo>
|
||||
<menuitem
|
||||
id="hr_payroll.menu_hr_payroll_employees_root"
|
||||
name="Contracts"
|
||||
name="Employee Salary Contract"
|
||||
parent="hr_payroll.menu_hr_payroll_root"
|
||||
sequence="1"
|
||||
action="hr_contract.action_hr_contract"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
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_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
|
||||
|
|
|
|||
|
Gitea Version: 1.21.4 |
