srivyn_test #32

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

View File

@ -107,7 +107,8 @@ RUN if [ -f requirements.txt ]; then \
pypdf \
phonenumbers \
python-docx \
pyzk
pyzk \
firebase-admin
# ------------------------------------------------------------------
# Create Required Directories

View File

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

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
{
'name': 'Base Changes',
'category': 'Base',
'version': '1.1',
'author': 'Seshi Kanth',
'summary': 'Changes of the form',
'description': 'This module contains form changes.',
'depends': [
'base',
'web',
],
'data': [
# 'views/user_menu.xml',
],
'assets': {
'web.assets_backend': [
'base_custom/static/src/css/backend.css',
'base_custom/static/src/js/user_menu_patch.js',
'base_custom/static/src/js/hide_user_menu.js',
'base_custom/static/src/xml/user_menu.xml',
],
},
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

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

View File

@ -0,0 +1,17 @@
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = "ir.http"
def session_info(self):
session_info = super().session_info()
employee = self.env.user.employee_id
session_info.update({
"employee_name": employee.name or self.env.user.name,
"employee_designation": employee.job_id.name if employee.job_id else "",
})
return session_info

View File

@ -0,0 +1,23 @@
.o_switch_company_menu .oe_topbar_name{
display: none !important;
}
.oe_topbar_name{
display:flex;
flex-direction:column;
line-height:1.2;
}
.oe_topbar_name .fw-bold{
font-size:13px;
font-weight:600;
}
.oe_topbar_name .designation{
font-size:11px;
color:#7b7b7b;
}
.text-muted {
color: #000000 !important;
}

View File

@ -0,0 +1,18 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
const userMenuRegistry = registry.category("user_menuitems");
const itemsToHide = [
"documentation",
"support",
"shortcuts",
"odoo_account",
];
for (const item of itemsToHide) {
if (userMenuRegistry.contains(item)) {
userMenuRegistry.remove(item);
}
}

View File

@ -0,0 +1,23 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { UserMenu } from "@web/webclient/user_menu/user_menu";
import { session } from "@web/session";
patch(UserMenu.prototype, {
setup() {
super.setup();
this.employeeName = session.employee_name;
this.employeeDesignation = session.employee_designation;
},
get employeeInfo() {
return {
employeeName: this.employeeName,
employeeDesignation: this.employeeDesignation,
};
},
});

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="web.UserMenu"
t-inherit-mode="extension">
<xpath expr="//small[contains(@class,'oe_topbar_name')]"
position="replace">
<small class="oe_topbar_name d-none d-lg-inline-block ms-2 text-start"
style="max-width:220px">
<div class="fw-bold text-truncate">
<t t-esc="employeeInfo.employeeName"/>
</div>
<div class="text-muted text-truncate designation">
<t t-esc="employeeInfo.employeeDesignation"/>
</div>
</small>
</xpath>
</t>
</templates>

View File

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

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
{
"name": "Onboarding",
"summary": "Employee-side onboarding bridge for JOD and onboarding links",
"description": """
Provides an employee-owned bridge record that can be used without
installing recruitment or ATS modules.
""",
"author": "FTPROTECH",
"website": "https://www.ftprotech.com",
"category": "Human Resources",
"version": "1.0",
"depends": [
"hr_employee_extended",
"mail",
"website",
],
"data": [
"data/ir_sequence_data.xml",
"data/onboarding_attachment_data.xml",
"data/mail_template.xml",
"data/template.xml",
"security/ir.model.access.csv",
"wizards/employee_bridge_attachment_wizard.xml",
"views/recruitment_employee_bridge_views.xml",
"views/onboarding_attachment_views.xml",
"views/hr_employee_views.xml",
],
"license": "LGPL-3",
"assets": {
"web.assets_frontend": [
"employee_bridge/static/src/js/post_onboarding_form.js",
],
},
"application": False,
"installable": True,
}

View File

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

View File

@ -0,0 +1,43 @@
from odoo import http
from odoo.http import request
from odoo.addons.website.controllers.main import Website # <-- ADD THIS IMPORT
# CHANGE INHERITANCE: http.Controller -> Website
class EmployeeBridgeController(Website):
def _get_bridge(self, token):
return request.env["recruitment.employee.bridge"].sudo().search([
("access_token", "=", token),
("active", "=", True),
], limit=1)
def _render_form(self, bridge):
# Removed the 'message' variable as we will use a separate template for success
return request.render("employee_bridge.employee_bridge_jod_form_template", {
"bridge": bridge,
})
@http.route("/employee_bridge/jod/<string:token>", type="http", auth="public", csrf=False, website=True)
def employee_bridge_jod(self, token, **post):
bridge = self._get_bridge(token)
if not bridge:
return request.not_found()
if request.httprequest.method == "POST":
bridge.write_from_public_form(post)
# RENDER A SEPARATE THANK YOU TEMPLATE INSTEAD OF THE FORM
return request.render("employee_bridge.employee_bridge_thank_you_template", {
"bridge": bridge,
})
return self._render_form(bridge)
@http.route("/employee_bridge/fetch_related_state_ids", type="json", auth="public", website=True)
def fetch_related_state_ids(self, country_id=None):
states = request.env["res.country.state"].sudo()
return {
state.id: state.name
for state in states.search([("country_id", "=?", country_id)])
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<record id="seq_recruitment_employee_bridge" model="ir.sequence">
<field name="name">Onboarding</field>
<field name="code">recruitment.employee.bridge</field>
<field name="prefix">EB/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="email_template_jod_form" model="mail.template">
<field name="name">JOD Form Email</field>
<field name="model_id" ref="employee_bridge.model_recruitment_employee_bridge"/>
<field name="email_from">{{ user.email_formatted or object.company_id.email or '' }}</field>
<field name="email_to">{{ object.work_email or object.private_email or object.employee_id.work_email or '' }}</field>
<field name="subject">Joining Onboarding Form - {{ object.employee_name or object.employee_id.name or '' }}</field>
<field name="body_html" type="html">
<div style="margin:0; padding:0; font-size:13px; line-height:1.7;">
<p>Dear <t t-esc="object.employee_name or object.employee_id.name or 'Candidate'"/>,</p>
<p>Welcome to <t t-esc="object.employee_id.company_id.name"/>.</p>
<p>
Please submit your employee joining details using the link below.
</p>
<p>
<a t-att-href="ctx.get('joining_form_link')" target="_blank"
style="background-color:#005580; color:#ffffff; padding:10px 18px; text-decoration:none; border-radius:4px; display:inline-block;">
Open Employee Joining Form
</a>
</p>
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
<p><strong>Attachments to Request</strong></p>
<t t-if="ctx.get('personal_docs')">
<p><strong>Personal Documents:</strong> <t t-esc="', '.join(ctx.get('personal_docs'))"/></p>
</t>
<t t-if="ctx.get('education_docs')">
<p><strong>Education Documents:</strong> <t t-esc="', '.join(ctx.get('education_docs'))"/></p>
</t>
<t t-if="ctx.get('previous_employer_docs')">
<p><strong>Previous Employer Documents:</strong> <t t-esc="', '.join(ctx.get('previous_employer_docs'))"/></p>
</t>
<t t-if="ctx.get('other_docs')">
<p><strong>Other Documents:</strong> <t t-esc="', '.join(ctx.get('other_docs'))"/></p>
</t>
</t>
<p>Regards,<br/>HR Team</p>
</div>
</field>
</record>
</odoo>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<record id="onboarding_attachment_aadhaar" model="employee.bridge.requested.attachment">
<field name="name">Aadhaar Card</field>
<field name="attachment_type">personal</field>
<field name="is_default">1</field>
</record>
<record id="onboarding_attachment_pan" model="employee.bridge.requested.attachment">
<field name="name">PAN Card</field>
<field name="attachment_type">personal</field>
<field name="is_default">1</field>
</record>
<record id="onboarding_attachment_photo" model="employee.bridge.requested.attachment">
<field name="name">Passport Size Photo</field>
<field name="attachment_type">personal</field>
<field name="is_default">1</field>
</record>
<record id="onboarding_attachment_education" model="employee.bridge.requested.attachment">
<field name="name">Education Certificates</field>
<field name="attachment_type">education</field>
<field name="is_default">1</field>
</record>
<record id="onboarding_attachment_experience" model="employee.bridge.requested.attachment">
<field name="name">Experience / Relieving Letter</field>
<field name="attachment_type">previous_employer</field>
<field name="is_default">1</field>
</record>
<record id="onboarding_attachment_payslip" model="employee.bridge.requested.attachment">
<field name="name">Last Three Months Payslips</field>
<field name="attachment_type">previous_employer</field>
<field name="is_default">1</field>
</record>
<record id="onboarding_attachment_bank" model="employee.bridge.requested.attachment">
<field name="name">Cancelled Cheque / Bank Proof</field>
<field name="attachment_type">others</field>
<field name="is_default">1</field>
</record>
</odoo>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,64 @@
from odoo import _, fields, models
from odoo.exceptions import ValidationError
class HrEmployee(models.Model):
_inherit = "hr.employee"
recruitment_bridge_ids = fields.One2many(
"recruitment.employee.bridge",
"employee_id",
string="Onboarding Records",
)
recruitment_bridge_count = fields.Integer(compute="_compute_recruitment_bridge_count")
def _compute_recruitment_bridge_count(self):
data = self.env["recruitment.employee.bridge"].read_group(
[("employee_id", "in", self.ids), ("active", "=", True)],
["employee_id"],
["employee_id"],
)
counts = {item["employee_id"][0]: item["employee_id_count"] for item in data}
for employee in self:
employee.recruitment_bridge_count = counts.get(employee.id, 0)
def _get_or_create_employee_bridge(self):
self.ensure_one()
bridge = self.env["recruitment.employee.bridge"].search(
[("employee_id", "=", self.id), ("active", "=", True)],
limit=1,
order="id desc",
)
if not bridge:
bridge = self.env["recruitment.employee.bridge"].create({
"employee_id": self.id,
"company_id": self.company_id.id or self.env.company.id,
"source": "jod",
"state": "draft",
})
return bridge
def send_jod_form_to_employee(self):
self.ensure_one()
bridge = self._get_or_create_employee_bridge()
bridge.action_send_jod_form()
return {
"name": _("Onboarding"),
"type": "ir.actions.act_window",
"res_model": "recruitment.employee.bridge",
"view_mode": "form",
"res_id": bridge.id,
}
def action_open_recruitment_bridge(self):
self.ensure_one()
bridge = self._get_or_create_employee_bridge()
if not bridge:
raise ValidationError(_("No onboarding record found."))
return {
"name": _("Onboarding"),
"type": "ir.actions.act_window",
"res_model": "recruitment.employee.bridge",
"view_mode": "form",
"res_id": bridge.id,
}

View File

@ -0,0 +1,69 @@
from odoo import fields, models
class EmployeeBridgeRequestedAttachment(models.Model):
_name = "employee.bridge.requested.attachment"
_description = "Onboarding Requested Attachment"
name = fields.Char(required=True)
attachment_type = fields.Selection([
("personal", "Personal Documents"),
("education", "Education Documents"),
("previous_employer", "Previous Employer"),
("others", "Others"),
], default="others", required=True)
is_default = fields.Boolean(string="Is Default")
class EmployeeBridgeAttachment(models.Model):
_name = "employee.bridge.attachment"
_description = "Onboarding Attachment"
_rec_name = "name"
name = fields.Char(required=True)
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, ondelete="cascade")
employee_id = fields.Many2one("hr.employee")
requested_attachment_id = fields.Many2one("employee.bridge.requested.attachment")
attachment_type = fields.Selection(related="requested_attachment_id.attachment_type")
file = fields.Binary(required=True)
file_name = fields.Char()
review_status = fields.Selection([
("draft", "Under Review"),
("pass", "PASS"),
("fail", "FAIL"),
], default="draft")
review_comments = fields.Char()
def action_preview_file(self):
self.ensure_one()
attachment = self.env["ir.attachment"].sudo().create({
"name": self.file_name or self.name,
"datas": self.file,
"res_model": self._name,
"res_id": self.id,
"type": "binary",
})
return {
"name": "File Preview",
"type": "ir.actions.act_url",
"url": f"/web/content/{attachment.id}?download=false",
"target": "current",
}
class FamilyDetails(models.Model):
_inherit = "family.details"
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
class EducationHistory(models.Model):
_inherit = "education.history"
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
class EmployerHistory(models.Model):
_inherit = "employer.history"
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")

View File

@ -0,0 +1,559 @@
import json
from datetime import datetime
from secrets import token_urlsafe
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class RecruitmentEmployeeBridge(models.Model):
_name = "recruitment.employee.bridge"
_description = "Onboarding"
_inherit = ["mail.thread", "mail.activity.mixin"]
_rec_name = "name"
_order = "id desc"
@api.depends('name', 'employee_id', 'employee_name')
def _compute_display_name(self):
for record in self:
employee_name = record.employee_name or record.employee_id.name or ''
if employee_name:
record.display_name = f"{record.name} - {employee_name}"
else:
record.display_name = record.name
name = fields.Char(default="/", copy=False, readonly=True, tracking=True)
active = fields.Boolean(default=True)
employee_id = fields.Many2one(
"hr.employee",
string="Employee",
ondelete="restrict",
tracking=True,
index=True,
)
company_id = fields.Many2one(
"res.company",
string="Company",
default=lambda self: self.env.company,
required=True,
index=True,
)
source = fields.Selection(
[
("jod", "JOD"),
("manual", "Manual"),
("recruitment", "Recruitment"),
],
default="jod",
required=True,
tracking=True,
)
state = fields.Selection(
[
("draft", "Draft"),
("jod_sent", "JOD Sent"),
("jod_received", "JOD Received"),
("validated", "Validated"),
],
default="draft",
required=True,
tracking=True,
)
employee_name = fields.Char(string="Employee Name", tracking=True)
employee_code = fields.Char(string="Employee Code", tracking=True)
candidate_image = fields.Image(string="Photo")
work_email = fields.Char(string="Work Email", tracking=True)
private_email = fields.Char(string="Private Email")
alternate_phone = fields.Char(string="Alternate Mobile")
work_phone = fields.Char(string="Work Phone")
mobile_phone = fields.Char(string="Mobile Phone")
department_id = fields.Many2one("hr.department", string="Department")
job_id = fields.Many2one("hr.job", string="Job Position")
emp_type = fields.Many2one("hr.contract.type", string="Employment Type")
doj = fields.Date(string="Date of Joining")
gender = fields.Selection([
("male", "Male"),
("female", "Female"),
("other", "Other"),
])
birthday = fields.Date()
blood_group = fields.Selection([
("A+", "A+"),
("A-", "A-"),
("B+", "B+"),
("B-", "B-"),
("O+", "O+"),
("O-", "O-"),
("AB+", "AB+"),
("AB-", "AB-"),
], string="Blood Group")
marital = fields.Selection(
selection=[
("single", "Single"),
("married", "Married"),
("cohabitant", "Legal Cohabitant"),
("widower", "Widower"),
("divorced", "Divorced"),
],
default="single",
string="Marital Status",
)
marriage_anniversary_date = fields.Date(string="Anniversary Date")
private_street = fields.Char(string="Private Street")
private_street2 = fields.Char(string="Private Street2")
private_city = fields.Char(string="Private City")
private_state_id = fields.Many2one("res.country.state", string="Private State")
private_zip = fields.Char(string="Private Zip")
private_country_id = fields.Many2one("res.country", string="Private Country")
permanent_street = fields.Char(string="Permanent Street")
permanent_street2 = fields.Char(string="Permanent Street2")
permanent_city = fields.Char(string="Permanent City")
permanent_state_id = fields.Many2one("res.country.state", string="Permanent State")
permanent_zip = fields.Char(string="Permanent Zip")
permanent_country_id = fields.Many2one("res.country", string="Permanent Country")
pan_no = fields.Char(string="PAN No")
identification_id = fields.Char(string="Aadhar No")
previous_company_pf_no = fields.Char(string="Previous Company PF No")
previous_company_uan_no = fields.Char(string="Previous Company UAN No")
passport_no = fields.Char(string="Passport No")
passport_start_date = fields.Date(string="Passport Issued Date")
passport_end_date = fields.Date(string="Passport End Date")
passport_issued_location = fields.Char(string="Passport Issued Location")
full_name_as_in_bank = fields.Char(string="Name as per Bank")
bank_name = fields.Char()
bank_branch = fields.Char()
bank_account_no = fields.Char()
bank_ifsc_code = fields.Char()
requested_attachment_ids = fields.Many2many(
"employee.bridge.requested.attachment",
"employee_bridge_requested_attachment_rel",
"bridge_id",
"attachment_id",
string="Attachments to Request",
)
joining_attachment_ids = fields.One2many(
"employee.bridge.attachment",
"bridge_id",
string="Attachments",
)
education_history = fields.One2many("education.history", "bridge_id", string="Education Details")
employer_history = fields.One2many("employer.history", "bridge_id", string="Employer History")
family_details = fields.One2many("family.details", "bridge_id", string="Family Details")
joining_form_link = fields.Char(string="Joining Form Link", copy=False, readonly=True)
access_token = fields.Char(copy=False, readonly=True)
total_exp = fields.Float(string="Total Exp")
_sql_constraints = [
(
"unique_bridge_employee",
"unique(employee_id)",
"This employee is already connected to an employee bridge.",
),
]
def _get_base_url(self):
return self.env["ir.config_parameter"].sudo().get_param("web.base.url")
def _ensure_access_token(self):
for bridge in self:
if not bridge.access_token:
bridge.access_token = token_urlsafe(32)
def _get_joining_form_link(self):
self.ensure_one()
self._ensure_access_token()
return "%s/employee_bridge/jod/%s" % (self._get_base_url(), self.access_token)
def _group_requested_attachments(self, attachments):
return {
"personal_docs": attachments.filtered(lambda a: a.attachment_type == "personal").mapped("name"),
"education_docs": attachments.filtered(lambda a: a.attachment_type == "education").mapped("name"),
"previous_employer_docs": attachments.filtered(lambda a: a.attachment_type == "previous_employer").mapped("name"),
"other_docs": attachments.filtered(lambda a: a.attachment_type == "others").mapped("name"),
}
def _send_jod_mail(self, req_attachment_ids=False, email_values=None, email_body=False):
for bridge in self:
email_to = bridge.work_email or bridge.private_email or bridge.employee_id.work_email
if not email_to:
raise ValidationError(_("Add an email on the bridge before sending the JOD form."))
link = bridge._get_joining_form_link()
bridge.write({
"joining_form_link": link,
"state": "jod_sent",
})
attachments = req_attachment_ids or self.env["employee.bridge.requested.attachment"].browse()
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
context = {
"joining_form_link": link,
**bridge._group_requested_attachments(attachments),
}
values = dict(email_values or {})
values.setdefault("email_from", self.env.company.email or self.env.user.email)
values.setdefault("email_to", email_to)
if email_body:
values["body_html"] = email_body
if template:
rendered_subject = template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id]
rendered_body = template.with_context(**context)._render_field(
"body_html", [bridge.id], compute_lang=True
)[bridge.id]
values.setdefault("subject", rendered_subject)
values.setdefault("body_html", rendered_body)
template.sudo().with_context(**context).send_mail(bridge.id, email_values=values, force_send=True)
else:
values.setdefault("subject", _("Joining Onboarding Details"))
values.setdefault("body_html", _(
"<p>Hello %(name)s,</p>"
"<p>Please submit your joining onboarding details using this link:</p>"
"<p><a href='%(link)s'>Open Joining Form</a></p>"
) % {
"name": bridge.employee_name or bridge.employee_id.name or _("Employee"),
"link": link,
})
self.env["mail.mail"].sudo().create(values).send()
return True
def action_send_jod_form(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Select Attachments"),
"res_model": "employee.bridge.attachment.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_bridge_id": self.id,
},
}
def action_open_employee(self):
self.ensure_one()
if not self.employee_id:
raise ValidationError(_("No employee is linked to this bridge."))
return {
"name": _("Employee"),
"type": "ir.actions.act_window",
"res_model": "hr.employee",
"view_mode": "form",
"res_id": self.employee_id.id,
}
def action_validate_employee_details(self):
for bridge in self:
if not bridge.employee_id:
raise ValidationError(_("Link or create an employee before validating the JOD details."))
bridge.employee_id.write(bridge._get_employee_update_vals())
bridge.education_history.write({"employee_id": bridge.employee_id.id})
bridge.employer_history.write({"employee_id": bridge.employee_id.id})
bridge.family_details.write({"employee_id": bridge.employee_id.id})
bridge.joining_attachment_ids.write({"employee_id": bridge.employee_id.id})
bridge.state = "validated"
def _get_employee_create_vals(self):
self.ensure_one()
vals = {
"name": self.employee_name,
"employee_id": self.employee_code,
"work_email": self.work_email,
"private_email": self.private_email,
"work_phone": self.work_phone,
"mobile_phone": self.mobile_phone,
"image_1920": self.candidate_image,
"department_id": self.department_id.id,
"job_id": self.job_id.id,
"job_title": self.job_id.name,
"emp_type": self.emp_type.id,
"company_id": self.company_id.id,
}
vals = {key: value for key, value in vals.items() if value not in (False, "", 0)}
if not vals.get("name"):
raise ValidationError(_("Employee Name is required to create an employee."))
return vals
def _get_employee_update_vals(self):
self.ensure_one()
vals = {
"name": self.employee_name,
"employee_id": self.employee_code,
"work_email": self.work_email,
"private_email": self.private_email,
"work_phone": self.work_phone,
"mobile_phone": self.mobile_phone,
"image_1920": self.candidate_image,
"department_id": self.department_id.id,
"job_id": self.job_id.id,
"job_title": self.job_id.name,
"emp_type": self.emp_type.id,
"doj": self.doj,
"gender": self.gender,
"birthday": self.birthday,
"blood_group": self.blood_group,
"marital": self.marital,
"marriage_anniversary_date": self.marriage_anniversary_date,
"private_street": self.private_street,
"private_street2": self.private_street2,
"private_city": self.private_city,
"private_state_id": self.private_state_id.id,
"private_zip": self.private_zip,
"private_country_id": self.private_country_id.id,
"permanent_street": self.permanent_street,
"permanent_street2": self.permanent_street2,
"permanent_city": self.permanent_city,
"permanent_state_id": self.permanent_state_id.id,
"permanent_zip": self.permanent_zip,
"permanent_country_id": self.permanent_country_id.id,
"pan_no": self.pan_no,
"identification_id": self.identification_id,
"previous_company_pf_no": self.previous_company_pf_no,
"previous_company_uan_no": self.previous_company_uan_no,
"passport_id": self.passport_no,
"passport_start_date": self.passport_start_date,
"passport_end_date": self.passport_end_date,
"passport_issued_location": self.passport_issued_location,
}
return {
key: value
for key, value in vals.items()
if key in self.employee_id._fields and value not in (False, "", 0)
}
def action_create_employee(self):
for bridge in self:
if bridge.employee_id:
continue
bridge.employee_id = self.env["hr.employee"].sudo().create(bridge._get_employee_create_vals())
return self.action_open_employee()
def _sync_from_employee(self):
for bridge in self.filtered("employee_id"):
employee = bridge.employee_id
bridge.write({
"employee_name": bridge.employee_name or employee.name,
"employee_code": bridge.employee_code or employee.employee_id,
"work_email": bridge.work_email or employee.work_email,
"private_email": bridge.private_email or employee.private_email,
"work_phone": bridge.work_phone or employee.work_phone,
"mobile_phone": bridge.mobile_phone or employee.mobile_phone,
"department_id": bridge.department_id.id or employee.department_id.id,
"job_id": bridge.job_id.id or employee.job_id.id,
"emp_type": bridge.emp_type.id or employee.emp_type.id,
"company_id": employee.company_id.id or bridge.company_id.id,
"doj": bridge.doj or employee.doj,
"gender": bridge.gender or employee.gender,
"birthday": bridge.birthday or employee.birthday,
"blood_group": bridge.blood_group or employee.blood_group,
"marital": bridge.marital or employee.marital,
"marriage_anniversary_date": bridge.marriage_anniversary_date or employee.marriage_anniversary_date,
"private_street": bridge.private_street or employee.private_street,
"private_street2": bridge.private_street2 or employee.private_street2,
"private_city": bridge.private_city or employee.private_city,
"private_state_id": bridge.private_state_id.id or employee.private_state_id.id,
"private_zip": bridge.private_zip or employee.private_zip,
"private_country_id": bridge.private_country_id.id or employee.private_country_id.id,
"permanent_street": bridge.permanent_street or employee.permanent_street,
"permanent_street2": bridge.permanent_street2 or employee.permanent_street2,
"permanent_city": bridge.permanent_city or employee.permanent_city,
"permanent_state_id": bridge.permanent_state_id.id or employee.permanent_state_id.id,
"permanent_zip": bridge.permanent_zip or employee.permanent_zip,
"permanent_country_id": bridge.permanent_country_id.id or employee.permanent_country_id.id,
"pan_no": bridge.pan_no or employee.pan_no,
"identification_id": bridge.identification_id or employee.identification_id,
"previous_company_pf_no": bridge.previous_company_pf_no or employee.previous_company_pf_no,
"previous_company_uan_no": bridge.previous_company_uan_no or employee.previous_company_uan_no,
"passport_no": bridge.passport_no or employee.passport_id,
"passport_start_date": bridge.passport_start_date or employee.passport_start_date,
"passport_end_date": bridge.passport_end_date or employee.passport_end_date,
"passport_issued_location": bridge.passport_issued_location or employee.passport_issued_location,
})
@api.onchange("employee_id")
def _onchange_employee_id(self):
for bridge in self:
employee = bridge.employee_id
if not employee:
continue
bridge.employee_name = bridge.employee_name or employee.name
bridge.employee_code = bridge.employee_code or employee.employee_id
bridge.work_email = bridge.work_email or employee.work_email
bridge.private_email = bridge.private_email or employee.private_email
bridge.work_phone = bridge.work_phone or employee.work_phone
bridge.mobile_phone = bridge.mobile_phone or employee.mobile_phone
bridge.department_id = bridge.department_id or employee.department_id
bridge.job_id = bridge.job_id or employee.job_id
bridge.emp_type = bridge.emp_type or employee.emp_type
def write_from_public_form(self, values):
self.ensure_one()
private_state = self.env["res.country.state"].sudo().browse(int(values.get("present_state") or 0))
permanent_state = self.env["res.country.state"].sudo().browse(int(values.get("permanent_state") or 0))
mapped_values = dict(values)
mapped_values.update({
"candidate_image": values.get("candidate_image_base64"),
"private_email": values.get("email_from"),
"mobile_phone": values.get("partner_phone"),
"alternate_phone": values.get("alternate_phone"),
"private_street": values.get("present_street"),
"private_street2": values.get("present_street2"),
"private_city": values.get("present_city"),
"private_zip": values.get("present_zip"),
"private_state_id": private_state.id if private_state else False,
"private_country_id": private_state.country_id.id if private_state else False,
"permanent_state_id": permanent_state.id if permanent_state else False,
"permanent_country_id": permanent_state.country_id.id if permanent_state else False,
})
allowed_fields = {
"employee_name",
"candidate_image",
"private_email",
"mobile_phone",
"alternate_phone",
"doj",
"gender",
"birthday",
"blood_group",
"marital",
"marriage_anniversary_date",
"private_street",
"private_street2",
"private_city",
"private_state_id",
"private_country_id",
"private_zip",
"permanent_street",
"permanent_street2",
"permanent_city",
"permanent_state_id",
"permanent_country_id",
"permanent_zip",
"full_name_as_in_bank",
"bank_name",
"bank_branch",
"bank_account_no",
"bank_ifsc_code",
"pan_no",
"identification_id",
"previous_company_pf_no",
"previous_company_uan_no",
"passport_no",
"passport_start_date",
"passport_end_date",
"passport_issued_location",
}
bridge_vals = {
field_name: value or False
for field_name, value in mapped_values.items()
if field_name in allowed_fields and field_name in self._fields
}
if bridge_vals:
self.sudo().write(bridge_vals)
self._write_history_from_public_form(values)
self._replace_joining_attachments_from_public_form(values)
self.state = "jod_received"
def _safe_date(self, value):
try:
return datetime.strptime(value, "%Y-%m-%d").date() if value else False
except (TypeError, ValueError):
return False
def _safe_int(self, value):
try:
return int(value) if value else False
except (TypeError, ValueError):
return False
def _json_list(self, values, key):
raw = values.get(key, "[]")
try:
data = json.loads(raw) if raw else []
except (TypeError, ValueError):
data = []
return data if isinstance(data, list) else []
def _write_history_from_public_form(self, values):
self.ensure_one()
family_values = []
for member in self._json_list(values, "family_data_json"):
if member.get("name") and member.get("relation"):
family_values.append((0, 0, {
"relation_type": member.get("relation"),
"name": member.get("name"),
"contact_no": member.get("contact"),
"dob": self._safe_date(member.get("dob")),
"location": member.get("location"),
}))
education_values = []
for education in self._json_list(values, "education_data_json"):
if education.get("specialization"):
education_values.append((0, 0, {
"education_type": education.get("education_type") or "additional",
"name": education.get("specialization"),
"university": education.get("university"),
"start_year": self._safe_int(education.get("start_year")),
"end_year": self._safe_int(education.get("end_year")),
"marks_or_grade": education.get("marks_or_grade"),
}))
employer_values = []
for employer in self._json_list(values, "employer_history_data_json"):
if employer.get("company_name"):
employer_values.append((0, 0, {
"company_name": employer.get("company_name"),
"designation": employer.get("designation"),
"date_of_joining": self._safe_date(employer.get("date_of_joining")),
"last_working_day": self._safe_date(employer.get("last_working_day")),
"ctc": employer.get("ctc"),
}))
write_vals = {}
if family_values:
write_vals["family_details"] = [(5, 0, 0)] + family_values
if education_values:
write_vals["education_history"] = [(5, 0, 0)] + education_values
if employer_values:
write_vals["employer_history"] = [(5, 0, 0)] + employer_values
if write_vals:
self.sudo().write(write_vals)
def _replace_joining_attachments_from_public_form(self, values):
self.ensure_one()
attachment_values = []
seen_files = set()
for item in self._json_list(values, "attachments_data_json"):
attachment = self.env["employee.bridge.requested.attachment"].sudo().browse(
int(item.get("attachment_rec_id") or 0)
)
file_content = item.get("file_content")
file_name = item.get("file_name")
file_key = (attachment.id, file_name, file_content)
if attachment.exists() and file_content and file_key not in seen_files:
seen_files.add(file_key)
attachment_values.append((0, 0, {
"name": attachment.name,
"requested_attachment_id": attachment.id,
"file": file_content,
"file_name": file_name,
}))
self.sudo().write({"joining_attachment_ids": [(5, 0, 0)] + attachment_values})
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("name", "/") == "/":
vals["name"] = self.env["ir.sequence"].next_by_code("recruitment.employee.bridge") or "/"
if vals.get("employee_id") and not vals.get("company_id"):
employee = self.env["hr.employee"].browse(vals["employee_id"])
vals["company_id"] = employee.company_id.id or self.env.company.id
bridges = super().create(vals_list)
bridges._sync_from_employee()
return bridges
def write(self, vals):
res = super().write(vals)
if "employee_id" in vals:
self._sync_from_employee()
return res

View File

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

View File

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

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="hr_employee_view_form_bridge" model="ir.ui.view">
<field name="name">hr.employee.view.form.bridge</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_open_recruitment_bridge"
type="object"
class="oe_stat_button"
icon="fa-link"
groups="hr.group_hr_user"
invisible="not recruitment_bridge_count">
<field name="recruitment_bridge_count" widget="statinfo" string="Onboarding"/>
</button>
</xpath>
<xpath expr="//label[@for='user_id']" position="before">
<button string="Send JOD Form"
name="send_jod_form_to_employee"
type="object"
class="btn-primary"
groups="hr.group_hr_user"
invisible="not work_email"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_employee_bridge_requested_attachment_list" model="ir.ui.view">
<field name="name">employee.bridge.requested.attachment.list</field>
<field name="model">employee.bridge.requested.attachment</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="name"/>
<field name="attachment_type"/>
<field name="is_default"/>
</list>
</field>
</record>
<record id="view_employee_bridge_requested_attachment_form" model="ir.ui.view">
<field name="name">employee.bridge.requested.attachment.form</field>
<field name="model">employee.bridge.requested.attachment</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="attachment_type"/>
<field name="is_default"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_employee_bridge_requested_attachment" model="ir.actions.act_window">
<field name="name">Onboarding Documents</field>
<field name="res_model">employee.bridge.requested.attachment</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_employee_bridge_requested_attachment"
name="Onboarding Documents"
parent="menu_employee_bridge"
action="action_employee_bridge_requested_attachment"
sequence="20"
groups="hr.group_hr_user"/>
</odoo>

View File

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_employee_bridge_list" model="ir.ui.view">
<field name="name">employee.bridge.list</field>
<field name="model">recruitment.employee.bridge</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="employee_name"/>
<field name="employee_id"/>
<field name="employee_code"/>
<field name="work_email"/>
<field name="joining_form_link" optional="show"/>
<field name="source"/>
<field name="state" widget="badge"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="view_employee_bridge_form" model="ir.ui.view">
<field name="name">employee.bridge.form</field>
<field name="model">recruitment.employee.bridge</field>
<field name="arch" type="xml">
<form>
<header>
<button string="Send JOD Form" name="action_send_jod_form" type="object" class="btn-primary" invisible="state == 'validated'"/>
<button string="Create Employee" name="action_create_employee" type="object" class="btn-secondary" invisible="employee_id"/>
<field name="state" widget="statusbar" statusbar_visible="draft,jod_sent,jod_received,validated"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_employee" type="object" class="oe_stat_button" icon="fa-id-card-o" invisible="not employee_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="employee_code" readonly="1"/></span>
<span class="o_stat_text">Employee</span>
</div>
</button>
</div>
<group string="Onboarding">
<group>
<field name="name"/>
<field name="source"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="employee_id"/>
<field name="employee_code"/>
<field name="joining_form_link" widget="url"/>
<field name="requested_attachment_ids" widget="many2many_tags"/>
</group>
</group>
<button string="Validate Into Employee" name="action_validate_employee_details" type="object" class="btn-primary" invisible="state == 'validated' or not employee_id"/>
<notebook>
<page string="Basic Details" name="basic_details">
<group>
<group>
<field name="employee_name"/>
<field name="candidate_image" widget="image" class="oe_avatar"/>
<field name="employee_code"/>
<field name="work_email" widget="email"/>
<field name="private_email" widget="email"/>
<field name="work_phone" widget="phone"/>
<field name="mobile_phone" widget="phone"/>
</group>
<group>
<field name="department_id"/>
<field name="job_id"/>
<field name="emp_type"/>
<field name="total_exp"/>
<field name="doj"/>
</group>
</group>
</page>
<page string="JOD Details" name="jod_details">
<group string="Personal Details">
<group>
<field name="gender"/>
<field name="birthday"/>
<field name="blood_group"/>
</group>
<group>
<field name="marital"/>
<field name="marriage_anniversary_date" invisible="marital == 'single'"/>
</group>
</group>
<group string="Current Address">
<group>
<field name="private_street"/>
<field name="private_street2"/>
<field name="private_city"/>
</group>
<group>
<field name="private_state_id"/>
<field name="private_zip"/>
<field name="private_country_id"/>
</group>
</group>
<group string="Permanent Address">
<group>
<field name="permanent_street"/>
<field name="permanent_street2"/>
<field name="permanent_city"/>
</group>
<group>
<field name="permanent_state_id"/>
<field name="permanent_zip"/>
<field name="permanent_country_id"/>
</group>
</group>
<group string="Authentication">
<group>
<field name="pan_no"/>
<field name="identification_id"/>
</group>
<group>
<field name="previous_company_pf_no"/>
<field name="previous_company_uan_no"/>
</group>
</group>
<group string="Passport">
<group>
<field name="passport_no"/>
<field name="passport_start_date"/>
<field name="passport_end_date"/>
</group>
<group>
<field name="passport_issued_location"/>
</group>
</group>
</page>
<page string="Family Details" name="family_details">
<field name="family_details" nolabel="1">
<list editable="bottom">
<field name="relation_type"/>
<field name="name"/>
<field name="contact_no"/>
<field name="dob"/>
<field name="location"/>
</list>
</field>
</page>
<page string="Employer History" name="employer_history">
<field name="employer_history" nolabel="1">
<list editable="bottom">
<field name="company_name"/>
<field name="designation"/>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
</list>
</field>
</page>
<page string="Education Details" name="education_history">
<field name="education_history" nolabel="1">
<list editable="bottom">
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
</list>
</field>
</page>
<page string="Attachments" name="attachments">
<field name="joining_attachment_ids" nolabel="1">
<list editable="bottom" default_group_by="requested_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
<field name="requested_attachment_id"/>
<field name="name"/>
<field name="attachment_type"/>
<field name="file" widget="binary" filename="file_name"/>
<field name="file_name" optional="hide"/>
<button name="action_preview_file" type="object" string="Preview" class="oe_highlight" icon="fa-eye"/>
<field name="review_status"/>
<field name="review_comments" optional="hide"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_employee_bridge_search" model="ir.ui.view">
<field name="name">employee.bridge.search</field>
<field name="model">recruitment.employee.bridge</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="employee_id"/>
<filter name="jod_sent" string="JOD Sent" domain="[('state', '=', 'jod_sent')]"/>
<filter name="jod_received" string="JOD Received" domain="[('state', '=', 'jod_received')]"/>
<filter name="validated" string="Validated" domain="[('state', '=', 'validated')]"/>
<group expand="0" string="Group By">
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
<filter name="group_source" string="Source" context="{'group_by': 'source'}"/>
</group>
</search>
</field>
</record>
<record id="action_employee_bridge" model="ir.actions.act_window">
<field name="name">Onboarding</field>
<field name="res_model">recruitment.employee.bridge</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_employee_bridge"
name="Onboarding"
parent="hr.menu_hr_root"
sequence="90"
groups="hr.group_hr_user"/>
<menuitem id="menu_employee_bridge_onboarding"
name="Onboarding"
parent="menu_employee_bridge"
action="action_employee_bridge"
sequence="10"
groups="hr.group_hr_user"/>
</odoo>

View File

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

View File

@ -0,0 +1,92 @@
from odoo import api, fields, models
class EmployeeBridgeAttachmentWizard(models.TransientModel):
_name = "employee.bridge.attachment.wizard"
_description = "Onboarding Attachment Request Wizard"
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, readonly=True)
req_attachment_ids = fields.Many2many(
comodel_name="employee.bridge.requested.attachment",
relation="employee_bridge_req_attachment_rel", # relation table name
column1="request_id", # column referring to this model
column2="attachment_id", # column referring to the comodel
string="Attachments to Request",
)
attachment_ids = fields.Many2many("ir.attachment")
template_id = fields.Many2one(
"mail.template",
default=lambda self: self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False),
readonly=True,
)
email_from = fields.Char("Email From")
email_to = fields.Char("Email To")
email_cc = fields.Text("Email CC")
email_subject = fields.Char()
email_body = fields.Html(
"Body",
render_engine="qweb",
render_options={"post_process": True},
prefetch=True,
translate=True,
sanitize="email_outgoing",
)
@api.model
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
bridge = self.env["recruitment.employee.bridge"].browse(
self.env.context.get("default_bridge_id") or self.env.context.get("bridge_id")
)
if bridge:
attachments = bridge.requested_attachment_ids or self.env["employee.bridge.requested.attachment"].search([
("is_default", "=", True),
])
defaults.update({
"bridge_id": bridge.id,
"req_attachment_ids": [(6, 0, attachments.ids)],
"email_from": self.env.company.email or self.env.user.email,
"email_to": bridge.work_email or bridge.private_email or bridge.employee_id.work_email,
})
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
if template:
context = {
"joining_form_link": bridge._get_joining_form_link(),
**bridge._group_requested_attachments(attachments),
}
defaults.update({
"template_id": template.id,
"email_subject": template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id],
"email_body": template.with_context(**context)._render_field(
"body_html", [bridge.id], compute_lang=True
)[bridge.id],
})
return defaults
@api.onchange("req_attachment_ids")
def _onchange_req_attachment_ids(self):
if not self.bridge_id or not self.template_id:
return
context = {
"joining_form_link": self.bridge_id._get_joining_form_link(),
**self.bridge_id._group_requested_attachments(self.req_attachment_ids),
}
self.email_body = self.template_id.with_context(**context)._render_field(
"body_html", [self.bridge_id.id], compute_lang=True
)[self.bridge_id.id]
def action_confirm(self):
self.ensure_one()
self.bridge_id.requested_attachment_ids = [(6, 0, self.req_attachment_ids.ids)]
self.bridge_id._send_jod_mail(
req_attachment_ids=self.req_attachment_ids,
email_values={
"email_from": self.email_from,
"email_to": self.email_to,
"email_cc": self.email_cc,
"subject": self.email_subject,
"attachment_ids": [(6, 0, self.attachment_ids.ids)],
},
email_body=self.email_body,
)
return {"type": "ir.actions.act_window_close"}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_employee_bridge_attachment_wizard_form" model="ir.ui.view">
<field name="name">employee.bridge.attachment.wizard.form</field>
<field name="model">employee.bridge.attachment.wizard</field>
<field name="arch" type="xml">
<form string="Select Attachments">
<group>
<field name="bridge_id" invisible="1"/>
<field name="req_attachment_ids" widget="many2many_tags" force_save="1"/>
</group>
<notebook>
<page name="attachment" string="Attachments (Binary)">
<field name="attachment_ids" widget="many2many_binary" domain="[('mimetype', 'not ilike', 'image')]"/>
</page>
<page name="email" string="Email">
<group>
<field name="template_id" options="{'no_create': True}" readonly="1" force_save="1"/>
<field name="email_from" force_save="1"/>
<field name="email_to" force_save="1"/>
<field name="email_cc"/>
<field name="email_subject" force_save="1"/>
<field name="email_body" widget="html_mail" class="oe-bordered-editor" options="{'codeview': true}" force_save="1"/>
<field name="attachment_ids" widget="many2many_tags" string="Attachments" force_save="1"/>
</group>
</page>
</notebook>
<footer>
<button name="action_confirm" type="object" string="Send JOD Form" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -10,12 +10,10 @@
""",
'author': 'FTPROTECH',
'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',
}

View File

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

View File

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

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<!-- Original job record -->
<record model="hr.job" id="employee_jod_internal_job_id">
<field name="name">Internal Job</field>
<field name="active" eval="False"/>
</record>
<!-- Recruitment stage -->
<record model="hr.recruitment.stage" id="hired_stage8">
<field name="name">IJ</field>
<field name="is_default_field" eval="False"/>
<field name="hired_stage" eval="True"/>
<!-- <field name="post_onboarding_form" eval="True"/>-->
</record>
<!-- Changed ID for the recruitment record -->
<record model="hr.job.recruitment" id="employee_jod_internal_job_recruitment_id">
<field name="recruitment_sequence">IJ001</field>
<field name="job_id" ref="employee_jod_internal_job_id"/>
<field name="recruitment_stage_ids" eval="[(4, ref('hired_stage8'))]"/>
<field name="active" eval="False"/>
</record>
</data>
</odoo>

View File

@ -313,13 +313,4 @@
</t>
</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>

View File

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

View File

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_employee_view_form_applicant_id" model="ir.ui.view">
<field name="name">hr.employee.view.form.applicant.id</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//label[@for='user_id']" position="before">
<field name="applicant_id" string="Application ID" />
</xpath>
</field>
</record>
<record id="view_post_onboarding_attachment_wizard_form_inherit" model="ir.ui.view">
<field name="name">post.onboarding.attachment.wizard.form.inherit</field>
<field name="model">post.onboarding.attachment.wizard</field>
<field name="inherit_id" ref="hr_recruitment_extended.view_post_onboarding_attachment_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='req_attachment_ids']" position="after">
<field name="send_mail"/>
</xpath>
<xpath expr="//notebook" position="attributes">
<attribute name="invisible">not send_mail</attribute>
</xpath>
<xpath expr="//button[@name='action_confirm']" position="attributes">
<attribute name="string">Send</attribute>
</xpath>
</field>
</record>
<record id="hr_applicant_view_form_inherit_extend" model="ir.ui.view">
<field name="name">hr.applicant.view.form.extend</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='candidate_id']" position="after">
<field name="joining_form_link" force_save="1" readonly="1"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -1,4 +1,5 @@
from odoo import api, fields, models, tools, _
from odoo.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

View File

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

View File

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

View File

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

View File

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

View File

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

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

Internal Server Error

Gitea Version: 1.21.4