Merge pull request 'feature/share_module' (#28) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/28
This commit is contained in:
commit
bdde18401e
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
from . import wizards
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Onboarding",
|
||||
"summary": "Employee-side onboarding bridge for JOD and onboarding links",
|
||||
"description": """
|
||||
Provides an employee-owned bridge record that can be used without
|
||||
installing recruitment or ATS modules.
|
||||
""",
|
||||
"author": "FTPROTECH",
|
||||
"website": "https://www.ftprotech.com",
|
||||
"category": "Human Resources",
|
||||
"version": "1.0",
|
||||
"depends": [
|
||||
"hr_employee_extended",
|
||||
"mail",
|
||||
"website",
|
||||
],
|
||||
"data": [
|
||||
"data/ir_sequence_data.xml",
|
||||
"data/onboarding_attachment_data.xml",
|
||||
"data/mail_template.xml",
|
||||
"data/template.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"wizards/employee_bridge_attachment_wizard.xml",
|
||||
"views/recruitment_employee_bridge_views.xml",
|
||||
"views/onboarding_attachment_views.xml",
|
||||
"views/hr_employee_views.xml",
|
||||
],
|
||||
"license": "LGPL-3",
|
||||
"assets": {
|
||||
"web.assets_frontend": [
|
||||
"employee_bridge/static/src/js/post_onboarding_form.js",
|
||||
],
|
||||
},
|
||||
"application": False,
|
||||
"installable": True,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.website.controllers.main import Website # <-- ADD THIS IMPORT
|
||||
|
||||
|
||||
# CHANGE INHERITANCE: http.Controller -> Website
|
||||
class EmployeeBridgeController(Website):
|
||||
|
||||
def _get_bridge(self, token):
|
||||
return request.env["recruitment.employee.bridge"].sudo().search([
|
||||
("access_token", "=", token),
|
||||
("active", "=", True),
|
||||
], limit=1)
|
||||
|
||||
def _render_form(self, bridge):
|
||||
# Removed the 'message' variable as we will use a separate template for success
|
||||
return request.render("employee_bridge.employee_bridge_jod_form_template", {
|
||||
"bridge": bridge,
|
||||
})
|
||||
|
||||
@http.route("/employee_bridge/jod/<string:token>", type="http", auth="public", csrf=False, website=True)
|
||||
def employee_bridge_jod(self, token, **post):
|
||||
bridge = self._get_bridge(token)
|
||||
if not bridge:
|
||||
return request.not_found()
|
||||
|
||||
if request.httprequest.method == "POST":
|
||||
bridge.write_from_public_form(post)
|
||||
|
||||
# RENDER A SEPARATE THANK YOU TEMPLATE INSTEAD OF THE FORM
|
||||
return request.render("employee_bridge.employee_bridge_thank_you_template", {
|
||||
"bridge": bridge,
|
||||
})
|
||||
|
||||
return self._render_form(bridge)
|
||||
|
||||
@http.route("/employee_bridge/fetch_related_state_ids", type="json", auth="public", website=True)
|
||||
def fetch_related_state_ids(self, country_id=None):
|
||||
states = request.env["res.country.state"].sudo()
|
||||
return {
|
||||
state.id: state.name
|
||||
for state in states.search([("country_id", "=?", country_id)])
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="seq_recruitment_employee_bridge" model="ir.sequence">
|
||||
<field name="name">Onboarding</field>
|
||||
<field name="code">recruitment.employee.bridge</field>
|
||||
<field name="prefix">EB/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="email_template_jod_form" model="mail.template">
|
||||
<field name="name">JOD Form Email</field>
|
||||
<field name="model_id" ref="employee_bridge.model_recruitment_employee_bridge"/>
|
||||
<field name="email_from">{{ user.email_formatted or object.company_id.email or '' }}</field>
|
||||
<field name="email_to">{{ object.work_email or object.private_email or object.employee_id.work_email or '' }}</field>
|
||||
<field name="subject">Joining Onboarding Form - {{ object.employee_name or object.employee_id.name or '' }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin:0; padding:0; font-size:13px; line-height:1.7;">
|
||||
<p>Dear <t t-esc="object.employee_name or object.employee_id.name or 'Candidate'"/>,</p>
|
||||
<p>Welcome to <t t-esc="object.employee_id.company_id.name"/>.</p>
|
||||
<p>
|
||||
Please submit your employee joining details using the link below.
|
||||
</p>
|
||||
<p>
|
||||
<a t-att-href="ctx.get('joining_form_link')" target="_blank"
|
||||
style="background-color:#005580; color:#ffffff; padding:10px 18px; text-decoration:none; border-radius:4px; display:inline-block;">
|
||||
Open Employee Joining Form
|
||||
</a>
|
||||
</p>
|
||||
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
|
||||
<p><strong>Attachments to Request</strong></p>
|
||||
<t t-if="ctx.get('personal_docs')">
|
||||
<p><strong>Personal Documents:</strong> <t t-esc="', '.join(ctx.get('personal_docs'))"/></p>
|
||||
</t>
|
||||
<t t-if="ctx.get('education_docs')">
|
||||
<p><strong>Education Documents:</strong> <t t-esc="', '.join(ctx.get('education_docs'))"/></p>
|
||||
</t>
|
||||
<t t-if="ctx.get('previous_employer_docs')">
|
||||
<p><strong>Previous Employer Documents:</strong> <t t-esc="', '.join(ctx.get('previous_employer_docs'))"/></p>
|
||||
</t>
|
||||
<t t-if="ctx.get('other_docs')">
|
||||
<p><strong>Other Documents:</strong> <t t-esc="', '.join(ctx.get('other_docs'))"/></p>
|
||||
</t>
|
||||
</t>
|
||||
<p>Regards,<br/>HR Team</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="onboarding_attachment_aadhaar" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Aadhaar Card</field>
|
||||
<field name="attachment_type">personal</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_pan" model="employee.bridge.requested.attachment">
|
||||
<field name="name">PAN Card</field>
|
||||
<field name="attachment_type">personal</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_photo" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Passport Size Photo</field>
|
||||
<field name="attachment_type">personal</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_education" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Education Certificates</field>
|
||||
<field name="attachment_type">education</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_experience" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Experience / Relieving Letter</field>
|
||||
<field name="attachment_type">previous_employer</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_payslip" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Last Three Months Payslips</field>
|
||||
<field name="attachment_type">previous_employer</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_attachment_bank" model="employee.bridge.requested.attachment">
|
||||
<field name="name">Cancelled Cheque / Bank Proof</field>
|
||||
<field name="attachment_type">others</field>
|
||||
<field name="is_default">1</field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,3 @@
|
|||
from . import recruitment_employee_bridge
|
||||
from . import hr_employee
|
||||
from . import onboarding_documents
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
from odoo import _, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
recruitment_bridge_ids = fields.One2many(
|
||||
"recruitment.employee.bridge",
|
||||
"employee_id",
|
||||
string="Onboarding Records",
|
||||
)
|
||||
recruitment_bridge_count = fields.Integer(compute="_compute_recruitment_bridge_count")
|
||||
|
||||
def _compute_recruitment_bridge_count(self):
|
||||
data = self.env["recruitment.employee.bridge"].read_group(
|
||||
[("employee_id", "in", self.ids), ("active", "=", True)],
|
||||
["employee_id"],
|
||||
["employee_id"],
|
||||
)
|
||||
counts = {item["employee_id"][0]: item["employee_id_count"] for item in data}
|
||||
for employee in self:
|
||||
employee.recruitment_bridge_count = counts.get(employee.id, 0)
|
||||
|
||||
def _get_or_create_employee_bridge(self):
|
||||
self.ensure_one()
|
||||
bridge = self.env["recruitment.employee.bridge"].search(
|
||||
[("employee_id", "=", self.id), ("active", "=", True)],
|
||||
limit=1,
|
||||
order="id desc",
|
||||
)
|
||||
if not bridge:
|
||||
bridge = self.env["recruitment.employee.bridge"].create({
|
||||
"employee_id": self.id,
|
||||
"company_id": self.company_id.id or self.env.company.id,
|
||||
"source": "jod",
|
||||
"state": "draft",
|
||||
})
|
||||
return bridge
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_or_create_employee_bridge()
|
||||
bridge.action_send_jod_form()
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
||||
def action_open_recruitment_bridge(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_or_create_employee_bridge()
|
||||
if not bridge:
|
||||
raise ValidationError(_("No onboarding record found."))
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class EmployeeBridgeRequestedAttachment(models.Model):
|
||||
_name = "employee.bridge.requested.attachment"
|
||||
_description = "Onboarding Requested Attachment"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
attachment_type = fields.Selection([
|
||||
("personal", "Personal Documents"),
|
||||
("education", "Education Documents"),
|
||||
("previous_employer", "Previous Employer"),
|
||||
("others", "Others"),
|
||||
], default="others", required=True)
|
||||
is_default = fields.Boolean(string="Is Default")
|
||||
|
||||
|
||||
class EmployeeBridgeAttachment(models.Model):
|
||||
_name = "employee.bridge.attachment"
|
||||
_description = "Onboarding Attachment"
|
||||
_rec_name = "name"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, ondelete="cascade")
|
||||
employee_id = fields.Many2one("hr.employee")
|
||||
requested_attachment_id = fields.Many2one("employee.bridge.requested.attachment")
|
||||
attachment_type = fields.Selection(related="requested_attachment_id.attachment_type")
|
||||
file = fields.Binary(required=True)
|
||||
file_name = fields.Char()
|
||||
review_status = fields.Selection([
|
||||
("draft", "Under Review"),
|
||||
("pass", "PASS"),
|
||||
("fail", "FAIL"),
|
||||
], default="draft")
|
||||
review_comments = fields.Char()
|
||||
|
||||
def action_preview_file(self):
|
||||
self.ensure_one()
|
||||
attachment = self.env["ir.attachment"].sudo().create({
|
||||
"name": self.file_name or self.name,
|
||||
"datas": self.file,
|
||||
"res_model": self._name,
|
||||
"res_id": self.id,
|
||||
"type": "binary",
|
||||
})
|
||||
return {
|
||||
"name": "File Preview",
|
||||
"type": "ir.actions.act_url",
|
||||
"url": f"/web/content/{attachment.id}?download=false",
|
||||
"target": "current",
|
||||
}
|
||||
|
||||
|
||||
class FamilyDetails(models.Model):
|
||||
_inherit = "family.details"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||
|
||||
|
||||
class EducationHistory(models.Model):
|
||||
_inherit = "education.history"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||
|
||||
|
||||
class EmployerHistory(models.Model):
|
||||
_inherit = "employer.history"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||
|
|
@ -0,0 +1,559 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class RecruitmentEmployeeBridge(models.Model):
|
||||
_name = "recruitment.employee.bridge"
|
||||
_description = "Onboarding"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_rec_name = "name"
|
||||
_order = "id desc"
|
||||
|
||||
@api.depends('name', 'employee_id', 'employee_name')
|
||||
def _compute_display_name(self):
|
||||
for record in self:
|
||||
employee_name = record.employee_name or record.employee_id.name or ''
|
||||
|
||||
if employee_name:
|
||||
record.display_name = f"{record.name} - {employee_name}"
|
||||
else:
|
||||
record.display_name = record.name
|
||||
|
||||
name = fields.Char(default="/", copy=False, readonly=True, tracking=True)
|
||||
active = fields.Boolean(default=True)
|
||||
employee_id = fields.Many2one(
|
||||
"hr.employee",
|
||||
string="Employee",
|
||||
ondelete="restrict",
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
source = fields.Selection(
|
||||
[
|
||||
("jod", "JOD"),
|
||||
("manual", "Manual"),
|
||||
("recruitment", "Recruitment"),
|
||||
],
|
||||
default="jod",
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "Draft"),
|
||||
("jod_sent", "JOD Sent"),
|
||||
("jod_received", "JOD Received"),
|
||||
("validated", "Validated"),
|
||||
],
|
||||
default="draft",
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
employee_name = fields.Char(string="Employee Name", tracking=True)
|
||||
employee_code = fields.Char(string="Employee Code", tracking=True)
|
||||
candidate_image = fields.Image(string="Photo")
|
||||
work_email = fields.Char(string="Work Email", tracking=True)
|
||||
private_email = fields.Char(string="Private Email")
|
||||
alternate_phone = fields.Char(string="Alternate Mobile")
|
||||
work_phone = fields.Char(string="Work Phone")
|
||||
mobile_phone = fields.Char(string="Mobile Phone")
|
||||
department_id = fields.Many2one("hr.department", string="Department")
|
||||
job_id = fields.Many2one("hr.job", string="Job Position")
|
||||
emp_type = fields.Many2one("hr.contract.type", string="Employment Type")
|
||||
doj = fields.Date(string="Date of Joining")
|
||||
gender = fields.Selection([
|
||||
("male", "Male"),
|
||||
("female", "Female"),
|
||||
("other", "Other"),
|
||||
])
|
||||
birthday = fields.Date()
|
||||
blood_group = fields.Selection([
|
||||
("A+", "A+"),
|
||||
("A-", "A-"),
|
||||
("B+", "B+"),
|
||||
("B-", "B-"),
|
||||
("O+", "O+"),
|
||||
("O-", "O-"),
|
||||
("AB+", "AB+"),
|
||||
("AB-", "AB-"),
|
||||
], string="Blood Group")
|
||||
marital = fields.Selection(
|
||||
selection=[
|
||||
("single", "Single"),
|
||||
("married", "Married"),
|
||||
("cohabitant", "Legal Cohabitant"),
|
||||
("widower", "Widower"),
|
||||
("divorced", "Divorced"),
|
||||
],
|
||||
default="single",
|
||||
string="Marital Status",
|
||||
)
|
||||
marriage_anniversary_date = fields.Date(string="Anniversary Date")
|
||||
private_street = fields.Char(string="Private Street")
|
||||
private_street2 = fields.Char(string="Private Street2")
|
||||
private_city = fields.Char(string="Private City")
|
||||
private_state_id = fields.Many2one("res.country.state", string="Private State")
|
||||
private_zip = fields.Char(string="Private Zip")
|
||||
private_country_id = fields.Many2one("res.country", string="Private Country")
|
||||
permanent_street = fields.Char(string="Permanent Street")
|
||||
permanent_street2 = fields.Char(string="Permanent Street2")
|
||||
permanent_city = fields.Char(string="Permanent City")
|
||||
permanent_state_id = fields.Many2one("res.country.state", string="Permanent State")
|
||||
permanent_zip = fields.Char(string="Permanent Zip")
|
||||
permanent_country_id = fields.Many2one("res.country", string="Permanent Country")
|
||||
pan_no = fields.Char(string="PAN No")
|
||||
identification_id = fields.Char(string="Aadhar No")
|
||||
previous_company_pf_no = fields.Char(string="Previous Company PF No")
|
||||
previous_company_uan_no = fields.Char(string="Previous Company UAN No")
|
||||
passport_no = fields.Char(string="Passport No")
|
||||
passport_start_date = fields.Date(string="Passport Issued Date")
|
||||
passport_end_date = fields.Date(string="Passport End Date")
|
||||
passport_issued_location = fields.Char(string="Passport Issued Location")
|
||||
full_name_as_in_bank = fields.Char(string="Name as per Bank")
|
||||
bank_name = fields.Char()
|
||||
bank_branch = fields.Char()
|
||||
bank_account_no = fields.Char()
|
||||
bank_ifsc_code = fields.Char()
|
||||
requested_attachment_ids = fields.Many2many(
|
||||
"employee.bridge.requested.attachment",
|
||||
"employee_bridge_requested_attachment_rel",
|
||||
"bridge_id",
|
||||
"attachment_id",
|
||||
string="Attachments to Request",
|
||||
)
|
||||
joining_attachment_ids = fields.One2many(
|
||||
"employee.bridge.attachment",
|
||||
"bridge_id",
|
||||
string="Attachments",
|
||||
)
|
||||
education_history = fields.One2many("education.history", "bridge_id", string="Education Details")
|
||||
employer_history = fields.One2many("employer.history", "bridge_id", string="Employer History")
|
||||
family_details = fields.One2many("family.details", "bridge_id", string="Family Details")
|
||||
joining_form_link = fields.Char(string="Joining Form Link", copy=False, readonly=True)
|
||||
access_token = fields.Char(copy=False, readonly=True)
|
||||
total_exp = fields.Float(string="Total Exp")
|
||||
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"unique_bridge_employee",
|
||||
"unique(employee_id)",
|
||||
"This employee is already connected to an employee bridge.",
|
||||
),
|
||||
]
|
||||
|
||||
def _get_base_url(self):
|
||||
return self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||
|
||||
def _ensure_access_token(self):
|
||||
for bridge in self:
|
||||
if not bridge.access_token:
|
||||
bridge.access_token = token_urlsafe(32)
|
||||
|
||||
def _get_joining_form_link(self):
|
||||
self.ensure_one()
|
||||
self._ensure_access_token()
|
||||
return "%s/employee_bridge/jod/%s" % (self._get_base_url(), self.access_token)
|
||||
|
||||
def _group_requested_attachments(self, attachments):
|
||||
return {
|
||||
"personal_docs": attachments.filtered(lambda a: a.attachment_type == "personal").mapped("name"),
|
||||
"education_docs": attachments.filtered(lambda a: a.attachment_type == "education").mapped("name"),
|
||||
"previous_employer_docs": attachments.filtered(lambda a: a.attachment_type == "previous_employer").mapped("name"),
|
||||
"other_docs": attachments.filtered(lambda a: a.attachment_type == "others").mapped("name"),
|
||||
}
|
||||
|
||||
def _send_jod_mail(self, req_attachment_ids=False, email_values=None, email_body=False):
|
||||
for bridge in self:
|
||||
email_to = bridge.work_email or bridge.private_email or bridge.employee_id.work_email
|
||||
if not email_to:
|
||||
raise ValidationError(_("Add an email on the bridge before sending the JOD form."))
|
||||
link = bridge._get_joining_form_link()
|
||||
bridge.write({
|
||||
"joining_form_link": link,
|
||||
"state": "jod_sent",
|
||||
})
|
||||
attachments = req_attachment_ids or self.env["employee.bridge.requested.attachment"].browse()
|
||||
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||
context = {
|
||||
"joining_form_link": link,
|
||||
**bridge._group_requested_attachments(attachments),
|
||||
}
|
||||
values = dict(email_values or {})
|
||||
values.setdefault("email_from", self.env.company.email or self.env.user.email)
|
||||
values.setdefault("email_to", email_to)
|
||||
if email_body:
|
||||
values["body_html"] = email_body
|
||||
if template:
|
||||
rendered_subject = template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id]
|
||||
rendered_body = template.with_context(**context)._render_field(
|
||||
"body_html", [bridge.id], compute_lang=True
|
||||
)[bridge.id]
|
||||
values.setdefault("subject", rendered_subject)
|
||||
values.setdefault("body_html", rendered_body)
|
||||
template.sudo().with_context(**context).send_mail(bridge.id, email_values=values, force_send=True)
|
||||
else:
|
||||
values.setdefault("subject", _("Joining Onboarding Details"))
|
||||
values.setdefault("body_html", _(
|
||||
"<p>Hello %(name)s,</p>"
|
||||
"<p>Please submit your joining onboarding details using this link:</p>"
|
||||
"<p><a href='%(link)s'>Open Joining Form</a></p>"
|
||||
) % {
|
||||
"name": bridge.employee_name or bridge.employee_id.name or _("Employee"),
|
||||
"link": link,
|
||||
})
|
||||
self.env["mail.mail"].sudo().create(values).send()
|
||||
return True
|
||||
|
||||
def action_send_jod_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Select Attachments"),
|
||||
"res_model": "employee.bridge.attachment.wizard",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_bridge_id": self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_open_employee(self):
|
||||
self.ensure_one()
|
||||
if not self.employee_id:
|
||||
raise ValidationError(_("No employee is linked to this bridge."))
|
||||
return {
|
||||
"name": _("Employee"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.employee",
|
||||
"view_mode": "form",
|
||||
"res_id": self.employee_id.id,
|
||||
}
|
||||
|
||||
def action_validate_employee_details(self):
|
||||
for bridge in self:
|
||||
if not bridge.employee_id:
|
||||
raise ValidationError(_("Link or create an employee before validating the JOD details."))
|
||||
bridge.employee_id.write(bridge._get_employee_update_vals())
|
||||
bridge.education_history.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.employer_history.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.family_details.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.joining_attachment_ids.write({"employee_id": bridge.employee_id.id})
|
||||
bridge.state = "validated"
|
||||
|
||||
def _get_employee_create_vals(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
"name": self.employee_name,
|
||||
"employee_id": self.employee_code,
|
||||
"work_email": self.work_email,
|
||||
"private_email": self.private_email,
|
||||
"work_phone": self.work_phone,
|
||||
"mobile_phone": self.mobile_phone,
|
||||
"image_1920": self.candidate_image,
|
||||
"department_id": self.department_id.id,
|
||||
"job_id": self.job_id.id,
|
||||
"job_title": self.job_id.name,
|
||||
"emp_type": self.emp_type.id,
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
vals = {key: value for key, value in vals.items() if value not in (False, "", 0)}
|
||||
if not vals.get("name"):
|
||||
raise ValidationError(_("Employee Name is required to create an employee."))
|
||||
return vals
|
||||
|
||||
def _get_employee_update_vals(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
"name": self.employee_name,
|
||||
"employee_id": self.employee_code,
|
||||
"work_email": self.work_email,
|
||||
"private_email": self.private_email,
|
||||
"work_phone": self.work_phone,
|
||||
"mobile_phone": self.mobile_phone,
|
||||
"image_1920": self.candidate_image,
|
||||
"department_id": self.department_id.id,
|
||||
"job_id": self.job_id.id,
|
||||
"job_title": self.job_id.name,
|
||||
"emp_type": self.emp_type.id,
|
||||
"doj": self.doj,
|
||||
"gender": self.gender,
|
||||
"birthday": self.birthday,
|
||||
"blood_group": self.blood_group,
|
||||
"marital": self.marital,
|
||||
"marriage_anniversary_date": self.marriage_anniversary_date,
|
||||
"private_street": self.private_street,
|
||||
"private_street2": self.private_street2,
|
||||
"private_city": self.private_city,
|
||||
"private_state_id": self.private_state_id.id,
|
||||
"private_zip": self.private_zip,
|
||||
"private_country_id": self.private_country_id.id,
|
||||
"permanent_street": self.permanent_street,
|
||||
"permanent_street2": self.permanent_street2,
|
||||
"permanent_city": self.permanent_city,
|
||||
"permanent_state_id": self.permanent_state_id.id,
|
||||
"permanent_zip": self.permanent_zip,
|
||||
"permanent_country_id": self.permanent_country_id.id,
|
||||
"pan_no": self.pan_no,
|
||||
"identification_id": self.identification_id,
|
||||
"previous_company_pf_no": self.previous_company_pf_no,
|
||||
"previous_company_uan_no": self.previous_company_uan_no,
|
||||
"passport_id": self.passport_no,
|
||||
"passport_start_date": self.passport_start_date,
|
||||
"passport_end_date": self.passport_end_date,
|
||||
"passport_issued_location": self.passport_issued_location,
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
for key, value in vals.items()
|
||||
if key in self.employee_id._fields and value not in (False, "", 0)
|
||||
}
|
||||
|
||||
def action_create_employee(self):
|
||||
for bridge in self:
|
||||
if bridge.employee_id:
|
||||
continue
|
||||
bridge.employee_id = self.env["hr.employee"].sudo().create(bridge._get_employee_create_vals())
|
||||
return self.action_open_employee()
|
||||
|
||||
def _sync_from_employee(self):
|
||||
for bridge in self.filtered("employee_id"):
|
||||
employee = bridge.employee_id
|
||||
bridge.write({
|
||||
"employee_name": bridge.employee_name or employee.name,
|
||||
"employee_code": bridge.employee_code or employee.employee_id,
|
||||
"work_email": bridge.work_email or employee.work_email,
|
||||
"private_email": bridge.private_email or employee.private_email,
|
||||
"work_phone": bridge.work_phone or employee.work_phone,
|
||||
"mobile_phone": bridge.mobile_phone or employee.mobile_phone,
|
||||
"department_id": bridge.department_id.id or employee.department_id.id,
|
||||
"job_id": bridge.job_id.id or employee.job_id.id,
|
||||
"emp_type": bridge.emp_type.id or employee.emp_type.id,
|
||||
"company_id": employee.company_id.id or bridge.company_id.id,
|
||||
"doj": bridge.doj or employee.doj,
|
||||
"gender": bridge.gender or employee.gender,
|
||||
"birthday": bridge.birthday or employee.birthday,
|
||||
"blood_group": bridge.blood_group or employee.blood_group,
|
||||
"marital": bridge.marital or employee.marital,
|
||||
"marriage_anniversary_date": bridge.marriage_anniversary_date or employee.marriage_anniversary_date,
|
||||
"private_street": bridge.private_street or employee.private_street,
|
||||
"private_street2": bridge.private_street2 or employee.private_street2,
|
||||
"private_city": bridge.private_city or employee.private_city,
|
||||
"private_state_id": bridge.private_state_id.id or employee.private_state_id.id,
|
||||
"private_zip": bridge.private_zip or employee.private_zip,
|
||||
"private_country_id": bridge.private_country_id.id or employee.private_country_id.id,
|
||||
"permanent_street": bridge.permanent_street or employee.permanent_street,
|
||||
"permanent_street2": bridge.permanent_street2 or employee.permanent_street2,
|
||||
"permanent_city": bridge.permanent_city or employee.permanent_city,
|
||||
"permanent_state_id": bridge.permanent_state_id.id or employee.permanent_state_id.id,
|
||||
"permanent_zip": bridge.permanent_zip or employee.permanent_zip,
|
||||
"permanent_country_id": bridge.permanent_country_id.id or employee.permanent_country_id.id,
|
||||
"pan_no": bridge.pan_no or employee.pan_no,
|
||||
"identification_id": bridge.identification_id or employee.identification_id,
|
||||
"previous_company_pf_no": bridge.previous_company_pf_no or employee.previous_company_pf_no,
|
||||
"previous_company_uan_no": bridge.previous_company_uan_no or employee.previous_company_uan_no,
|
||||
"passport_no": bridge.passport_no or employee.passport_id,
|
||||
"passport_start_date": bridge.passport_start_date or employee.passport_start_date,
|
||||
"passport_end_date": bridge.passport_end_date or employee.passport_end_date,
|
||||
"passport_issued_location": bridge.passport_issued_location or employee.passport_issued_location,
|
||||
})
|
||||
|
||||
@api.onchange("employee_id")
|
||||
def _onchange_employee_id(self):
|
||||
for bridge in self:
|
||||
employee = bridge.employee_id
|
||||
if not employee:
|
||||
continue
|
||||
bridge.employee_name = bridge.employee_name or employee.name
|
||||
bridge.employee_code = bridge.employee_code or employee.employee_id
|
||||
bridge.work_email = bridge.work_email or employee.work_email
|
||||
bridge.private_email = bridge.private_email or employee.private_email
|
||||
bridge.work_phone = bridge.work_phone or employee.work_phone
|
||||
bridge.mobile_phone = bridge.mobile_phone or employee.mobile_phone
|
||||
bridge.department_id = bridge.department_id or employee.department_id
|
||||
bridge.job_id = bridge.job_id or employee.job_id
|
||||
bridge.emp_type = bridge.emp_type or employee.emp_type
|
||||
|
||||
def write_from_public_form(self, values):
|
||||
self.ensure_one()
|
||||
private_state = self.env["res.country.state"].sudo().browse(int(values.get("present_state") or 0))
|
||||
permanent_state = self.env["res.country.state"].sudo().browse(int(values.get("permanent_state") or 0))
|
||||
mapped_values = dict(values)
|
||||
mapped_values.update({
|
||||
"candidate_image": values.get("candidate_image_base64"),
|
||||
"private_email": values.get("email_from"),
|
||||
"mobile_phone": values.get("partner_phone"),
|
||||
"alternate_phone": values.get("alternate_phone"),
|
||||
"private_street": values.get("present_street"),
|
||||
"private_street2": values.get("present_street2"),
|
||||
"private_city": values.get("present_city"),
|
||||
"private_zip": values.get("present_zip"),
|
||||
"private_state_id": private_state.id if private_state else False,
|
||||
"private_country_id": private_state.country_id.id if private_state else False,
|
||||
"permanent_state_id": permanent_state.id if permanent_state else False,
|
||||
"permanent_country_id": permanent_state.country_id.id if permanent_state else False,
|
||||
})
|
||||
allowed_fields = {
|
||||
"employee_name",
|
||||
"candidate_image",
|
||||
"private_email",
|
||||
"mobile_phone",
|
||||
"alternate_phone",
|
||||
"doj",
|
||||
"gender",
|
||||
"birthday",
|
||||
"blood_group",
|
||||
"marital",
|
||||
"marriage_anniversary_date",
|
||||
"private_street",
|
||||
"private_street2",
|
||||
"private_city",
|
||||
"private_state_id",
|
||||
"private_country_id",
|
||||
"private_zip",
|
||||
"permanent_street",
|
||||
"permanent_street2",
|
||||
"permanent_city",
|
||||
"permanent_state_id",
|
||||
"permanent_country_id",
|
||||
"permanent_zip",
|
||||
"full_name_as_in_bank",
|
||||
"bank_name",
|
||||
"bank_branch",
|
||||
"bank_account_no",
|
||||
"bank_ifsc_code",
|
||||
"pan_no",
|
||||
"identification_id",
|
||||
"previous_company_pf_no",
|
||||
"previous_company_uan_no",
|
||||
"passport_no",
|
||||
"passport_start_date",
|
||||
"passport_end_date",
|
||||
"passport_issued_location",
|
||||
}
|
||||
bridge_vals = {
|
||||
field_name: value or False
|
||||
for field_name, value in mapped_values.items()
|
||||
if field_name in allowed_fields and field_name in self._fields
|
||||
}
|
||||
if bridge_vals:
|
||||
self.sudo().write(bridge_vals)
|
||||
self._write_history_from_public_form(values)
|
||||
self._replace_joining_attachments_from_public_form(values)
|
||||
self.state = "jod_received"
|
||||
|
||||
def _safe_date(self, value):
|
||||
try:
|
||||
return datetime.strptime(value, "%Y-%m-%d").date() if value else False
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def _safe_int(self, value):
|
||||
try:
|
||||
return int(value) if value else False
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def _json_list(self, values, key):
|
||||
raw = values.get(key, "[]")
|
||||
try:
|
||||
data = json.loads(raw) if raw else []
|
||||
except (TypeError, ValueError):
|
||||
data = []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
def _write_history_from_public_form(self, values):
|
||||
self.ensure_one()
|
||||
family_values = []
|
||||
for member in self._json_list(values, "family_data_json"):
|
||||
if member.get("name") and member.get("relation"):
|
||||
family_values.append((0, 0, {
|
||||
"relation_type": member.get("relation"),
|
||||
"name": member.get("name"),
|
||||
"contact_no": member.get("contact"),
|
||||
"dob": self._safe_date(member.get("dob")),
|
||||
"location": member.get("location"),
|
||||
}))
|
||||
|
||||
education_values = []
|
||||
for education in self._json_list(values, "education_data_json"):
|
||||
if education.get("specialization"):
|
||||
education_values.append((0, 0, {
|
||||
"education_type": education.get("education_type") or "additional",
|
||||
"name": education.get("specialization"),
|
||||
"university": education.get("university"),
|
||||
"start_year": self._safe_int(education.get("start_year")),
|
||||
"end_year": self._safe_int(education.get("end_year")),
|
||||
"marks_or_grade": education.get("marks_or_grade"),
|
||||
}))
|
||||
|
||||
employer_values = []
|
||||
for employer in self._json_list(values, "employer_history_data_json"):
|
||||
if employer.get("company_name"):
|
||||
employer_values.append((0, 0, {
|
||||
"company_name": employer.get("company_name"),
|
||||
"designation": employer.get("designation"),
|
||||
"date_of_joining": self._safe_date(employer.get("date_of_joining")),
|
||||
"last_working_day": self._safe_date(employer.get("last_working_day")),
|
||||
"ctc": employer.get("ctc"),
|
||||
}))
|
||||
|
||||
write_vals = {}
|
||||
if family_values:
|
||||
write_vals["family_details"] = [(5, 0, 0)] + family_values
|
||||
if education_values:
|
||||
write_vals["education_history"] = [(5, 0, 0)] + education_values
|
||||
if employer_values:
|
||||
write_vals["employer_history"] = [(5, 0, 0)] + employer_values
|
||||
if write_vals:
|
||||
self.sudo().write(write_vals)
|
||||
|
||||
def _replace_joining_attachments_from_public_form(self, values):
|
||||
self.ensure_one()
|
||||
attachment_values = []
|
||||
seen_files = set()
|
||||
for item in self._json_list(values, "attachments_data_json"):
|
||||
attachment = self.env["employee.bridge.requested.attachment"].sudo().browse(
|
||||
int(item.get("attachment_rec_id") or 0)
|
||||
)
|
||||
file_content = item.get("file_content")
|
||||
file_name = item.get("file_name")
|
||||
file_key = (attachment.id, file_name, file_content)
|
||||
if attachment.exists() and file_content and file_key not in seen_files:
|
||||
seen_files.add(file_key)
|
||||
attachment_values.append((0, 0, {
|
||||
"name": attachment.name,
|
||||
"requested_attachment_id": attachment.id,
|
||||
"file": file_content,
|
||||
"file_name": file_name,
|
||||
}))
|
||||
self.sudo().write({"joining_attachment_ids": [(5, 0, 0)] + attachment_values})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("name", "/") == "/":
|
||||
vals["name"] = self.env["ir.sequence"].next_by_code("recruitment.employee.bridge") or "/"
|
||||
if vals.get("employee_id") and not vals.get("company_id"):
|
||||
employee = self.env["hr.employee"].browse(vals["employee_id"])
|
||||
vals["company_id"] = employee.company_id.id or self.env.company.id
|
||||
bridges = super().create(vals_list)
|
||||
bridges._sync_from_employee()
|
||||
return bridges
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if "employee_id" in vals:
|
||||
self._sync_from_employee()
|
||||
return res
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_employee_bridge_user,employee.bridge.user,model_recruitment_employee_bridge,base.group_user,1,1,1,0
|
||||
access_employee_bridge_manager,employee.bridge.manager,model_recruitment_employee_bridge,hr.group_hr_manager,1,1,1,1
|
||||
access_employee_bridge_requested_attachment_user,employee.bridge.requested.attachment.user,model_employee_bridge_requested_attachment,base.group_user,1,1,1,0
|
||||
access_employee_bridge_requested_attachment_manager,employee.bridge.requested.attachment.manager,model_employee_bridge_requested_attachment,hr.group_hr_manager,1,1,1,1
|
||||
access_employee_bridge_attachment_user,employee.bridge.attachment.user,model_employee_bridge_attachment,base.group_user,1,1,1,0
|
||||
access_employee_bridge_attachment_manager,employee.bridge.attachment.manager,model_employee_bridge_attachment,hr.group_hr_manager,1,1,1,1
|
||||
access_employee_bridge_attachment_wizard,employee.bridge.attachment.wizard.user,model_employee_bridge_attachment_wizard,base.group_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,754 @@
|
|||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { assets, loadCSS, loadJS } from "@web/core/assets";
|
||||
|
||||
publicWidget.registry.employeeBridgeJod = publicWidget.Widget.extend({
|
||||
selector: "#post_onboarding_form",
|
||||
events: {
|
||||
"change [name='candidate_image']": "previewApplicantPhoto",
|
||||
"click #delete-photo-btn": "deleteCandidatePhoto",
|
||||
"click #preview-photo-btn": "previewFullImage",
|
||||
"click #add-education-row": "addEducationRow", // Ensure button click event is correctly bound
|
||||
|
||||
|
||||
|
||||
'change .attachment-input': 'handleAttachmentUpload',
|
||||
'click .upload-new-btn': 'handleUploadNewFile',
|
||||
'click .delete-file-btn': 'handleDeleteUploadedFile',
|
||||
'input .file-name-input': 'handleFileNameChange',
|
||||
"click .remove-file": "removeFile",
|
||||
"click .preview-file": "previewFile",
|
||||
"click .view-attachments-btn": "openAttachmentModal", // Opens modal with files
|
||||
"click .close-modal-btn": "closeAttachmentModal", // Close modal
|
||||
"submit": "handleFormSubmit",
|
||||
"change [name='experience']": "onChangeExperience",
|
||||
"change [name='marital']": "onChangeMarital",
|
||||
},
|
||||
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.uploadedFiles = {}; // Store files per attachment ID for this form only.
|
||||
this.isSubmitting = false;
|
||||
},
|
||||
|
||||
_getFileSignature(file) {
|
||||
return [file.name, file.size, file.lastModified, file.type].join("|");
|
||||
},
|
||||
|
||||
addUploadedFileRow(attachmentId, file, base64String) {
|
||||
console.log("addUploadedFileRow", attachmentId, file.name);
|
||||
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
||||
|
||||
if (!this.uploadedFiles[attachmentId]) {
|
||||
this.uploadedFiles[attachmentId] = [];
|
||||
}
|
||||
|
||||
const fileSignature = this._getFileSignature(file);
|
||||
const isDuplicate = this.uploadedFiles[attachmentId].some(
|
||||
uploadedFile => uploadedFile.signature === fileSignature
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a unique file ID using attachmentId and a timestamp
|
||||
const fileId = `${attachmentId}-${Date.now()}-${this.uploadedFiles[attachmentId].length}`;
|
||||
|
||||
const fileRecord = {
|
||||
attachment_rec_id : attachmentId,
|
||||
id: fileId, // Unique file ID
|
||||
name: file.name,
|
||||
base64: base64String,
|
||||
type: file.type,
|
||||
signature: fileSignature,
|
||||
};
|
||||
|
||||
this.uploadedFiles[attachmentId].push(fileRecord);
|
||||
|
||||
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
|
||||
const previewImageId = `preview_image_${fileId}`;
|
||||
const fileNameInputId = `file_name_input_${fileId}`;
|
||||
let previewContent = '';
|
||||
let previewClickHandler = '';
|
||||
|
||||
// Check if the file is an image or PDF and set preview content accordingly
|
||||
if (file.type.startsWith('image/')) {
|
||||
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
|
||||
</div>`;
|
||||
previewClickHandler = () => {
|
||||
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
||||
this.$('#modal_attachment_photo_preview').show();
|
||||
this.$('#modal_attachment_pdf_preview').hide(); // Hide PDF preview
|
||||
this.$('#attachmentPreviewModal').modal('show');
|
||||
};
|
||||
} else if (file.type === 'application/pdf') {
|
||||
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
|
||||
</div>`;
|
||||
previewClickHandler = () => {
|
||||
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
||||
this.$('#modal_attachment_pdf_preview').show();
|
||||
this.$('#modal_attachment_photo_preview').hide(); // Hide image preview
|
||||
this.$('#attachmentPreviewModal').modal('show');
|
||||
};
|
||||
}
|
||||
|
||||
// Append new row to the table with a preview and buttons
|
||||
tableBody.append(`
|
||||
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<td>
|
||||
<input type="text" class="form-control file-name-input" id="${fileNameInputId}" value="${file.name}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column align-items-center justify-content-center gap-2">
|
||||
${previewContent}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-danger btn-sm delete-file-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-info btn-sm preview-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
||||
|
||||
// Attach click handler for preview (image or PDF)
|
||||
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
|
||||
|
||||
// Attach click handler for the preview button (to trigger modal)
|
||||
this.$(`.preview-btn[data-attachment-id="${attachmentId}"][data-file-id="${fileId}"]`).on('click', previewClickHandler);
|
||||
},
|
||||
|
||||
handleDeleteUploadedFile(ev) {
|
||||
const button = ev.currentTarget;
|
||||
const attachmentId = $(button).data('attachment-id');
|
||||
const fileId = $(button).data('file-id');
|
||||
|
||||
// Find the index of the file to delete based on unique file ID
|
||||
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
this.uploadedFiles[attachmentId].splice(fileIndex, 1); // Remove from array
|
||||
}
|
||||
|
||||
// Remove the row from DOM
|
||||
this.$(`tr[data-file-id="${fileId}"]`).remove();
|
||||
|
||||
// Hide table if no files left
|
||||
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
handleAttachmentUpload(ev) {
|
||||
console.log("handleAttachmentUpload");
|
||||
const input = ev.target;
|
||||
const attachmentId = $(input).data('attachment-id');
|
||||
|
||||
if (input.files.length > 0) {
|
||||
Array.from(input.files).forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
$(input).val('');
|
||||
},
|
||||
|
||||
handleUploadNewFile(ev) {
|
||||
const button = $(ev.currentTarget);
|
||||
const attachmentId = button.data('attachment-id');
|
||||
const index = button.data('index');
|
||||
const hiddenInput = this.$(`.upload-new-file-input[data-attachment-id='${attachmentId}']`);
|
||||
|
||||
hiddenInput.off('change').on('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64String = event.target.result.split(',')[1];
|
||||
this.uploadedFiles[attachmentId][index] = {
|
||||
name: file.name,
|
||||
base64: base64String,
|
||||
};
|
||||
|
||||
const imageId = `preview_image_${attachmentId}_${index}`;
|
||||
const filePreviewWrapperId = `preview_wrapper_${attachmentId}_${index}`;
|
||||
const fileType = file.type;
|
||||
|
||||
if (fileType.startsWith('image/')) {
|
||||
this.$(`#${filePreviewWrapperId}`).html(`
|
||||
<img id="${imageId}" src="data:image/png;base64,${base64String}" class="img-thumbnail"
|
||||
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;" />
|
||||
`);
|
||||
} else if (fileType === 'application/pdf') {
|
||||
this.$(`#${filePreviewWrapperId}`).html(`
|
||||
<iframe src="data:application/pdf;base64,${base64String}" width="80" height="80" class="img-thumbnail" style="border: none; cursor: pointer;"></iframe>
|
||||
`);
|
||||
}
|
||||
|
||||
const fileNameInputId = `file_name_input_${attachmentId}_${index}`;
|
||||
this.$(`#${fileNameInputId}`).val(file.name);
|
||||
|
||||
// CRITICAL FIX: Clear input after reading
|
||||
$(e.target).val('');
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
hiddenInput.trigger('click');
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleFileNameChange(event) {
|
||||
const attachmentId = $(event.target).closest('tr').data('attachment-id');
|
||||
const fileId = $(event.target).closest('tr').data('file-id');
|
||||
const newFileName = event.target.value;
|
||||
|
||||
if (!attachmentId || !fileId) {
|
||||
console.error('Missing attachmentId or fileId');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileList = this.uploadedFiles[attachmentId];
|
||||
|
||||
if (!fileList) {
|
||||
console.error(`No files found for attachmentId: ${attachmentId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileRecord = fileList.find(file => file.id === fileId);
|
||||
|
||||
if (!fileRecord) {
|
||||
console.error(`File with ID ${fileId} not found under attachment ${attachmentId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
fileRecord.name = newFileName;
|
||||
},
|
||||
|
||||
|
||||
|
||||
renderFilePreview(attachmentId) {
|
||||
const container = this.$(`#preview_container_${attachmentId}`);
|
||||
container.empty();
|
||||
|
||||
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||
container.addClass("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
container.removeClass("d-none");
|
||||
|
||||
this.uploadedFiles[attachmentId].forEach((file, index) => {
|
||||
const fileHtml = $(`
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<div class="position-relative">
|
||||
<img src="${file.base64}" class="rounded-circle shadow-sm" style="width: 80px; height: 80px; object-fit: cover; border: 1px solid #ddd; cursor: pointer;" data-index="${index}" data-attachment-id="${attachmentId}" />
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 remove-file" data-attachment-id="${attachmentId}" data-index="${index}" style="transform: translate(50%, -50%);"><i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
<small>${file.name}</small>
|
||||
</div>
|
||||
`);
|
||||
|
||||
fileHtml.find("img").on("click", this.previewAttachmentImage.bind(this));
|
||||
fileHtml.find(".remove-file").on("click", this.removeFile.bind(this));
|
||||
|
||||
container.append(fileHtml);
|
||||
});
|
||||
},
|
||||
|
||||
previewAttachmentImage(ev) {
|
||||
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||
const index = $(ev.currentTarget).data("index");
|
||||
const fileData = this.uploadedFiles[attachmentId][index];
|
||||
|
||||
this.$("#attachment_modal_preview").attr("src", fileData.base64);
|
||||
this.$("#attachmentPreviewModal").modal("show");
|
||||
},
|
||||
|
||||
removeFile(ev) {
|
||||
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||
const index = $(ev.currentTarget).data("index");
|
||||
|
||||
this.uploadedFiles[attachmentId].splice(index, 1);
|
||||
this.renderFilePreview(attachmentId);
|
||||
},
|
||||
|
||||
|
||||
previewFile(ev) {
|
||||
const fileUrl = ev.currentTarget.dataset.fileUrl;
|
||||
const modal = this.$("#photoPreviewModal");
|
||||
|
||||
this.$("#modal_photo_preview").attr("src", fileUrl);
|
||||
modal.modal("show");
|
||||
},
|
||||
|
||||
onChangeExperience(event) {
|
||||
const selectedValue = $(event.currentTarget).val();
|
||||
const employerHistorySection = $("#employer_history_data");
|
||||
|
||||
if (selectedValue === "experienced") {
|
||||
employerHistorySection.show();
|
||||
} else {
|
||||
employerHistorySection.hide();
|
||||
}
|
||||
},
|
||||
|
||||
onChangeMarital(event) {
|
||||
const selectedValue = $(event.currentTarget).val();
|
||||
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
|
||||
const family_details_data_spouse = this.$('#family_details_data_spouse')
|
||||
const family_details_data_kid1 = this.$('#family_details_data_kid1')
|
||||
const family_details_data_kid2 = this.$('#family_details_data_kid2')
|
||||
|
||||
|
||||
if (selectedValue === "married") {
|
||||
marriageAnniversarySection.show();
|
||||
// Show rows for spouse, kid1, and kid2
|
||||
family_details_data_spouse.show();
|
||||
family_details_data_kid1.show();
|
||||
family_details_data_kid2.show();
|
||||
} else {
|
||||
marriageAnniversarySection.hide();
|
||||
// Hide rows for spouse, kid1, and kid2
|
||||
family_details_data_spouse.hide();
|
||||
family_details_data_kid1.hide();
|
||||
family_details_data_kid2.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open modal and display uploaded files
|
||||
*/
|
||||
openAttachmentModal(event) {
|
||||
console.log("openAttachmentModal");
|
||||
const rowId = $(event.currentTarget).closest("tr").index(); // Get the row index
|
||||
this.currentRowId = rowId; // Store rowId for reference
|
||||
this.renderAttachmentModal(rowId); // Render the modal for the row
|
||||
},
|
||||
|
||||
renderAttachmentModal(rowId) {
|
||||
const fileList = this.uploadedFiles && this.uploadedFiles[rowId] ? this.uploadedFiles[rowId] : [];
|
||||
|
||||
let modalHtml = `
|
||||
<div id="attachmentModal" class="modal fade show" tabindex="-1" style="display: block; background: rgba(0,0,0,0.5);" aria-modal="true">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 600px;">
|
||||
<div class="modal-content" style="border-radius: 8px; box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header" style="background: #143d5d; color: white; border-bottom: 2px solid #dee2e6; font-weight: bold; padding: 12px 15px;">
|
||||
<h5 class="modal-title" style="margin: 0;">Uploaded Attachments</h5>
|
||||
<button type="button" class="close close-modal-btn" data-dismiss="modal" aria-label="Close" style="background: none; border: none; font-size: 20px; color: white; cursor: pointer;">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body" style="padding: 15px;">
|
||||
<ul class="attachment-list" style="list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto;">
|
||||
${fileList.length ? fileList.map((file, index) => `
|
||||
<li style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa;">
|
||||
<span style="font-weight: 500; flex-grow: 1;">${file.name}</span>
|
||||
<button type="button" class="remove-file" data-index="${index}"
|
||||
style="background: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;">
|
||||
<i style="margin-right: 5px;">🗑️</i>
|
||||
</button>
|
||||
</li>
|
||||
`).join("") : `<p style="color: #6c757d; text-align: center; font-size: 16px; padding: 10px;">No files uploaded.</p>`}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 12px;">
|
||||
<button type="button" class="btn close-modal-btn" data-dismiss="modal"
|
||||
style="background: #6c757d; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Remove old modal and append new one
|
||||
$("#attachmentModal").remove();
|
||||
$("body").append(modalHtml);
|
||||
|
||||
// Attach remove event to new modal content
|
||||
$("#attachmentModal").on("click", ".remove-file", this.removeFile.bind(this));
|
||||
$("#attachmentModal").on("click", ".close-modal-btn", this.closeAttachmentModal.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Close attachment modal
|
||||
*/
|
||||
closeAttachmentModal() {
|
||||
console.log("Closing modal");
|
||||
$("#attachmentModal").remove(); // Remove the modal from the DOM
|
||||
},
|
||||
|
||||
|
||||
// Function to preview the uploaded image
|
||||
previewApplicantPhoto(ev) {
|
||||
const input = ev.currentTarget;
|
||||
const preview = this.$("#photo_preview");
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
|
||||
preview.attr("src", e.target.result);
|
||||
|
||||
// Store the base64 in a hidden input field
|
||||
this.$("input[name='candidate_image_base64']").val(base64String);
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
},
|
||||
|
||||
// Function to delete the uploaded image
|
||||
deleteCandidatePhoto() {
|
||||
const preview = this.$("#photo_preview");
|
||||
const inputFile = this.$("input[name='candidate_image']");
|
||||
|
||||
preview.attr("src", "data:image/png;base64,"); // Reset preview
|
||||
inputFile.val(""); // Reset file input
|
||||
},
|
||||
|
||||
// Function to preview full image inside a modal
|
||||
previewFullImage() {
|
||||
const previewSrc = this.$("#photo_preview").attr("src");
|
||||
if (previewSrc) {
|
||||
this.$("#modal_photo_preview").attr("src", previewSrc);
|
||||
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
|
||||
}
|
||||
},
|
||||
|
||||
// Function to add a new education row dynamically
|
||||
addEducationRow(ev) {
|
||||
ev.preventDefault(); // Prevent default behavior
|
||||
|
||||
let newRow = `
|
||||
<tr class="predefined-row">
|
||||
<td class="education-relation-col">
|
||||
<select name="education_type" class="form-control">
|
||||
<option value="10">10th</option>
|
||||
<option value="inter">Inter</option>
|
||||
<option value="graduation">Graduation</option>
|
||||
<option value="post_graduation">Post Graduation</option>
|
||||
<option value="additional">Additional Qualification</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="specialization" class="form-control" placeholder="Enter Specialization"/></td>
|
||||
<td><input type="text" name="university" class="form-control" placeholder="Enter University"/></td>
|
||||
<td><input type="number" name="start_year" class="form-control" placeholder="Start Year"/></td>
|
||||
<td><input type="number" name="end_year" class="form-control" placeholder="End Year"/></td>
|
||||
<td><input type="text" name="marks_grade" class="form-control" placeholder="Marks/Grade"/></td>
|
||||
</tr>
|
||||
|
||||
`;
|
||||
|
||||
this.$("#education_details_data tbody").append(newRow); // Append new row inside the table
|
||||
console.log("New education row added!");
|
||||
},
|
||||
|
||||
|
||||
validateFamilyDetails() {
|
||||
let familyDetailsFilled = false;
|
||||
let educationDetailsFilled = false;
|
||||
const family_rows = document.querySelectorAll('#family_details_data tbody tr');
|
||||
const education_rows = document.querySelectorAll('#education_details_data tbody tr');
|
||||
|
||||
|
||||
family_rows.forEach(row => {
|
||||
const inputs = row.querySelectorAll('input[type="text"], input[type="date"]');
|
||||
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
|
||||
if (isRowFilled) {
|
||||
familyDetailsFilled = true;
|
||||
}
|
||||
});
|
||||
|
||||
education_rows.forEach(row => {
|
||||
const inputs = row.querySelectorAll('input[type="text"]');
|
||||
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
|
||||
if (isRowFilled) {
|
||||
educationDetailsFilled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!familyDetailsFilled) {
|
||||
alert('Please fill at least one family member detail.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!educationDetailsFilled) {
|
||||
alert('Please fill at least one Education details')
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
handleFormSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.validateFamilyDetails()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
this.$("#submit-btn").prop("disabled", true);
|
||||
|
||||
let employerHistoryData = [];
|
||||
this.$("#employer_history_data tbody tr").each((index, row) => {
|
||||
let rowData = {
|
||||
company_name: this.$(row).find("[name$='company_name']").val()?.trim() || "",
|
||||
designation: this.$(row).find("[name$='designation']").val()?.trim() || "",
|
||||
date_of_joining: this.$(row).find("[name$='doj']").val()?.trim() || "",
|
||||
last_working_day: this.$(row).find("[name$='lwd']").val()?.trim() || "",
|
||||
ctc: this.$(row).find("[name$='ctc']").val()?.trim() || "",
|
||||
};
|
||||
if (Object.values(rowData).some(value => value)) {
|
||||
employerHistoryData.push(rowData);
|
||||
}
|
||||
});
|
||||
|
||||
let familyData = [];
|
||||
this.$("#family_details_data tbody tr").each((index, row) => {
|
||||
let rowData = {
|
||||
relation: this.$(row).find(".relation-col").attr("value")?.trim(),
|
||||
name: this.$(row).find("[name$='_name']").val()?.trim() || "",
|
||||
contact: this.$(row).find("[name$='_contact']").val()?.trim() || "",
|
||||
dob: this.$(row).find("[name$='_dob']").val()?.trim() || "",
|
||||
location: this.$(row).find("[name$='_location']").val()?.trim() || "",
|
||||
};
|
||||
if (Object.entries(rowData).some(([key, value]) => key !== 'relation' && value)) {
|
||||
familyData.push(rowData);
|
||||
}
|
||||
});
|
||||
|
||||
let educationData = [];
|
||||
this.$("#education_details_data tbody tr").each((index, row) => {
|
||||
let educationCell = this.$(row).find(".education-relation-col");
|
||||
let educationType = educationCell.find("select").val()?.trim() || educationCell.attr("value")?.trim();
|
||||
|
||||
let rowData = {
|
||||
education_type: educationType,
|
||||
specialization: this.$(row).find("[name='specialization']").val()?.trim() || "",
|
||||
university: this.$(row).find("[name='university']").val()?.trim() || "",
|
||||
start_year: this.$(row).find("[name='start_year']").val()?.trim() || "",
|
||||
end_year: this.$(row).find("[name='end_year']").val()?.trim() || "",
|
||||
marks_or_grade: this.$(row).find("[name='marks_grade']").val()?.trim() || "",
|
||||
};
|
||||
if (Object.entries(rowData).some(([key, value]) => key !== 'education_type' && value)) {
|
||||
educationData.push(rowData);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let attachments = [];
|
||||
let fileReadPromises = [];
|
||||
let attachmentInputs = this.uploadedFiles; // your object {1: Array(1), 2: Array(2)}
|
||||
|
||||
Object.keys(attachmentInputs).forEach(key => {
|
||||
let filesArray = attachmentInputs[key];
|
||||
if (filesArray && Array.isArray(filesArray)) {
|
||||
filesArray.forEach(file => {
|
||||
// Only push if base64 actually exists (prevents ghost/empty data)
|
||||
if (file && file.base64) {
|
||||
attachments.push({
|
||||
attachment_rec_id: file.attachment_rec_id,
|
||||
file_name: file.name,
|
||||
file_content: file.base64,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Promise.all(fileReadPromises).then(() => {
|
||||
this.$("#family_data_json").val(JSON.stringify(familyData));
|
||||
this.$("#education_data_json").val(JSON.stringify(educationData));
|
||||
this.$("#employer_history_data_json").val(JSON.stringify(employerHistoryData));
|
||||
this.$("#attachments_data_json").val(JSON.stringify(attachments));
|
||||
|
||||
let formElement = this.$el.is("form") ? this.$el[0] : this.$el.find("form")[0];
|
||||
if (formElement) {
|
||||
formElement.submit();
|
||||
} else {
|
||||
console.error("Form element not found.");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Error reading files:", error);
|
||||
this.isSubmitting = false;
|
||||
this.$("#submit-btn").prop("disabled", false);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Enforce required fields when any field in a row is filled
|
||||
enforceRowValidation() {
|
||||
// Education Details Validation - Make fields required when typing
|
||||
this.$("#education_details_data").on("input", "tbody tr input", function () {
|
||||
let row = $(this).closest("tr");
|
||||
let inputs = row.find("input");
|
||||
|
||||
// Check if at least one field has a value
|
||||
let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
|
||||
|
||||
if (isFilled) {
|
||||
inputs.attr("required", true);
|
||||
} else {
|
||||
inputs.removeAttr("required"); // Remove required if all fields are empty
|
||||
}
|
||||
});
|
||||
|
||||
// Remove empty rows only when the user leaves the last input field
|
||||
this.$("#education_details_data").on("blur", "tbody tr input", function () {
|
||||
let row = $(this).closest("tr");
|
||||
let inputs = row.find("input");
|
||||
|
||||
if (row.hasClass("predefined-row")) {
|
||||
let isEmpty = inputs.toArray().every(input => $(input).val().trim() === "");
|
||||
if (isEmpty) {
|
||||
row.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Family Details Validation
|
||||
// this.$("#family_details_data").on("input", "tbody tr input", function () {
|
||||
// let row = $(this).closest("tr");
|
||||
// let inputs = row.find("input");
|
||||
//
|
||||
// let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
|
||||
//
|
||||
// if (isFilled) {
|
||||
// inputs.attr("required", true);
|
||||
// } else {
|
||||
// inputs.removeAttr("required");
|
||||
// }
|
||||
// });
|
||||
|
||||
// Remove row functionality (Manual delete using button)
|
||||
this.$("#education_details_data").on("click", ".remove-edu-row", function () {
|
||||
$(this).closest("tr").remove();
|
||||
});
|
||||
|
||||
this.$("#family_details_data").on("click", ".remove-family-row", function () {
|
||||
$(this).closest("tr").remove();
|
||||
});
|
||||
},
|
||||
|
||||
async _renderStateIds() {
|
||||
console.log("Fetching States...");
|
||||
|
||||
const country_id = $('#present_state_ids_container').data('country_id');
|
||||
|
||||
const state_ids = await rpc("/employee_bridge/fetch_related_state_ids", {
|
||||
country_id: country_id,
|
||||
});
|
||||
|
||||
const present_state_ids_container = $("#present_state_ids_container");
|
||||
const permanent_state_ids_container = $("#permanent_state_ids_container");
|
||||
|
||||
present_state_ids_container.empty();
|
||||
permanent_state_ids_container.empty();
|
||||
|
||||
console.log(state_ids);
|
||||
|
||||
if (typeof state_ids === 'object' && !Array.isArray(state_ids)) {
|
||||
const stateOptions = Object.entries(state_ids).map(([id, name]) => `
|
||||
<option value="${id}">${name}</option>
|
||||
`).join('');
|
||||
|
||||
const stateHtml = `
|
||||
<select name='permanent_state' id="permanent_state" class="form-control" required>
|
||||
<option value="" disabled selected>Select State</option>
|
||||
${stateOptions}
|
||||
</select>
|
||||
`;
|
||||
|
||||
const presentStateHtml = `
|
||||
<select name='present_state' id="present_state" class="form-control" required>
|
||||
<option value="" disabled selected>Select State</option>
|
||||
${stateOptions}
|
||||
</select>
|
||||
`;
|
||||
|
||||
permanent_state_ids_container.append(stateHtml);
|
||||
present_state_ids_container.append(presentStateHtml);
|
||||
} else {
|
||||
console.error("Expected an object like {id: name}, but got:", state_ids);
|
||||
}
|
||||
|
||||
console.log("Hello World");
|
||||
},
|
||||
|
||||
|
||||
|
||||
async start() {
|
||||
this._super(...arguments);
|
||||
this._renderStateIds();
|
||||
|
||||
// Ensure form submit event is properly bound
|
||||
// this.$el.on("submit", this.handleFormSubmit.bind(this));
|
||||
|
||||
// Bind validation enforcement
|
||||
this.enforceRowValidation();
|
||||
const selectedExperience = this.$("[name='experience']:checked").val();
|
||||
const selectedMarital = this.$("[name='marital']:checked").val();
|
||||
|
||||
const employerHistorySection = this.$("#employer_history_data");
|
||||
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
|
||||
|
||||
const family_details_data_spouse = this.$('#family_details_data_spouse')
|
||||
const family_details_data_kid1 = this.$('#family_details_data_kid1')
|
||||
const family_details_data_kid2 = this.$('#family_details_data_kid2')
|
||||
|
||||
|
||||
if (selectedExperience === "experienced") {
|
||||
employerHistorySection.show();
|
||||
} else {
|
||||
employerHistorySection.hide();
|
||||
}
|
||||
|
||||
if (selectedMarital === 'married') {
|
||||
marriageAnniversarySection.show();
|
||||
// Show rows for spouse, kid1, and kid2 if married
|
||||
family_details_data_spouse.show();
|
||||
family_details_data_kid1.show();
|
||||
family_details_data_kid2.show();
|
||||
} else {
|
||||
marriageAnniversarySection.hide();
|
||||
// Show rows for spouse, kid1, and kid2 if married
|
||||
family_details_data_spouse.hide();
|
||||
family_details_data_kid1.hide();
|
||||
family_details_data_kid2.hide();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="hr_employee_view_form_bridge" model="ir.ui.view">
|
||||
<field name="name">hr.employee.view.form.bridge</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_recruitment_bridge"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-link"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not recruitment_bridge_count">
|
||||
<field name="recruitment_bridge_count" widget="statinfo" string="Onboarding"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//label[@for='user_id']" position="before">
|
||||
<button string="Send JOD Form"
|
||||
name="send_jod_form_to_employee"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not work_email"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_bridge_requested_attachment_list" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.requested.attachment.list</field>
|
||||
<field name="model">employee.bridge.requested.attachment</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="attachment_type"/>
|
||||
<field name="is_default"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_bridge_requested_attachment_form" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.requested.attachment.form</field>
|
||||
<field name="model">employee.bridge.requested.attachment</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="attachment_type"/>
|
||||
<field name="is_default"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_employee_bridge_requested_attachment" model="ir.actions.act_window">
|
||||
<field name="name">Onboarding Documents</field>
|
||||
<field name="res_model">employee.bridge.requested.attachment</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_employee_bridge_requested_attachment"
|
||||
name="Onboarding Documents"
|
||||
parent="menu_employee_bridge"
|
||||
action="action_employee_bridge_requested_attachment"
|
||||
sequence="20"
|
||||
groups="hr.group_hr_user"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_bridge_list" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.list</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="employee_name"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="work_email"/>
|
||||
<field name="joining_form_link" optional="show"/>
|
||||
<field name="source"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_bridge_form" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.form</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button string="Send JOD Form" name="action_send_jod_form" type="object" class="btn-primary" invisible="state == 'validated'"/>
|
||||
<button string="Create Employee" name="action_create_employee" type="object" class="btn-secondary" invisible="employee_id"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,jod_sent,jod_received,validated"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_open_employee" type="object" class="oe_stat_button" icon="fa-id-card-o" invisible="not employee_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="employee_code" readonly="1"/></span>
|
||||
<span class="o_stat_text">Employee</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group string="Onboarding">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="source"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="joining_form_link" widget="url"/>
|
||||
<field name="requested_attachment_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</group>
|
||||
<button string="Validate Into Employee" name="action_validate_employee_details" type="object" class="btn-primary" invisible="state == 'validated' or not employee_id"/>
|
||||
<notebook>
|
||||
<page string="Basic Details" name="basic_details">
|
||||
<group>
|
||||
<group>
|
||||
<field name="employee_name"/>
|
||||
<field name="candidate_image" widget="image" class="oe_avatar"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="work_email" widget="email"/>
|
||||
<field name="private_email" widget="email"/>
|
||||
<field name="work_phone" widget="phone"/>
|
||||
<field name="mobile_phone" widget="phone"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="emp_type"/>
|
||||
<field name="total_exp"/>
|
||||
<field name="doj"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="JOD Details" name="jod_details">
|
||||
<group string="Personal Details">
|
||||
<group>
|
||||
<field name="gender"/>
|
||||
<field name="birthday"/>
|
||||
<field name="blood_group"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="marital"/>
|
||||
<field name="marriage_anniversary_date" invisible="marital == 'single'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Current Address">
|
||||
<group>
|
||||
<field name="private_street"/>
|
||||
<field name="private_street2"/>
|
||||
<field name="private_city"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="private_state_id"/>
|
||||
<field name="private_zip"/>
|
||||
<field name="private_country_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Permanent Address">
|
||||
<group>
|
||||
<field name="permanent_street"/>
|
||||
<field name="permanent_street2"/>
|
||||
<field name="permanent_city"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="permanent_state_id"/>
|
||||
<field name="permanent_zip"/>
|
||||
<field name="permanent_country_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Authentication">
|
||||
<group>
|
||||
<field name="pan_no"/>
|
||||
<field name="identification_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="previous_company_pf_no"/>
|
||||
<field name="previous_company_uan_no"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Passport">
|
||||
<group>
|
||||
<field name="passport_no"/>
|
||||
<field name="passport_start_date"/>
|
||||
<field name="passport_end_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="passport_issued_location"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Family Details" name="family_details">
|
||||
<field name="family_details" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="relation_type"/>
|
||||
<field name="name"/>
|
||||
<field name="contact_no"/>
|
||||
<field name="dob"/>
|
||||
<field name="location"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Employer History" name="employer_history">
|
||||
<field name="employer_history" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="company_name"/>
|
||||
<field name="designation"/>
|
||||
<field name="date_of_joining"/>
|
||||
<field name="last_working_day"/>
|
||||
<field name="ctc"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Education Details" name="education_history">
|
||||
<field name="education_history" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="education_type"/>
|
||||
<field name="name"/>
|
||||
<field name="university"/>
|
||||
<field name="start_year"/>
|
||||
<field name="end_year"/>
|
||||
<field name="marks_or_grade"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Attachments" name="attachments">
|
||||
<field name="joining_attachment_ids" nolabel="1">
|
||||
<list editable="bottom" default_group_by="requested_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
|
||||
<field name="requested_attachment_id"/>
|
||||
<field name="name"/>
|
||||
<field name="attachment_type"/>
|
||||
<field name="file" widget="binary" filename="file_name"/>
|
||||
<field name="file_name" optional="hide"/>
|
||||
<button name="action_preview_file" type="object" string="Preview" class="oe_highlight" icon="fa-eye"/>
|
||||
<field name="review_status"/>
|
||||
<field name="review_comments" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_bridge_search" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.search</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="employee_id"/>
|
||||
<filter name="jod_sent" string="JOD Sent" domain="[('state', '=', 'jod_sent')]"/>
|
||||
<filter name="jod_received" string="JOD Received" domain="[('state', '=', 'jod_received')]"/>
|
||||
<filter name="validated" string="Validated" domain="[('state', '=', 'validated')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
|
||||
<filter name="group_source" string="Source" context="{'group_by': 'source'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_employee_bridge" model="ir.actions.act_window">
|
||||
<field name="name">Onboarding</field>
|
||||
<field name="res_model">recruitment.employee.bridge</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_employee_bridge"
|
||||
name="Onboarding"
|
||||
parent="hr.menu_hr_root"
|
||||
sequence="90"
|
||||
groups="hr.group_hr_user"/>
|
||||
|
||||
<menuitem id="menu_employee_bridge_onboarding"
|
||||
name="Onboarding"
|
||||
parent="menu_employee_bridge"
|
||||
action="action_employee_bridge"
|
||||
sequence="10"
|
||||
groups="hr.group_hr_user"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import employee_bridge_attachment_wizard
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EmployeeBridgeAttachmentWizard(models.TransientModel):
|
||||
_name = "employee.bridge.attachment.wizard"
|
||||
_description = "Onboarding Attachment Request Wizard"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, readonly=True)
|
||||
req_attachment_ids = fields.Many2many(
|
||||
comodel_name="employee.bridge.requested.attachment",
|
||||
relation="employee_bridge_req_attachment_rel", # relation table name
|
||||
column1="request_id", # column referring to this model
|
||||
column2="attachment_id", # column referring to the comodel
|
||||
string="Attachments to Request",
|
||||
)
|
||||
attachment_ids = fields.Many2many("ir.attachment")
|
||||
template_id = fields.Many2one(
|
||||
"mail.template",
|
||||
default=lambda self: self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False),
|
||||
readonly=True,
|
||||
)
|
||||
email_from = fields.Char("Email From")
|
||||
email_to = fields.Char("Email To")
|
||||
email_cc = fields.Text("Email CC")
|
||||
email_subject = fields.Char()
|
||||
email_body = fields.Html(
|
||||
"Body",
|
||||
render_engine="qweb",
|
||||
render_options={"post_process": True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize="email_outgoing",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
bridge = self.env["recruitment.employee.bridge"].browse(
|
||||
self.env.context.get("default_bridge_id") or self.env.context.get("bridge_id")
|
||||
)
|
||||
if bridge:
|
||||
attachments = bridge.requested_attachment_ids or self.env["employee.bridge.requested.attachment"].search([
|
||||
("is_default", "=", True),
|
||||
])
|
||||
defaults.update({
|
||||
"bridge_id": bridge.id,
|
||||
"req_attachment_ids": [(6, 0, attachments.ids)],
|
||||
"email_from": self.env.company.email or self.env.user.email,
|
||||
"email_to": bridge.work_email or bridge.private_email or bridge.employee_id.work_email,
|
||||
})
|
||||
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||
if template:
|
||||
context = {
|
||||
"joining_form_link": bridge._get_joining_form_link(),
|
||||
**bridge._group_requested_attachments(attachments),
|
||||
}
|
||||
defaults.update({
|
||||
"template_id": template.id,
|
||||
"email_subject": template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id],
|
||||
"email_body": template.with_context(**context)._render_field(
|
||||
"body_html", [bridge.id], compute_lang=True
|
||||
)[bridge.id],
|
||||
})
|
||||
return defaults
|
||||
|
||||
@api.onchange("req_attachment_ids")
|
||||
def _onchange_req_attachment_ids(self):
|
||||
if not self.bridge_id or not self.template_id:
|
||||
return
|
||||
context = {
|
||||
"joining_form_link": self.bridge_id._get_joining_form_link(),
|
||||
**self.bridge_id._group_requested_attachments(self.req_attachment_ids),
|
||||
}
|
||||
self.email_body = self.template_id.with_context(**context)._render_field(
|
||||
"body_html", [self.bridge_id.id], compute_lang=True
|
||||
)[self.bridge_id.id]
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
self.bridge_id.requested_attachment_ids = [(6, 0, self.req_attachment_ids.ids)]
|
||||
self.bridge_id._send_jod_mail(
|
||||
req_attachment_ids=self.req_attachment_ids,
|
||||
email_values={
|
||||
"email_from": self.email_from,
|
||||
"email_to": self.email_to,
|
||||
"email_cc": self.email_cc,
|
||||
"subject": self.email_subject,
|
||||
"attachment_ids": [(6, 0, self.attachment_ids.ids)],
|
||||
},
|
||||
email_body=self.email_body,
|
||||
)
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_bridge_attachment_wizard_form" model="ir.ui.view">
|
||||
<field name="name">employee.bridge.attachment.wizard.form</field>
|
||||
<field name="model">employee.bridge.attachment.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Select Attachments">
|
||||
<group>
|
||||
<field name="bridge_id" invisible="1"/>
|
||||
<field name="req_attachment_ids" widget="many2many_tags" force_save="1"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="attachment" string="Attachments (Binary)">
|
||||
<field name="attachment_ids" widget="many2many_binary" domain="[('mimetype', 'not ilike', 'image')]"/>
|
||||
</page>
|
||||
<page name="email" string="Email">
|
||||
<group>
|
||||
<field name="template_id" options="{'no_create': True}" readonly="1" force_save="1"/>
|
||||
<field name="email_from" force_save="1"/>
|
||||
<field name="email_to" force_save="1"/>
|
||||
<field name="email_cc"/>
|
||||
<field name="email_subject" force_save="1"/>
|
||||
<field name="email_body" widget="html_mail" class="oe-bordered-editor" options="{'codeview': true}" force_save="1"/>
|
||||
<field name="attachment_ids" widget="many2many_tags" string="Attachments" force_save="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object" string="Send JOD Form" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -10,12 +10,10 @@
|
|||
""",
|
||||
'author': 'FTPROTECH',
|
||||
'website': 'https://ftprotech.in',
|
||||
'depends': ['hr', 'mail','hr_employee_extended','hr_recruitment_extended'],
|
||||
'data': [
|
||||
'data/data.xml',
|
||||
'data/actions.xml',
|
||||
'data/template.xml',
|
||||
'views/emp_jod.xml',
|
||||
],
|
||||
'depends': ['hr', 'mail', 'hr_employee_extended', 'employee_bridge'],
|
||||
'data': [
|
||||
'data/actions.xml',
|
||||
'data/template.xml',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +1,22 @@
|
|||
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:
|
||||
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')
|
||||
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)}"
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools import misc
|
||||
|
||||
|
||||
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()
|
||||
|
||||
template = request.env.ref("employee_jod.emp_joining_form_template", raise_if_not_found=False)
|
||||
if not template:
|
||||
return "Error: Template not found"
|
||||
|
||||
return request.env["ir.qweb"]._render(template.id, {
|
||||
"docs": employee,
|
||||
"doc": employee,
|
||||
"time": misc.datetime,
|
||||
"user": request.env.user,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@
|
|||
<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>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Original job record -->
|
||||
<record model="hr.job" id="employee_jod_internal_job_id">
|
||||
<field name="name">Internal Job</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Recruitment stage -->
|
||||
<record model="hr.recruitment.stage" id="hired_stage8">
|
||||
<field name="name">IJ</field>
|
||||
<field name="is_default_field" eval="False"/>
|
||||
<field name="hired_stage" eval="True"/>
|
||||
<!-- <field name="post_onboarding_form" eval="True"/>-->
|
||||
</record>
|
||||
|
||||
<!-- Changed ID for the recruitment record -->
|
||||
<record model="hr.job.recruitment" id="employee_jod_internal_job_recruitment_id">
|
||||
<field name="recruitment_sequence">IJ001</field>
|
||||
<field name="job_id" ref="employee_jod_internal_job_id"/>
|
||||
<field name="recruitment_stage_ids" eval="[(4, ref('hired_stage8'))]"/>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -313,13 +313,4 @@
|
|||
</t>
|
||||
</template>
|
||||
|
||||
<!-- <template id="thank_you_template_inherit" inherit_id="hr_recruitment_exteded.thank_you_template" name="Thank You Template Extended">-->
|
||||
<!-- <xpath expr="//div[@class='container mt-5 text-center']" position="inside">-->
|
||||
<!-- <div t-if="applicant.post_onboarding_form_status == 'done'" style="margin-top: 20px;">-->
|
||||
<!-- <a t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>-->
|
||||
<!-- </div>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- </template>-->
|
||||
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,165 +1,8 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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')
|
||||
from odoo import models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
return super().send_jod_form_to_employee()
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="hr_employee_view_form_applicant_id" model="ir.ui.view">
|
||||
<field name="name">hr.employee.view.form.applicant.id</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//label[@for='user_id']" position="before">
|
||||
<field name="applicant_id" string="Application ID" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_post_onboarding_attachment_wizard_form_inherit" model="ir.ui.view">
|
||||
<field name="name">post.onboarding.attachment.wizard.form.inherit</field>
|
||||
<field name="model">post.onboarding.attachment.wizard</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.view_post_onboarding_attachment_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='req_attachment_ids']" position="after">
|
||||
<field name="send_mail"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="attributes">
|
||||
<attribute name="invisible">not send_mail</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_confirm']" position="attributes">
|
||||
<attribute name="string">Send</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="hr_applicant_view_form_inherit_extend" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.extend</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='candidate_id']" position="after">
|
||||
<field name="joining_form_link" force_save="1" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AttendanceAnalytics(models.Model):
|
||||
|
|
@ -64,6 +65,8 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
('absent', 'Absent'),
|
||||
|
||||
('invalid_attendance', 'Invalid Attendance'),
|
||||
|
||||
('half_day', 'Half Day'),
|
||||
|
||||
('late_in', 'Late In'),
|
||||
|
|
@ -192,7 +195,7 @@ class AttendanceAnalytics(models.Model):
|
|||
for rec in self:
|
||||
if rec.status == 'present':
|
||||
rec.color = 10
|
||||
elif rec.status == 'absent':
|
||||
elif rec.status in ('absent', 'invalid_attendance'):
|
||||
rec.color = 1
|
||||
elif rec.status == 'half_day':
|
||||
rec.color = 2
|
||||
|
|
@ -280,6 +283,11 @@ class AttendanceAnalytics(models.Model):
|
|||
def action_create_shiftswap_request(self):
|
||||
self.ensure_one()
|
||||
|
||||
if self.date and self.date < fields.Date.context_today(self):
|
||||
raise UserError(
|
||||
_("Shift Swap cannot be requested for past dates.")
|
||||
)
|
||||
|
||||
request = self.env['shift.swap.request'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('roster_date', '=', self.date),
|
||||
|
|
@ -341,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)
|
||||
|
|
@ -350,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
|
||||
|
||||
|
|
@ -359,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'
|
||||
)
|
||||
|
||||
),
|
||||
|
||||
|
|
@ -432,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
|
||||
|
|
@ -450,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
|
||||
|
||||
|
|
@ -474,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)
|
||||
)
|
||||
|
|
@ -527,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
|
||||
|
|
@ -551,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
|
||||
|
|
@ -591,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
|
||||
|
|
@ -624,7 +654,7 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
+
|
||||
|
|
@ -640,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
|
||||
|
|
@ -671,7 +693,7 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
END
|
||||
|
||||
) AS required_checkout_time,
|
||||
) / 60.0 AS required_checkout_time,
|
||||
|
||||
CASE
|
||||
|
||||
|
|
@ -683,7 +705,7 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
+
|
||||
|
|
@ -699,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
|
||||
|
|
@ -739,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
|
||||
)
|
||||
),
|
||||
|
||||
|
|
@ -767,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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -782,7 +795,7 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
+
|
||||
|
|
@ -798,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
|
||||
|
|
@ -842,7 +846,7 @@ class AttendanceAnalytics(models.Model):
|
|||
WHEN
|
||||
(
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
+
|
||||
|
|
@ -858,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
|
||||
|
|
@ -897,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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -932,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'
|
||||
|
|
@ -970,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'
|
||||
|
|
@ -1026,6 +1016,36 @@ class AttendanceAnalytics(models.Model):
|
|||
LEFT JOIN resource_calendar rc
|
||||
ON rc.id = ed.resource_calendar_id
|
||||
|
||||
LEFT JOIN calendar_day_schedule cds
|
||||
ON cds.calendar_id = ed.resource_calendar_id
|
||||
AND cds.dayofweek = (
|
||||
(
|
||||
(
|
||||
EXTRACT(DOW FROM ed.date)::integer
|
||||
+ 6
|
||||
) % 7
|
||||
)::text
|
||||
)
|
||||
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COALESCE(
|
||||
NULLIF(rc.shift_start_time * 60, 0),
|
||||
cds.shift_start_minutes,
|
||||
0
|
||||
) AS shift_start_minutes,
|
||||
COALESCE(
|
||||
NULLIF(rc.shift_end_time * 60, 0),
|
||||
cds.shift_end_minutes,
|
||||
0
|
||||
) AS shift_end_minutes,
|
||||
COALESCE(
|
||||
NULLIF(rc.hours_per_day, 0),
|
||||
cds.hours_per_day,
|
||||
0
|
||||
) AS hours_per_day
|
||||
) es ON TRUE
|
||||
|
||||
LEFT JOIN department_grace dg
|
||||
ON dg.calendar_id
|
||||
= ed.resource_calendar_id
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
<field name="name">attendance.analytics.list</field>
|
||||
<field name="model">attendance.analytics</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Attendance Analytics">
|
||||
<list string="Attendance Analytics"
|
||||
decoration-danger="status == 'absent' or status == 'invalid_attendance'">
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="date"/>
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
<field name="status"
|
||||
widget="badge"
|
||||
decoration-success="status == 'present'"
|
||||
decoration-danger="status == 'absent'"
|
||||
decoration-danger="status == 'absent' or status == 'invalid_attendance'"
|
||||
decoration-warning="status == 'late_in'"
|
||||
decoration-info="status == 'half_day'"
|
||||
decoration-primary="status == 'holiday'"/>
|
||||
|
|
@ -146,4 +147,4 @@
|
|||
action="action_attendance_analytics"
|
||||
sequence="-51"/>
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -152,7 +152,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
parsed_data = self._post_process_jd_data(parsed_data, parsed_payload["text"])
|
||||
else:
|
||||
parsed_data = self._post_process_resume_data(parsed_data, parsed_payload["text"], line.file_name)
|
||||
line.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
||||
line.write(dict(
|
||||
line._prepare_editable_vals(parsed_data),
|
||||
extracted_payload=json.dumps(parsed_data, indent=2, ensure_ascii=False),
|
||||
))
|
||||
|
||||
try:
|
||||
processed += 1
|
||||
|
|
@ -255,6 +258,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
|
||||
try:
|
||||
parsed_data = json.loads(line.extracted_payload)
|
||||
parsed_data = line._get_edited_parsed_data(parsed_data)
|
||||
|
||||
with self.env.cr.savepoint():
|
||||
|
||||
|
|
@ -480,6 +484,17 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"relation_type (father/mother/spouse/kid1/kid2), name, contact_no, dob, and location."
|
||||
),
|
||||
},
|
||||
"certifications": {
|
||||
"type": "list",
|
||||
"description": "List of explicit certifications, courses, training, or licenses if present.",
|
||||
},
|
||||
"projects": {
|
||||
"type": "list",
|
||||
"description": (
|
||||
"List of explicit resume projects if present. Each item should include project_name, role, "
|
||||
"technologies, duration, and description when available."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def _get_jd_required_fields(self):
|
||||
|
|
@ -510,6 +525,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"Normalize skills into clean individual names. "
|
||||
"For experience values, return numeric years when clearly inferable. "
|
||||
"If the resume contains education details, previous employer details, or family details, return them as structured arrays of objects using the requested field names. "
|
||||
"Read the complete resume including tables, side columns, and later pages before returning JSON. "
|
||||
"education_history must include every school, intermediate, diploma, graduation, post graduation, or additional qualification row that is explicitly present. "
|
||||
"employer_history must include internships, current employer, previous employers, project/company experience, and work periods when explicitly present. "
|
||||
"family_details must include every explicitly mentioned family member and must be [] if the resume has no family section. "
|
||||
"For each employer entry, extract the role-specific work description into work_description. "
|
||||
"Only include entries that are explicitly present in the document. "
|
||||
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
|
||||
|
|
@ -642,10 +661,13 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
data["email"] = email_match.group(1)
|
||||
|
||||
phone_matches = re.findall(r"(\+?\d[\d\-\s()]{7,}\d)", extracted_text)
|
||||
if phone_matches and not data.get("phone"):
|
||||
data["phone"] = phone_matches[0].strip()
|
||||
if len(phone_matches) > 1 and not data.get("alternate_phone"):
|
||||
data["alternate_phone"] = phone_matches[1].strip()
|
||||
phone_values = []
|
||||
for phone_candidate in [data.get("phone"), data.get("alternate_phone")] + phone_matches:
|
||||
phone_value = self._clean_resume_phone_value(phone_candidate)
|
||||
if phone_value and phone_value not in phone_values:
|
||||
phone_values.append(phone_value)
|
||||
data["phone"] = phone_values[0] if phone_values else False
|
||||
data["alternate_phone"] = phone_values[1] if len(phone_values) > 1 else False
|
||||
|
||||
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", extracted_text, re.I)
|
||||
if linkedin_match:
|
||||
|
|
@ -654,11 +676,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
if not data.get("total_experience_years"):
|
||||
data["total_experience_years"] = self._guess_total_experience(extracted_text)
|
||||
|
||||
resume_skills = self._normalize_resume_skill_values(data.get("skills") or [], extracted_text)
|
||||
data["skills"] = self.env[
|
||||
"document.parser.service"
|
||||
].validate_explicit_skills(
|
||||
extracted_text,
|
||||
data.get("skills") or []
|
||||
resume_skills
|
||||
)
|
||||
data["education_history"] = self._normalize_resume_list(data.get("education_history"))
|
||||
data["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
|
||||
|
|
@ -1431,8 +1454,57 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
normalized = re.sub(r"[^\d+]", "", value)
|
||||
return normalized or False
|
||||
|
||||
def _clean_resume_phone_value(self, value):
|
||||
if not value:
|
||||
return False
|
||||
raw_value = str(value).strip()
|
||||
if re.fullmatch(r"(?:19|20)\d{2}\s*[-/]\s*(?:19|20)\d{2}", raw_value):
|
||||
return False
|
||||
normalized = self._normalize_phone(raw_value)
|
||||
digits = re.sub(r"\D", "", normalized or "")
|
||||
if len(digits) < 10 or len(digits) > 15:
|
||||
return False
|
||||
return normalized
|
||||
|
||||
def _normalize_resume_skill_values(self, skills, extracted_text):
|
||||
skill_parts = []
|
||||
for skill in skills:
|
||||
for skill_part in self._split_resume_skill_value(skill):
|
||||
normalized = self._normalize_skill_name(skill_part)
|
||||
if normalized and self._is_valid_resume_skill(normalized, extracted_text):
|
||||
skill_parts.append(normalized)
|
||||
normalized_skills = []
|
||||
index = 0
|
||||
while index < len(skill_parts):
|
||||
current_skill = skill_parts[index]
|
||||
next_skill = skill_parts[index + 1] if index + 1 < len(skill_parts) else False
|
||||
combined_skill = "%s %s" % (current_skill, next_skill) if next_skill else False
|
||||
if combined_skill and self._is_valid_resume_skill(combined_skill, extracted_text):
|
||||
normalized_skills.append(combined_skill)
|
||||
index += 2
|
||||
continue
|
||||
normalized_skills.append(current_skill)
|
||||
index += 1
|
||||
return self._deduplicate_skill_names(normalized_skills)
|
||||
|
||||
def _split_resume_skill_value(self, value):
|
||||
value = re.sub(r"^[\s\-*\u2022•]+", "", str(value or "")).strip()
|
||||
if not value:
|
||||
return []
|
||||
if ":" in value:
|
||||
label, value = [item.strip() for item in value.split(":", 1)]
|
||||
if not re.search(r"(programming|frameworks?|databases?|analytics?|cloud|devops|tools?|technologies|skills?)", label, re.I):
|
||||
value = "%s %s" % (label, value)
|
||||
value = re.sub(r"\((?:advanced|intermediate|beginner|expert)\)", "", value, flags=re.I)
|
||||
return [item for item in re.split(r"[,;|/]", value) if item.strip()]
|
||||
|
||||
def _normalize_skill_name(self, value):
|
||||
value = re.sub(r"\s+", " ", (value or "")).strip(" -,:;")
|
||||
value = re.sub(r"^[\s\-*\u2022•]+", "", value or "")
|
||||
value = re.sub(r"\s+", " ", value).strip(" -,:;()")
|
||||
if ":" in value:
|
||||
label, skill_value = [item.strip() for item in value.split(":", 1)]
|
||||
if re.search(r"(programming|frameworks?|databases?|analytics?|cloud|devops|tools?|technologies|skills?)", label, re.I):
|
||||
value = skill_value
|
||||
value = re.sub(r"^[0-9.)\-(\s]+", "", value).strip()
|
||||
if not value:
|
||||
return False
|
||||
|
|
@ -2034,13 +2106,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"<ul>",
|
||||
]
|
||||
for row in rows:
|
||||
html_parts.append(
|
||||
"<li><strong>%s</strong>: <span class='text-%s'>%s</span></li>" % (
|
||||
escape(row["filename"]),
|
||||
row["level"],
|
||||
escape(row["message"]),
|
||||
)
|
||||
)
|
||||
html_parts.append("<li><strong>%s</strong>: <span class='text-%s'>%s</span>" % (
|
||||
escape(row["filename"]),
|
||||
row["level"],
|
||||
escape(row["message"]),
|
||||
))
|
||||
html_parts.append("</li>")
|
||||
html_parts.extend(["</ul>", "</div>"])
|
||||
return "".join(html_parts)
|
||||
|
||||
|
|
@ -2072,3 +2143,539 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
|
|||
extracted_payload = fields.Text(readonly=True)
|
||||
candidate_id = fields.Many2one("hr.candidate", readonly=True)
|
||||
applicant_id = fields.Many2one("hr.applicant", readonly=True)
|
||||
target_model = fields.Selection(related="wizard_id.target_model", readonly=True)
|
||||
|
||||
full_name = fields.Char()
|
||||
first_name = fields.Char()
|
||||
last_name = fields.Char()
|
||||
email = fields.Char()
|
||||
phone = fields.Char()
|
||||
alternate_phone = fields.Char()
|
||||
linkedin_profile = fields.Char()
|
||||
current_location = fields.Char()
|
||||
current_organization = fields.Char()
|
||||
total_experience_years = fields.Float()
|
||||
relevant_experience_years = fields.Float()
|
||||
notice_period = fields.Char()
|
||||
degree = fields.Char()
|
||||
skills_text = fields.Text(string="Skills")
|
||||
summary = fields.Text()
|
||||
education_history_json = fields.Text(string="Education History")
|
||||
employer_history_json = fields.Text(string="Employer History")
|
||||
family_details_json = fields.Text(string="Family Details")
|
||||
other_payload_json = fields.Text(string="Other Parsed Data")
|
||||
education_line_ids = fields.One2many(
|
||||
"hr.recruitment.auto.doc.education.line",
|
||||
"wizard_line_id",
|
||||
string="Education History",
|
||||
)
|
||||
employer_line_ids = fields.One2many(
|
||||
"hr.recruitment.auto.doc.employer.line",
|
||||
"wizard_line_id",
|
||||
string="Employer History",
|
||||
)
|
||||
family_line_ids = fields.One2many(
|
||||
"hr.recruitment.auto.doc.family.line",
|
||||
"wizard_line_id",
|
||||
string="Family Details",
|
||||
)
|
||||
certification_line_ids = fields.One2many(
|
||||
"hr.recruitment.auto.doc.certification.line",
|
||||
"wizard_line_id",
|
||||
string="Certifications",
|
||||
)
|
||||
project_line_ids = fields.One2many(
|
||||
"hr.recruitment.auto.doc.project.line",
|
||||
"wizard_line_id",
|
||||
string="Projects",
|
||||
)
|
||||
other_line_ids = fields.One2many(
|
||||
"hr.recruitment.auto.doc.other.line",
|
||||
"wizard_line_id",
|
||||
string="Other Parsed Data",
|
||||
)
|
||||
|
||||
request_id = fields.Char(string="Request ID")
|
||||
start_date = fields.Char()
|
||||
end_date = fields.Char()
|
||||
site_location = fields.Char()
|
||||
job_title = fields.Char()
|
||||
job_summary = fields.Text()
|
||||
requirements = fields.Text()
|
||||
primary_skills_text = fields.Text(string="Primary Skills")
|
||||
secondary_skills_text = fields.Text(string="Secondary Skills")
|
||||
budget = fields.Char()
|
||||
experience_years = fields.Float()
|
||||
job_category = fields.Char()
|
||||
|
||||
def action_save_line_changes(self):
|
||||
self.ensure_one()
|
||||
parsed_data = self._get_edited_parsed_data()
|
||||
self.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
||||
return self._action_open_parent_wizard()
|
||||
|
||||
def action_close_line(self):
|
||||
self.ensure_one()
|
||||
return self._action_open_parent_wizard()
|
||||
|
||||
def _action_open_parent_wizard(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": self.wizard_id._name,
|
||||
"res_id": self.wizard_id.id,
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
def _prepare_editable_vals(self, parsed_data):
|
||||
parsed_data = parsed_data or {}
|
||||
return {
|
||||
"full_name": parsed_data.get("full_name"),
|
||||
"first_name": parsed_data.get("first_name"),
|
||||
"last_name": parsed_data.get("last_name"),
|
||||
"email": parsed_data.get("email"),
|
||||
"phone": parsed_data.get("phone"),
|
||||
"alternate_phone": parsed_data.get("alternate_phone"),
|
||||
"linkedin_profile": parsed_data.get("linkedin_profile"),
|
||||
"current_location": parsed_data.get("current_location"),
|
||||
"current_organization": parsed_data.get("current_organization"),
|
||||
"total_experience_years": self._float_or_zero(parsed_data.get("total_experience_years")),
|
||||
"relevant_experience_years": self._float_or_zero(parsed_data.get("relevant_experience_years")),
|
||||
"notice_period": parsed_data.get("notice_period"),
|
||||
"degree": parsed_data.get("degree"),
|
||||
"skills_text": self._list_to_text(parsed_data.get("skills")),
|
||||
"summary": parsed_data.get("summary"),
|
||||
"education_history_json": self._json_to_text(parsed_data.get("education_history")),
|
||||
"employer_history_json": self._json_to_text(parsed_data.get("employer_history")),
|
||||
"family_details_json": self._json_to_text(parsed_data.get("family_details")),
|
||||
"other_payload_json": self._json_to_text(self._get_other_parsed_data(parsed_data)),
|
||||
"education_line_ids": self._prepare_education_line_commands(parsed_data.get("education_history")),
|
||||
"employer_line_ids": self._prepare_employer_line_commands(parsed_data.get("employer_history"), parsed_data.get("projects")),
|
||||
"family_line_ids": self._prepare_family_line_commands(parsed_data.get("family_details")),
|
||||
"certification_line_ids": self._prepare_certification_line_commands(parsed_data.get("certifications")),
|
||||
"project_line_ids": self._prepare_project_line_commands(parsed_data.get("projects")),
|
||||
"other_line_ids": self._prepare_other_line_commands(self._get_other_parsed_data(parsed_data)),
|
||||
"request_id": parsed_data.get("request_id"),
|
||||
"start_date": parsed_data.get("start_date"),
|
||||
"end_date": parsed_data.get("end_date"),
|
||||
"site_location": parsed_data.get("site_location"),
|
||||
"job_title": parsed_data.get("job_title"),
|
||||
"job_summary": parsed_data.get("job_summary"),
|
||||
"requirements": parsed_data.get("requirements"),
|
||||
"primary_skills_text": self._list_to_text(parsed_data.get("primary_skills")),
|
||||
"secondary_skills_text": self._list_to_text(parsed_data.get("secondary_skills")),
|
||||
"budget": parsed_data.get("budget"),
|
||||
"experience_years": self._float_or_zero(parsed_data.get("experience_years")),
|
||||
"job_category": parsed_data.get("job_category"),
|
||||
}
|
||||
|
||||
def _get_edited_parsed_data(self, base_data=None):
|
||||
data = dict(base_data or {})
|
||||
if self.target_model == "job_recruitment":
|
||||
data.update({
|
||||
"request_id": self.request_id,
|
||||
"start_date": self.start_date,
|
||||
"end_date": self.end_date,
|
||||
"site_location": self.site_location,
|
||||
"job_title": self.job_title,
|
||||
"job_summary": self.job_summary,
|
||||
"requirements": self.requirements,
|
||||
"primary_skills": self._text_to_list(self.primary_skills_text),
|
||||
"secondary_skills": self._text_to_list(self.secondary_skills_text),
|
||||
"budget": self.budget,
|
||||
"experience_years": self.experience_years,
|
||||
"job_category": self.job_category,
|
||||
})
|
||||
else:
|
||||
data.update({
|
||||
"full_name": self.full_name,
|
||||
"first_name": self.first_name,
|
||||
"last_name": self.last_name,
|
||||
"email": self.email,
|
||||
"phone": self.phone,
|
||||
"alternate_phone": self.alternate_phone,
|
||||
"linkedin_profile": self.linkedin_profile,
|
||||
"current_location": self.current_location,
|
||||
"current_organization": self.current_organization,
|
||||
"total_experience_years": self.total_experience_years,
|
||||
"relevant_experience_years": self.relevant_experience_years,
|
||||
"notice_period": self.notice_period,
|
||||
"degree": self.degree,
|
||||
"skills": self._text_to_list(self.skills_text),
|
||||
"summary": self.summary,
|
||||
"education_history": self._get_education_history_from_lines(),
|
||||
"employer_history": self._get_employer_history_from_lines(),
|
||||
"family_details": self._get_family_details_from_lines(),
|
||||
"certifications": self._get_certifications_from_lines(),
|
||||
"projects": self._get_projects_from_lines(),
|
||||
})
|
||||
data.update(self._get_other_payload_from_lines())
|
||||
return data
|
||||
|
||||
def _prepare_education_line_commands(self, education_history):
|
||||
commands = [(5, 0, 0)]
|
||||
for education in self._normalize_dict_list(education_history):
|
||||
commands.append((0, 0, {
|
||||
"education_type": education.get("education_type"),
|
||||
"specialization": education.get("specialization") or education.get("name") or education.get("degree"),
|
||||
"university": education.get("university") or education.get("institution") or education.get("college"),
|
||||
"start_year": education.get("start_year"),
|
||||
"end_year": education.get("end_year"),
|
||||
"marks_or_grade": education.get("marks_or_grade") or education.get("marks") or education.get("grade") or education.get("cgpa") or education.get("percentage"),
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _prepare_employer_line_commands(self, employer_history, projects=None):
|
||||
commands = [(5, 0, 0)]
|
||||
employers = self._normalize_shared_employer_descriptions(
|
||||
self._normalize_dict_list(employer_history),
|
||||
projects,
|
||||
)
|
||||
for employer in employers:
|
||||
commands.append((0, 0, {
|
||||
"company_name": employer.get("company_name") or employer.get("employer"),
|
||||
"designation": employer.get("designation") or employer.get("role") or employer.get("job_title"),
|
||||
"date_of_joining": employer.get("date_of_joining") or employer.get("start_date"),
|
||||
"last_working_day": employer.get("last_working_day") or employer.get("end_date"),
|
||||
"ctc": employer.get("ctc") or employer.get("salary"),
|
||||
"work_description": employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities"),
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _prepare_family_line_commands(self, family_details):
|
||||
commands = [(5, 0, 0)]
|
||||
for member in self._normalize_dict_list(family_details):
|
||||
commands.append((0, 0, {
|
||||
"relation_type": member.get("relation_type") or member.get("relation"),
|
||||
"name": member.get("name"),
|
||||
"contact_no": member.get("contact_no") or member.get("contact") or member.get("phone"),
|
||||
"dob": member.get("dob"),
|
||||
"location": member.get("location") or member.get("address"),
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _prepare_certification_line_commands(self, certifications):
|
||||
commands = [(5, 0, 0)]
|
||||
for certification in self._normalize_value_list(certifications):
|
||||
if isinstance(certification, dict):
|
||||
commands.append((0, 0, {
|
||||
"certification_name": certification.get("certification_name") or certification.get("name") or certification.get("title"),
|
||||
"provider": certification.get("provider") or certification.get("issuer") or certification.get("organization"),
|
||||
"completion_date": certification.get("completion_date") or certification.get("date") or certification.get("year"),
|
||||
}))
|
||||
else:
|
||||
commands.append((0, 0, {"certification_name": str(certification)}))
|
||||
return commands
|
||||
|
||||
def _prepare_project_line_commands(self, projects):
|
||||
commands = [(5, 0, 0)]
|
||||
for project in self._normalize_dict_list(projects):
|
||||
technologies = project.get("technologies")
|
||||
commands.append((0, 0, {
|
||||
"project_name": project.get("project_name") or project.get("name") or project.get("title"),
|
||||
"role": project.get("role"),
|
||||
"technologies": ", ".join(technologies) if isinstance(technologies, list) else technologies,
|
||||
"duration": project.get("duration"),
|
||||
"description": project.get("description") or project.get("summary") or project.get("work_description"),
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _prepare_other_line_commands(self, other_payload):
|
||||
commands = [(5, 0, 0)]
|
||||
for key, value in (other_payload or {}).items():
|
||||
commands.append((0, 0, {
|
||||
"key": key,
|
||||
"value": self._json_to_text(value) or str(value),
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _get_education_history_from_lines(self):
|
||||
return [line._to_payload() for line in self.education_line_ids if line._to_payload()]
|
||||
|
||||
def _get_employer_history_from_lines(self):
|
||||
return [line._to_payload() for line in self.employer_line_ids if line._to_payload()]
|
||||
|
||||
def _get_family_details_from_lines(self):
|
||||
return [line._to_payload() for line in self.family_line_ids if line._to_payload()]
|
||||
|
||||
def _get_certifications_from_lines(self):
|
||||
return [line._to_payload() for line in self.certification_line_ids if line._to_payload()]
|
||||
|
||||
def _get_projects_from_lines(self):
|
||||
return [line._to_payload() for line in self.project_line_ids if line._to_payload()]
|
||||
|
||||
def _get_other_payload_from_lines(self):
|
||||
return {
|
||||
line.key: line._json_value()
|
||||
for line in self.other_line_ids
|
||||
if line.key and line.value not in (False, None, "")
|
||||
}
|
||||
|
||||
def _normalize_dict_list(self, value):
|
||||
if isinstance(value, list):
|
||||
return [item for item in value if isinstance(item, dict)]
|
||||
if isinstance(value, dict):
|
||||
return [value]
|
||||
return []
|
||||
|
||||
def _normalize_value_list(self, value):
|
||||
if isinstance(value, list):
|
||||
return [item for item in value if item not in (False, None, "", {})]
|
||||
if value in (False, None, "", {}):
|
||||
return []
|
||||
return [value]
|
||||
|
||||
def _normalize_shared_employer_descriptions(self, employers, projects=None):
|
||||
if len(employers) < 2:
|
||||
return employers
|
||||
|
||||
descriptions = [
|
||||
employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")
|
||||
for employer in employers
|
||||
]
|
||||
descriptions = [description for description in descriptions if description]
|
||||
if descriptions:
|
||||
shared_description = max(descriptions, key=lambda item: len(str(item)))
|
||||
else:
|
||||
project_descriptions = []
|
||||
for project in self._normalize_dict_list(projects):
|
||||
description = project.get("description") or project.get("summary") or project.get("work_description")
|
||||
if description:
|
||||
project_descriptions.append("%s: %s" % (
|
||||
project.get("project_name") or project.get("name") or project.get("title") or "Project",
|
||||
description,
|
||||
))
|
||||
shared_description = "\n".join(project_descriptions) if project_descriptions else False
|
||||
|
||||
if not shared_description:
|
||||
return employers
|
||||
|
||||
for employer in employers:
|
||||
if not (employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")):
|
||||
employer["work_description"] = shared_description
|
||||
return employers
|
||||
|
||||
def _list_to_text(self, value):
|
||||
if not value:
|
||||
return False
|
||||
if isinstance(value, list):
|
||||
return "\n".join(str(item) for item in value if item not in (False, None, ""))
|
||||
return str(value)
|
||||
|
||||
def _text_to_list(self, value):
|
||||
if not value:
|
||||
return []
|
||||
return [item.strip(" -,\t") for item in str(value).splitlines() if item.strip(" -,\t")]
|
||||
|
||||
def _json_to_text(self, value):
|
||||
if value in (False, None, "", [], {}):
|
||||
return False
|
||||
return json.dumps(value, indent=2, ensure_ascii=False)
|
||||
|
||||
def _text_to_json_list(self, value):
|
||||
parsed = self._text_to_json_value(value, [])
|
||||
if isinstance(parsed, list):
|
||||
return [item for item in parsed if item not in (False, None, "", {})]
|
||||
if isinstance(parsed, dict):
|
||||
return [parsed]
|
||||
return []
|
||||
|
||||
def _text_to_json_dict(self, value):
|
||||
parsed = self._text_to_json_value(value, {})
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
|
||||
def _text_to_json_value(self, value, default):
|
||||
if not value:
|
||||
return default
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _get_other_parsed_data(self, parsed_data):
|
||||
known_keys = {
|
||||
"full_name", "first_name", "last_name", "email", "phone", "alternate_phone",
|
||||
"linkedin_profile", "current_location", "current_organization",
|
||||
"total_experience_years", "relevant_experience_years", "notice_period",
|
||||
"degree", "skills", "summary", "education_history", "employer_history",
|
||||
"family_details", "certifications", "projects", "request_id", "start_date", "end_date", "site_location",
|
||||
"job_title", "job_summary", "requirements", "primary_skills",
|
||||
"secondary_skills", "budget", "experience_years", "job_category",
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
for key, value in (parsed_data or {}).items()
|
||||
if key not in known_keys and value not in (False, None, "", [], {})
|
||||
}
|
||||
|
||||
def _float_or_zero(self, value):
|
||||
if value in (False, None, ""):
|
||||
return 0.0
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
class HrRecruitmentAutoDocEducationLine(models.TransientModel):
|
||||
_name = "hr.recruitment.auto.doc.education.line"
|
||||
_description = "HR Recruitment Auto Document Education Line"
|
||||
_order = "id"
|
||||
|
||||
wizard_line_id = fields.Many2one(
|
||||
"hr.recruitment.auto.doc.wizard.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
education_type = fields.Char()
|
||||
specialization = fields.Char()
|
||||
university = fields.Char()
|
||||
start_year = fields.Char()
|
||||
end_year = fields.Char()
|
||||
marks_or_grade = fields.Char()
|
||||
|
||||
def _to_payload(self):
|
||||
self.ensure_one()
|
||||
payload = {
|
||||
"education_type": self.education_type,
|
||||
"specialization": self.specialization,
|
||||
"university": self.university,
|
||||
"start_year": self.start_year,
|
||||
"end_year": self.end_year,
|
||||
"marks_or_grade": self.marks_or_grade,
|
||||
}
|
||||
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||
|
||||
|
||||
class HrRecruitmentAutoDocEmployerLine(models.TransientModel):
|
||||
_name = "hr.recruitment.auto.doc.employer.line"
|
||||
_description = "HR Recruitment Auto Document Employer Line"
|
||||
_order = "id"
|
||||
|
||||
wizard_line_id = fields.Many2one(
|
||||
"hr.recruitment.auto.doc.wizard.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
company_name = fields.Char()
|
||||
designation = fields.Char()
|
||||
date_of_joining = fields.Char()
|
||||
last_working_day = fields.Char()
|
||||
ctc = fields.Char()
|
||||
work_description = fields.Text()
|
||||
|
||||
def _to_payload(self):
|
||||
self.ensure_one()
|
||||
payload = {
|
||||
"company_name": self.company_name,
|
||||
"designation": self.designation,
|
||||
"date_of_joining": self.date_of_joining,
|
||||
"last_working_day": self.last_working_day,
|
||||
"ctc": self.ctc,
|
||||
"work_description": self.work_description,
|
||||
}
|
||||
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||
|
||||
|
||||
class HrRecruitmentAutoDocFamilyLine(models.TransientModel):
|
||||
_name = "hr.recruitment.auto.doc.family.line"
|
||||
_description = "HR Recruitment Auto Document Family Line"
|
||||
_order = "id"
|
||||
|
||||
wizard_line_id = fields.Many2one(
|
||||
"hr.recruitment.auto.doc.wizard.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
relation_type = fields.Char()
|
||||
name = fields.Char()
|
||||
contact_no = fields.Char()
|
||||
dob = fields.Char()
|
||||
location = fields.Char()
|
||||
|
||||
def _to_payload(self):
|
||||
self.ensure_one()
|
||||
payload = {
|
||||
"relation_type": self.relation_type,
|
||||
"name": self.name,
|
||||
"contact_no": self.contact_no,
|
||||
"dob": self.dob,
|
||||
"location": self.location,
|
||||
}
|
||||
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||
|
||||
|
||||
class HrRecruitmentAutoDocCertificationLine(models.TransientModel):
|
||||
_name = "hr.recruitment.auto.doc.certification.line"
|
||||
_description = "HR Recruitment Auto Document Certification Line"
|
||||
_order = "id"
|
||||
|
||||
wizard_line_id = fields.Many2one(
|
||||
"hr.recruitment.auto.doc.wizard.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
certification_name = fields.Char()
|
||||
provider = fields.Char()
|
||||
completion_date = fields.Char()
|
||||
|
||||
def _to_payload(self):
|
||||
self.ensure_one()
|
||||
payload = {
|
||||
"certification_name": self.certification_name,
|
||||
"provider": self.provider,
|
||||
"completion_date": self.completion_date,
|
||||
}
|
||||
return {key: value for key, value in payload.items() if value not in (False, None, "")}
|
||||
|
||||
|
||||
class HrRecruitmentAutoDocProjectLine(models.TransientModel):
|
||||
_name = "hr.recruitment.auto.doc.project.line"
|
||||
_description = "HR Recruitment Auto Document Project Line"
|
||||
_order = "id"
|
||||
|
||||
wizard_line_id = fields.Many2one(
|
||||
"hr.recruitment.auto.doc.wizard.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
project_name = fields.Char()
|
||||
role = fields.Char()
|
||||
technologies = fields.Char()
|
||||
duration = fields.Char()
|
||||
description = fields.Text()
|
||||
|
||||
def _to_payload(self):
|
||||
self.ensure_one()
|
||||
payload = {
|
||||
"project_name": self.project_name,
|
||||
"role": self.role,
|
||||
"technologies": [item.strip() for item in (self.technologies or "").split(",") if item.strip()],
|
||||
"duration": self.duration,
|
||||
"description": self.description,
|
||||
}
|
||||
return {key: value for key, value in payload.items() if value not in (False, None, "", [])}
|
||||
|
||||
|
||||
class HrRecruitmentAutoDocOtherLine(models.TransientModel):
|
||||
_name = "hr.recruitment.auto.doc.other.line"
|
||||
_description = "HR Recruitment Auto Document Other Parsed Data Line"
|
||||
_order = "id"
|
||||
|
||||
wizard_line_id = fields.Many2one(
|
||||
"hr.recruitment.auto.doc.wizard.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
key = fields.Char(required=True)
|
||||
value = fields.Text()
|
||||
|
||||
def _json_value(self):
|
||||
self.ensure_one()
|
||||
if not self.value:
|
||||
return False
|
||||
try:
|
||||
return json.loads(self.value)
|
||||
except Exception:
|
||||
return self.value
|
||||
|
|
|
|||
|
|
@ -48,7 +48,141 @@
|
|||
</div>
|
||||
|
||||
<group string="Uploaded Files" invisible="not line_ids">
|
||||
<field name="line_ids" nolabel="1" readonly="1">
|
||||
<field name="line_ids" nolabel="1" create="0" delete="0">
|
||||
<form string="Parsed Document" create="0" delete="0">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="file_name" readonly="1"/>
|
||||
<field name="state" readonly="1"/>
|
||||
<field name="message" readonly="1"/>
|
||||
<field name="target_model" invisible="1"/>
|
||||
<field name="extracted_payload" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="candidate_id" readonly="1" invisible="target_model == 'job_recruitment'"/>
|
||||
<field name="applicant_id" readonly="1" invisible="target_model != 'applicant'"/>
|
||||
<field name="attachment_id" readonly="1"/>
|
||||
<field name="file" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook invisible="target_model == 'job_recruitment'">
|
||||
<page string="Candidate Details">
|
||||
<group>
|
||||
<group>
|
||||
<field name="full_name"/>
|
||||
<field name="first_name"/>
|
||||
<field name="last_name"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
<field name="alternate_phone"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="linkedin_profile"/>
|
||||
<field name="current_location"/>
|
||||
<field name="current_organization"/>
|
||||
<field name="total_experience_years"/>
|
||||
<field name="relevant_experience_years"/>
|
||||
<field name="notice_period"/>
|
||||
<field name="degree"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="summary" nolabel="1" placeholder="Summary"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Skills">
|
||||
<field name="skills_text" nolabel="1" placeholder="Enter one skill per line"/>
|
||||
</page>
|
||||
<page string="Education">
|
||||
<field name="education_line_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="education_type"/>
|
||||
<field name="specialization"/>
|
||||
<field name="university"/>
|
||||
<field name="start_year"/>
|
||||
<field name="end_year"/>
|
||||
<field name="marks_or_grade"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Experience">
|
||||
<field name="employer_line_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="company_name"/>
|
||||
<field name="designation"/>
|
||||
<field name="date_of_joining"/>
|
||||
<field name="last_working_day"/>
|
||||
<field name="ctc"/>
|
||||
<field name="work_description"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Family">
|
||||
<field name="family_line_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="relation_type"/>
|
||||
<field name="name"/>
|
||||
<field name="contact_no"/>
|
||||
<field name="dob"/>
|
||||
<field name="location"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Certifications">
|
||||
<field name="certification_line_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="certification_name"/>
|
||||
<field name="provider"/>
|
||||
<field name="completion_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Other">
|
||||
<field name="other_line_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="key"/>
|
||||
<field name="value"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
<notebook invisible="target_model != 'job_recruitment'">
|
||||
<page string="Job Details">
|
||||
<group>
|
||||
<group>
|
||||
<field name="request_id"/>
|
||||
<field name="job_title"/>
|
||||
<field name="site_location"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="budget"/>
|
||||
<field name="experience_years"/>
|
||||
<field name="job_category"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="job_summary" nolabel="1" placeholder="Job Summary"/>
|
||||
<field name="requirements" nolabel="1" placeholder="Requirements"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Skills">
|
||||
<group>
|
||||
<field name="primary_skills_text" nolabel="1" placeholder="Enter one primary skill per line"/>
|
||||
<field name="secondary_skills_text" nolabel="1" placeholder="Enter one secondary skill per line"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_save_line_changes" string="Save" type="object" class="btn-primary"/>
|
||||
<button name="action_close_line" string="Close" type="object" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
<kanban class="o_kanban_small_column">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
'data/data.xml',
|
||||
'data/sequence.xml',
|
||||
'data/mail_template.xml',
|
||||
'data/templates.xml',
|
||||
# 'data/templates.xml',
|
||||
'views/res_config_settings.xml',
|
||||
'views/survey_survey.xml',
|
||||
'views/hr_recruitment_category.xml',
|
||||
'views/submission_share_history.xml',
|
||||
'views/job_category.xml',
|
||||
'views/hr_location.xml',
|
||||
|
|
@ -64,7 +67,7 @@
|
|||
'web.assets_frontend': [
|
||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||
# 'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,8 +156,11 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
|
||||
|
||||
|
||||
@http.route(['/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
||||
website=True)
|
||||
@http.route([
|
||||
'/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>',
|
||||
'/FTPROTECH/JoiningForm/<int:applicant_id>',
|
||||
], type='http', auth="public",
|
||||
website=True)
|
||||
def post_onboarding_form(self, applicant_id, **kwargs):
|
||||
"""Renders the website form for applicants to submit additional details."""
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
|
|
@ -174,8 +177,11 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
return request.not_found()
|
||||
|
||||
|
||||
@http.route(['/SRIVYNPLATFORMS/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
||||
methods=['POST'], website=True, csrf=False)
|
||||
@http.route([
|
||||
'/SRIVYNPLATFORMS/submit/<int:applicant_id>/JoinForm',
|
||||
'/FTPROTECH/submit/<int:applicant_id>/JoinForm',
|
||||
], type='http', auth="public",
|
||||
methods=['POST'], website=True, csrf=False)
|
||||
def process_employee_joining_form(self,applicant_id,**post):
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
|
|
@ -188,10 +194,10 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0)))
|
||||
|
||||
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
|
||||
|
||||
employee_id = post.get('employee_id')
|
||||
applicant_data = {
|
||||
'applicant_id': int(post.get('applicant_id', 0)),
|
||||
'employee_id': int(post.get('employee_id', 0)),
|
||||
'employee_id': int(employee_id) if employee_id else False,
|
||||
'candidate_image': post.get('candidate_image_base64', ''),
|
||||
'doj': datetime.strptime(post.get('doj'), '%Y-%m-%d').date() if post.get('doj', None) else '',
|
||||
'email_from': post.get('email_from', ''),
|
||||
|
|
@ -276,10 +282,17 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
attachments_data_json = post.get('attachments_data_json', '[]')
|
||||
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
||||
|
||||
applicant.write(applicant_data)
|
||||
applicant.replace_joining_attachments(attachments_data)
|
||||
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
||||
raise_if_not_found=False)
|
||||
applicant.write(applicant_data)
|
||||
applicant.replace_joining_attachments(attachments_data)
|
||||
if 'recruitment.employee.bridge' in request.env.registry:
|
||||
bridge = request.env['recruitment.employee.bridge'].sudo().search([
|
||||
('applicant_id', '=', applicant.id),
|
||||
('active', '=', True),
|
||||
], limit=1, order='id desc')
|
||||
if bridge:
|
||||
bridge.state = 'jod_received'
|
||||
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
||||
raise_if_not_found=False)
|
||||
group = request.env.ref('hr.group_hr_manager')
|
||||
users = request.env['res.users'].sudo().search([
|
||||
('groups_id', 'in', group.ids),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from . import res_config_settings
|
||||
from . import survey_survey
|
||||
from . import hr_recruitment_category
|
||||
from . import submission_share_history
|
||||
from . import hr_recruitment
|
||||
from . import hr_job_recruitment
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class CandidateExperience(models.Model):
|
|||
experience_code = fields.Char('Experience Code')
|
||||
experience_from = fields.Integer(string="Experience From (Years)")
|
||||
experience_to = fields.Integer(string="Experience To (Years)")
|
||||
company_id = fields.Many2one('res.company','Company', default=lambda self: self.env.company)
|
||||
# display_name = fields.Char(string="Display Name")
|
||||
# active = fields.Boolean()
|
||||
|
||||
|
|
|
|||
|
|
@ -284,8 +284,12 @@ class HRApplicant(models.Model):
|
|||
'applicant_id',
|
||||
string='Request Forms'
|
||||
)
|
||||
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
||||
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||
doc_requests_form_status = fields.Selection(
|
||||
[('draft', 'Draft'), ('email_sent_to_candidate', 'Email Sent to Candidate'), ('done', 'Done')],
|
||||
default='draft',
|
||||
)
|
||||
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
||||
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
||||
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
||||
employee_code = fields.Char(related="employee_id.employee_id")
|
||||
|
|
@ -300,8 +304,14 @@ class HRApplicant(models.Model):
|
|||
('validated', 'Validated')], default='pending')
|
||||
|
||||
approval_required = fields.Boolean(related='recruitment_stage_id.require_approval')
|
||||
application_submitted = fields.Boolean(string="Application Submitted")
|
||||
resume = fields.Binary(related='candidate_id.resume', readonly=False, compute_sudo=True)
|
||||
application_submitted = fields.Boolean(string="Application Submitted")
|
||||
send_second_application_form = fields.Boolean(string="Send Second Application Form")
|
||||
send_post_onboarding_form = fields.Boolean(string="Send Post Onboarding Form")
|
||||
second_application_form_status = fields.Selection(
|
||||
[('draft', 'Draft'), ('email_sent_to_candidate', 'Email Sent to Candidate'), ('done', 'Done')],
|
||||
default='draft',
|
||||
)
|
||||
resume = fields.Binary(related='candidate_id.resume', readonly=False, compute_sudo=True)
|
||||
resume_type = fields.Char(related='candidate_id.resume_type', readonly=False, compute_sudo=True)
|
||||
resume_name = fields.Char(related='candidate_id.resume_name', readonly=False, compute_sudo=True)
|
||||
|
||||
|
|
@ -350,11 +360,11 @@ class HRApplicant(models.Model):
|
|||
'joining_attachment_ids': [(0, 0, values) for values in latest_attachments.values()]
|
||||
})
|
||||
|
||||
def action_share_applicant(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Share Applicant'),
|
||||
def action_share_applicant(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Share Applicant'),
|
||||
'res_model': 'client.submission.mails.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('hr_recruitment_extended.view_client_submission_mails_template_wizard_form').id,
|
||||
|
|
@ -362,12 +372,15 @@ class HRApplicant(models.Model):
|
|||
'context': {
|
||||
'default_template_id': self.env.ref(
|
||||
'hr_recruitment_extended.application_client_submission_email_template'
|
||||
).id,
|
||||
},
|
||||
}
|
||||
|
||||
def submit_for_approval(self):
|
||||
for rec in self:
|
||||
).id,
|
||||
},
|
||||
}
|
||||
|
||||
def submit_to_client(self):
|
||||
return self.action_share_applicant()
|
||||
|
||||
def submit_for_approval(self):
|
||||
for rec in self:
|
||||
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
|
||||
if not manager_id:
|
||||
raise ValidationError(_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
||||
|
|
@ -419,11 +432,11 @@ class HRApplicant(models.Model):
|
|||
template.send_mail(applicant.id, force_send=True)
|
||||
applicant.second_application_form_status = 'email_sent_to_candidate'
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
for rec in self:
|
||||
if not rec.employee_id:
|
||||
raise ValidationError(_('You must first create the employee before before Sending the Post Onboarding Form'))
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
for rec in self:
|
||||
if not rec.employee_id:
|
||||
raise ValidationError(_('You must first create the employee before before Sending the Post Onboarding Form'))
|
||||
|
||||
elif not rec.employee_id.employee_id:
|
||||
raise ValidationError(_('Employee Code for the Employee (%s) is missing')%(rec.employee_id.name))
|
||||
return {
|
||||
|
|
@ -432,14 +445,17 @@ class HRApplicant(models.Model):
|
|||
'res_model': 'post.onboarding.attachment.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_type': 'form',
|
||||
'target': 'new',
|
||||
'context': {'default_req_attachment_ids': []}
|
||||
}
|
||||
|
||||
|
||||
def send_pre_onboarding_doc_request_form_to_candidate(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'context': {'default_req_attachment_ids': []}
|
||||
}
|
||||
|
||||
def send_post_onboarding_form_to_candidate(self):
|
||||
return self.send_jod_form_to_employee()
|
||||
|
||||
|
||||
def send_pre_onboarding_doc_request_form_to_candidate(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Select Attachments',
|
||||
'res_model': 'post.onboarding.attachment.wizard',
|
||||
'view_mode': 'form',
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@ class RecruitmentCategory(models.Model):
|
|||
|
||||
category_name = fields.Char(string="Category Name")
|
||||
default_user = fields.Many2one('res.users')
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company ,string='Company', ondelete='cascade')
|
||||
|
||||
|
||||
class ApplicationsStageStatus(models.Model):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
from odoo import fields, models, api, _
|
||||
|
||||
|
||||
class ApplicantCategory(models.Model):
|
||||
_inherit = "hr.applicant.category"
|
||||
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
allow_cross_company_candidates = fields.Boolean(
|
||||
string="Allow Cross-Company Candidate Access",config_parameter='hr_recruitment_extended.allow_cross_company_candidates',
|
||||
)
|
||||
|
||||
def set_values(self):
|
||||
res = super().set_values()
|
||||
rule = self.env.ref("hr_recruitment.hr_candidate_comp_rule")
|
||||
rule.write({
|
||||
"active": not self.allow_cross_company_candidates
|
||||
})
|
||||
self.env.registry.clear_cache()
|
||||
return res
|
||||
|
|
@ -4,4 +4,5 @@ from odoo import models, fields, api, _
|
|||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
||||
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
class SurveySurvey(models.Model):
|
||||
_inherit = "survey.survey"
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
|
|
@ -14,6 +14,43 @@
|
|||
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_job_recruitment_comp_rule" model="ir.rule">
|
||||
<field name="name">Job Recruitment multi company rule</field>
|
||||
<field name="model_id" ref="model_hr_job_recruitment"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<record id="hr_applicant_category_comp_rule" model="ir.rule">
|
||||
<field name="name">hr Applicant category multi company rule</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant_category"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="candidate_experience_comp_rule" model="ir.rule">
|
||||
<field name="name">candidate experience multi company rule</field>
|
||||
<field name="model_id" ref="model_candidate_experience"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
<record id="survey_survey_comp_rule" model="ir.rule">
|
||||
<field name="name">survey survey multi company rule</field>
|
||||
<field name="model_id" ref="survey.model_survey_survey"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="job_category_comp_rule" model="ir.rule">
|
||||
<field name="name">Job Category multi company rule</field>
|
||||
<field name="model_id" ref="model_job_category"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<function name="write" model="ir.model.data">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('module', '=', 'hr_recruitment_skills'), ('name','=','hr_applicant_skill_interviewer_rule')] "/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -10,6 +10,7 @@
|
|||
<field name="experience_code" placeholder = "E1" required="1" width="30%"/>
|
||||
<field name="experience_from" required="1" placeholder="0" />
|
||||
<field name="experience_to" required="1" placeholder="2" />
|
||||
<field name="company_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_applicant_category_view_tree_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.category.tree.inherit</field>
|
||||
<field name="model">hr.applicant.category</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_applicant_category_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="inside">
|
||||
<field name="company_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<list string="Category" editable="bottom">
|
||||
<field name="category_name" required="1"/>
|
||||
<field name="default_user"/>
|
||||
<field name="company_id" required="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.requisitions.access</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@name='recruitment_process_div']" position="inside">
|
||||
<setting string="Allow Cross-Company Candidate Access"
|
||||
help="Select it if you want to access the candidates across all the companies"
|
||||
id="cross_company_candidates_access_control">
|
||||
<field name="allow_cross_company_candidates"
|
||||
options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
</setting>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -56,7 +56,8 @@
|
|||
<group>
|
||||
|
||||
<field name="company_type" widget="radio"
|
||||
options="{'horizontal': true}"/>
|
||||
options="{'horizontal': true}" force_save="1"/>
|
||||
<field name="company_id" force_save="1"/>
|
||||
|
||||
<field name="email"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="survey_survey_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.survey.survey.form.inherit</field>
|
||||
<field name="model">survey.survey</field>
|
||||
<field name="inherit_id" ref="survey.survey_survey_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='restrict_user_ids']" position="after">
|
||||
<field name="company_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "HRMS Employee Dashboard",
|
||||
"name": "Dashboard",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Human Resources",
|
||||
"summary": "Employee self-service dashboard with attendance, leaves, expenses, equipment, and payslips",
|
||||
"summary": "Employee, manager, and HR self-service dashboard",
|
||||
"author": "Pranay",
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
|
|
@ -11,6 +11,12 @@
|
|||
"hr",
|
||||
"hr_attendance",
|
||||
"hr_holidays",
|
||||
"hr_expense",
|
||||
"calendar",
|
||||
"project_todo",
|
||||
"hr_resignation",
|
||||
"knowledge",
|
||||
"website",
|
||||
"maintenance",
|
||||
"employee_it_declaration",
|
||||
"business_travel_expense_management",
|
||||
|
|
@ -18,6 +24,7 @@
|
|||
],
|
||||
"data": [
|
||||
"views/hrms_emp_dashboard_views.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1 @@
|
|||
from . import res_config_settings
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
hrms_dashboard_show_on_duty = fields.Boolean(
|
||||
string="On-Duty Requests",
|
||||
config_parameter="hrms_emp_dashboard.show_on_duty",
|
||||
default=True,
|
||||
)
|
||||
hrms_dashboard_show_late_coming = fields.Boolean(
|
||||
string="Late Coming Requests",
|
||||
config_parameter="hrms_emp_dashboard.show_late_coming",
|
||||
default=True,
|
||||
)
|
||||
hrms_dashboard_show_overtime = fields.Boolean(
|
||||
string="Overtime Requests",
|
||||
config_parameter="hrms_emp_dashboard.show_overtime",
|
||||
default=True,
|
||||
)
|
||||
hrms_dashboard_show_shift_swap = fields.Boolean(
|
||||
string="Shift Swap Requests",
|
||||
config_parameter="hrms_emp_dashboard.show_shift_swap",
|
||||
default=True,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,21 +13,23 @@ class HrmsEmployeeDashboard extends Component {
|
|||
const today = new Date();
|
||||
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
const weekStart = new Date(today);
|
||||
weekStart.setDate(today.getDate() - ((today.getDay() + 6) % 7));
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
this.state = useState({
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null,
|
||||
period: "this_month",
|
||||
calendarView: "weekly",
|
||||
calendarView: "monthly",
|
||||
dateFrom: this.formatDate(monthStart),
|
||||
dateTo: this.formatDate(monthEnd),
|
||||
calendarDateFrom: this.formatDate(weekStart),
|
||||
calendarDateTo: this.formatDate(weekEnd),
|
||||
calendarDateFrom: this.formatDate(monthStart),
|
||||
calendarDateTo: this.formatDate(monthEnd),
|
||||
selectedDay: null,
|
||||
activeTab: this.getStoredActiveTab(),
|
||||
holidayTab: 'current',
|
||||
leaveRequests: [],
|
||||
leaveRequestsTotal: 0,
|
||||
leaveRequestsLimit: 5,
|
||||
});
|
||||
this.rpc = rpc;
|
||||
this.action = useService("action");
|
||||
|
|
@ -42,7 +44,6 @@ class HrmsEmployeeDashboard extends Component {
|
|||
});
|
||||
});
|
||||
onWillDestroy(() => this.destroyCharts());
|
||||
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
|
|
@ -52,6 +53,22 @@ class HrmsEmployeeDashboard extends Component {
|
|||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
getStoredActiveTab() {
|
||||
try {
|
||||
return window.localStorage.getItem("hrms_emp_dashboard.activeTab") || "employee";
|
||||
} catch {
|
||||
return "employee";
|
||||
}
|
||||
}
|
||||
|
||||
storeActiveTab(tab = this.state.activeTab) {
|
||||
try {
|
||||
window.localStorage.setItem("hrms_emp_dashboard.activeTab", tab || "employee");
|
||||
} catch {
|
||||
// Local storage can be unavailable in restricted browser contexts.
|
||||
}
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
this.state.loading = true;
|
||||
this.state.error = null;
|
||||
|
|
@ -63,17 +80,45 @@ class HrmsEmployeeDashboard extends Component {
|
|||
calendar_date_to: this.state.calendarDateTo,
|
||||
calendar_view: this.state.calendarView,
|
||||
});
|
||||
if (!response.success) {
|
||||
if (!response.success) {
|
||||
this.state.error = response.error || "Unable to load employee dashboard.";
|
||||
return;
|
||||
}
|
||||
this.state.data = response;
|
||||
if (!["employee", "manager", "hr"].includes(this.state.activeTab)) {
|
||||
this.state.activeTab = "employee";
|
||||
}
|
||||
if (this.state.activeTab === "manager" && !response.access?.manager) {
|
||||
this.state.activeTab = "employee";
|
||||
}
|
||||
if (this.state.activeTab === "hr" && !response.access?.hr) {
|
||||
this.state.activeTab = response.access?.manager ? "manager" : "employee";
|
||||
}
|
||||
this.storeActiveTab();
|
||||
|
||||
// --- FILTER HOLIDAYS FOR TABS (Frontend Logic) ---
|
||||
// If your backend doesn't separate them yet, we do it here based on 'date_from'
|
||||
const today = new Date();
|
||||
const currentMonth = today.getMonth();
|
||||
const currentYear = today.getFullYear();
|
||||
|
||||
// If backend returns public_holidays[]
|
||||
const allHolidays = response.public_holidays || [];
|
||||
const yearlyHolidays = response.all_public_holidays || [];
|
||||
|
||||
// "This Month" tab = holidays within the selected period filter (backend already filters)
|
||||
this.state.data.current_holidays = allHolidays;
|
||||
|
||||
// "Upcoming" tab = all yearly holidays strictly after today
|
||||
this.state.data.upcoming_holidays = yearlyHolidays.filter(h => {
|
||||
const hDate = new Date(h.date_from);
|
||||
return hDate > today;
|
||||
});
|
||||
// -------------------------------------------------
|
||||
this.loadLeaveRequests(true);
|
||||
|
||||
const expenses = response.expenses || {};
|
||||
const total =
|
||||
(expenses.series || []).reduce(
|
||||
(sum, value) => sum + Number(value || 0),
|
||||
0
|
||||
);
|
||||
const total = (expenses.series || []).reduce((sum, value) => sum + Number(value || 0), 0);
|
||||
this.state.expenseCount = total;
|
||||
setTimeout(() => this.initCharts(), 100);
|
||||
} catch (error) {
|
||||
|
|
@ -85,6 +130,30 @@ class HrmsEmployeeDashboard extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
async loadLeaveRequests(reset = false) {
|
||||
if (reset) {
|
||||
this.state.leaveRequestsLimit = 5;
|
||||
this.state.leaveRequests = [];
|
||||
}
|
||||
try {
|
||||
const response = await this.rpc("/hrms_emp_dashboard/leave_requests", {
|
||||
limit: this.state.leaveRequestsLimit,
|
||||
offset: 0,
|
||||
});
|
||||
if (response.success) {
|
||||
this.state.leaveRequests = response.leaves;
|
||||
this.state.leaveRequestsTotal = response.total;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not load leave requests:", e);
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreLeaves() {
|
||||
this.state.leaveRequestsLimit += 5;
|
||||
this.loadLeaveRequests();
|
||||
}
|
||||
|
||||
destroyCharts() {
|
||||
for (const chart of this.charts) {
|
||||
try {
|
||||
|
|
@ -301,6 +370,170 @@ class HrmsEmployeeDashboard extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
openDashboardMenu(menu) {
|
||||
if (!menu || !menu.action) {
|
||||
return;
|
||||
}
|
||||
this.storeActiveTab();
|
||||
if (menu.action.xml_id) {
|
||||
this.action.doAction(menu.action.xml_id);
|
||||
return;
|
||||
}
|
||||
this.action.doAction(menu.action);
|
||||
}
|
||||
|
||||
switchTab(tab) {
|
||||
this.state.activeTab = tab;
|
||||
this.storeActiveTab(tab);
|
||||
setTimeout(() => this.initCharts(), 80);
|
||||
}
|
||||
|
||||
openWorkAction(action) {
|
||||
if (action) {
|
||||
this.storeActiveTab();
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
async runApproval(record, operation) {
|
||||
try {
|
||||
const response = await this.rpc("/hrms_emp_dashboard/approval_action", {
|
||||
model: record.model,
|
||||
record_id: record.id,
|
||||
operation,
|
||||
});
|
||||
if (!response.success) {
|
||||
this.notification.add(response.error || "Unable to update request", { type: "danger" });
|
||||
return;
|
||||
}
|
||||
this.notification.add(response.message || "Request updated", { type: "success" });
|
||||
await this.loadData();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.notification.add("Unable to update request", { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
createFromDashboardMenu(menu) {
|
||||
if (!menu || !menu.create_action) {
|
||||
return;
|
||||
}
|
||||
this.storeActiveTab();
|
||||
this.action.doAction(menu.create_action);
|
||||
}
|
||||
|
||||
runDayAction(action) {
|
||||
const day = this.state.selectedDay;
|
||||
if (!day || !action) {
|
||||
return;
|
||||
}
|
||||
const handlers = {
|
||||
apply_leave: () => this.applyLeaveForDate(day.date),
|
||||
add_todo: () => this.addTodoForDate(day.date),
|
||||
open_attendance: () => this.openAttendancesForDate(day.date),
|
||||
open_calendar: () => this.openCalendarForDate(day.date, day.type),
|
||||
create_meeting: () => this.createMeetingForDate(day.date),
|
||||
};
|
||||
const handler = handlers[action.key];
|
||||
if (handler) {
|
||||
handler();
|
||||
this.closeDayDetails();
|
||||
}
|
||||
}
|
||||
|
||||
applyLeaveForDate(date) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: "Apply Leave",
|
||||
res_model: "hr.leave",
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context: {
|
||||
default_employee_id: this.state.data.employee.id,
|
||||
default_request_date_from: date,
|
||||
default_request_date_to: date,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addTodoForDate(date) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: "Add To-do",
|
||||
res_model: "project.task",
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context: {
|
||||
default_name: "To-do for " + date,
|
||||
default_date_deadline: date + " 18:00:00",
|
||||
default_user_ids: [[4, this.state.data.employee.user_id]],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openAttendancesForDate(date) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: "Attendance - " + date,
|
||||
res_model: "hr.attendance",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
domain: [
|
||||
["employee_id", "=", this.state.data.employee.id],
|
||||
["check_in", ">=", date + " 00:00:00"],
|
||||
["check_in", "<=", date + " 23:59:59"],
|
||||
],
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
|
||||
openCalendarForDate(date, type = "day") {
|
||||
const domain = [];
|
||||
if (type !== "month") {
|
||||
domain.push(["start", "<=", date + " 23:59:59"]);
|
||||
domain.push(["stop", ">=", date + " 00:00:00"]);
|
||||
}
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: "Calendar",
|
||||
res_model: "calendar.event",
|
||||
views: [[false, "calendar"], [false, "list"], [false, "form"]],
|
||||
domain,
|
||||
target: "current",
|
||||
context: {
|
||||
default_start: date + " 09:00:00",
|
||||
default_stop: date + " 10:00:00",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createMeetingFromCalendar() {
|
||||
this.createMeetingForDate(this.state.calendarDateFrom || this.formatDate(new Date()));
|
||||
}
|
||||
|
||||
createMeetingForDate(date) {
|
||||
if (date && date.length === 7) {
|
||||
date = date + "-01";
|
||||
}
|
||||
const start = date + " 09:00:00";
|
||||
const stop = date + " 10:00:00";
|
||||
const context = {
|
||||
default_name: "Meeting",
|
||||
default_start: start,
|
||||
default_stop: stop,
|
||||
};
|
||||
if (this.state.data.employee.partner_id) {
|
||||
context.default_partner_ids = [[4, this.state.data.employee.partner_id]];
|
||||
}
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: "New Meeting",
|
||||
res_model: "calendar.event",
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
// addExpense() {
|
||||
// this.action.doAction({
|
||||
// type: "ir.actions.act_window",
|
||||
|
|
@ -418,6 +651,71 @@ class HrmsEmployeeDashboard extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
buildDayDetails(day) {
|
||||
if (!day || day.status === 'empty') return [];
|
||||
const details = [];
|
||||
if (day.worked_display) details.push({ label: "Worked", value: day.worked_display });
|
||||
if (day.break_display) details.push({ label: "Break", value: day.break_display });
|
||||
if (day.expected_display) details.push({ label: "Expected", value: day.expected_display });
|
||||
if (day.balance_display) details.push({ label: "Balance", value: day.balance_display });
|
||||
if (day.leave && parseFloat(day.leave) > 0) details.push({ label: "Leave", value: day.leave + " hrs" });
|
||||
if (day.holiday && parseFloat(day.holiday) > 0) details.push({ label: "Holiday", value: day.holiday + " day(s)" });
|
||||
if (day.event_count) details.push({ label: "Events", value: day.event_count });
|
||||
if (day.date) details.push({ label: "Date", value: day.date });
|
||||
return details;
|
||||
}
|
||||
|
||||
async openDayDetails(day) {
|
||||
if (!day || day.status === 'empty') return;
|
||||
|
||||
let fullDay = day;
|
||||
let gotBackendSignals = false;
|
||||
|
||||
// Try backend first (for tasks, extra leaves, holidays)
|
||||
if (day.date && day.type !== 'month') {
|
||||
try {
|
||||
const response = await this.rpc("/hrms_emp_dashboard/day_details", {
|
||||
date: day.date,
|
||||
});
|
||||
if (response.success && response.day && response.day.signals && response.day.signals.length > 0) {
|
||||
fullDay = { ...day, ...response.day };
|
||||
gotBackendSignals = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Backend details unavailable, using calendar data:", e);
|
||||
}
|
||||
}
|
||||
|
||||
let finalSignals;
|
||||
|
||||
if (gotBackendSignals) {
|
||||
// Backend returned full signals — just strip any leftover "more"
|
||||
finalSignals = (fullDay.signals || []).filter(s => s.type !== 'more');
|
||||
} else {
|
||||
// Rebuild ALL signals from day.events (contains every event, no truncation)
|
||||
const existingSignals = fullDay.signals || [];
|
||||
const nonEventSignals = existingSignals.filter(s => s.type !== 'more' && s.type !== 'event');
|
||||
const eventSignals = (fullDay.events || []).map(event => ({
|
||||
type: 'event',
|
||||
label: event.display_time
|
||||
? event.display_time + ' ' + event.name
|
||||
: event.name,
|
||||
icon: 'fa fa-users',
|
||||
}));
|
||||
finalSignals = [...nonEventSignals, ...eventSignals];
|
||||
}
|
||||
|
||||
this.state.selectedDay = {
|
||||
...fullDay,
|
||||
signals: finalSignals,
|
||||
details: this.buildDayDetails(fullDay),
|
||||
};
|
||||
}
|
||||
|
||||
closeDayDetails() {
|
||||
this.state.selectedDay = null;
|
||||
}
|
||||
|
||||
get statusText() {
|
||||
return this.state.data?.attendance_state === "checked_in" ? "Check Out" : "Check In";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,27 @@
|
|||
<div t-if="state.loading" class="hrms-loading">Loading employee dashboard...</div>
|
||||
|
||||
<t t-if="state.data && !state.loading">
|
||||
<div class="hrms-dashboard-tabs">
|
||||
<button type="button"
|
||||
t-att-class="'hrms-dashboard-tab ' + (state.activeTab === 'employee' ? 'active' : '')"
|
||||
t-on-click="() => this.switchTab('employee')">
|
||||
<i class="fa fa-user"/> Employee Self Service
|
||||
</button>
|
||||
<button t-if="state.data.access.manager"
|
||||
type="button"
|
||||
t-att-class="'hrms-dashboard-tab ' + (state.activeTab === 'manager' ? 'active' : '')"
|
||||
t-on-click="() => this.switchTab('manager')">
|
||||
<i class="fa fa-users"/> Manager Self Service
|
||||
</button>
|
||||
<button t-if="state.data.access.hr"
|
||||
type="button"
|
||||
t-att-class="'hrms-dashboard-tab ' + (state.activeTab === 'hr' ? 'active' : '')"
|
||||
t-on-click="() => this.switchTab('hr')">
|
||||
<i class="fa fa-briefcase"/> HR Self Service
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div t-if="state.activeTab === 'employee'">
|
||||
<section class="hrms-employee-card">
|
||||
<div class="hrms-employee-main">
|
||||
<img class="hrms-avatar" t-att-src="state.data.employee.image_url" alt="Employee"/>
|
||||
|
|
@ -27,21 +48,15 @@
|
|||
</div>
|
||||
<div class="hrms-employee-actions">
|
||||
<div class="hrms-action-buttons">
|
||||
<!-- <button class="hrms-icon-button primary" t-on-click="toggleAttendance" t-att-title="statusText">-->
|
||||
<!-- <i t-att-class="state.data.attendance_state === 'checked_in' ? 'fa fa-sign-out' : 'fa fa-sign-in'"/>-->
|
||||
<!-- <span t-esc="statusText"/>-->
|
||||
<!-- </button>-->
|
||||
<button
|
||||
t-att-class="'hrms-icon-button ' + (state.data.attendance_state === 'checked_in' ? 'checkout' : 'checkin')"
|
||||
t-on-click="toggleAttendance"
|
||||
t-att-title="statusText">
|
||||
|
||||
<i t-att-class="state.data.attendance_state === 'checked_in'
|
||||
? 'fa fa-sign-out'
|
||||
: 'fa fa-sign-in'"/>
|
||||
|
||||
<i t-att-class="state.data.attendance_state === 'checked_in'
|
||||
? 'fa fa-sign-out'
|
||||
: 'fa fa-sign-in'"/>
|
||||
<span t-esc="statusText"/>
|
||||
</button>
|
||||
</button>
|
||||
<button class="hrms-icon-button" t-on-click="downloadPayslip" title="Download Payslip">
|
||||
<i class="fa fa-download"/>
|
||||
<span>Payslip</span>
|
||||
|
|
@ -53,7 +68,7 @@
|
|||
</div>
|
||||
<div class="hrms-filter-box">
|
||||
<span>Period</span>
|
||||
<select class="form-select form-select-sm" t-on-change="onPeriodChange" t-att-value="state.period">
|
||||
<select class="form-select form-select-sm form-emp-card-select" t-on-change="onPeriodChange" t-att-value="state.period">
|
||||
<option value="this_month" t-att-selected="state.period === 'this_month'">This Month</option>
|
||||
<option value="this_year" t-att-selected="state.period === 'this_year'">This Year</option>
|
||||
<option value="last_3_months" t-att-selected="state.period === 'last_3_months'">Last 3 Months</option>
|
||||
|
|
@ -73,6 +88,34 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hrms-menus-row">
|
||||
<article t-foreach="state.data.dashboard_menus" t-as="menu" t-key="menu.key"
|
||||
t-att-class="'hrms-menu ' + menu.color"
|
||||
t-on-click="() => this.openDashboardMenu(menu)"
|
||||
t-att-title="menu.subtitle">
|
||||
<span class="hrms-menu-glow"/>
|
||||
<span class="hrms-menu-icon"><i t-att-class="menu.icon"/></span>
|
||||
<span class="hrms-menu-content">
|
||||
<span class="hrms-menu-title" t-esc="menu.title"/>
|
||||
<small t-esc="menu.subtitle"/>
|
||||
</span>
|
||||
<span t-if="menu.badge" class="hrms-menu-badge" t-esc="menu.badge"/>
|
||||
<span t-if="menu.show_count" class="hrms-menu-count">
|
||||
<strong t-esc="menu.count"/>
|
||||
<small t-esc="menu.count_label"/>
|
||||
</span>
|
||||
<button t-if="menu.create_action"
|
||||
type="button"
|
||||
class="hrms-menu-plus-btn"
|
||||
t-on-click.stop="() => this.createFromDashboardMenu(menu)"
|
||||
t-att-title="'Create ' + menu.title">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
<span t-if="!menu.show_count and !menu.create_action" class="hrms-menu-arrow"><i class="fa fa-arrow-right"/></span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- KPI ROW: Full width, 5 columns -->
|
||||
<section class="hrms-kpi-row">
|
||||
<div class="hrms-kpi"><span>Expected Hours</span><strong t-esc="state.data.attendance_summary.expected_display"/></div>
|
||||
<div class="hrms-kpi present"><span>Worked Hours</span><strong t-esc="state.data.attendance_summary.worked_display"/></div>
|
||||
|
|
@ -84,14 +127,18 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div class="hrms-grid">
|
||||
<section class="hrms-panel wide">
|
||||
<div class="hrms-employee-columns">
|
||||
<div class="hrms-employee-main-column">
|
||||
<section class="hrms-panel hrms-calendar-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Attendance Calendar</h2>
|
||||
<h2>Work Calendar</h2>
|
||||
<div class="hrms-month-controls">
|
||||
<button class="btn btn-light btn-sm" t-on-click="previousMonth"><i class="fa fa-chevron-left"/></button>
|
||||
<strong t-esc="monthLabel"/>
|
||||
<button class="btn btn-light btn-sm" t-on-click="nextMonth"><i class="fa fa-chevron-right"/></button>
|
||||
<button class="btn btn-primary btn-sm" t-on-click="createMeetingFromCalendar">
|
||||
<i class="fa fa-plus me-1"/>Meeting
|
||||
</button>
|
||||
<select class="form-select form-select-sm hrms-calendar-select" t-on-change="onCalendarViewChange" t-att-value="state.calendarView">
|
||||
<option value="weekly" t-att-selected="state.calendarView === 'weekly'">Weekly</option>
|
||||
<option value="monthly" t-att-selected="state.calendarView === 'monthly'">Monthly</option>
|
||||
|
|
@ -109,10 +156,14 @@
|
|||
<span>Sun</span>
|
||||
</div>
|
||||
<div class="hrms-calendar" t-att-class="state.calendarView === 'yearly' ? 'yearly' : ''">
|
||||
<div t-foreach="state.data.attendance_calendar" t-as="day" t-key="day.date" class="hrms-day" t-att-class="day.status" t-att-title="day.label || day.worked_display">
|
||||
<t t-if="day.status === 'empty'">
|
||||
<span/>
|
||||
</t>
|
||||
<button t-foreach="state.data.attendance_calendar" t-as="day" t-key="day.date"
|
||||
type="button"
|
||||
class="hrms-day"
|
||||
t-att-class="day.status"
|
||||
t-att-disabled="day.status === 'empty'"
|
||||
t-att-title="day.label || day.worked_display"
|
||||
t-on-click="() => this.openDayDetails(day)">
|
||||
<t t-if="day.status === 'empty'"><span/></t>
|
||||
<t t-elif="day.type === 'month'">
|
||||
<div class="hrms-day-top">
|
||||
<span t-esc="day.weekday"/>
|
||||
|
|
@ -123,222 +174,418 @@
|
|||
<small><t t-esc="day.worked_display"/> worked</small>
|
||||
<small><t t-esc="day.break_display"/> break</small>
|
||||
<small><t t-esc="day.leave"/> leave · <t t-esc="day.holiday"/> holidays</small>
|
||||
<small t-if="day.event_count"><i class="fa fa-users me-1"/> <t t-esc="day.event_count"/> events</small>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="hrms-day-top">
|
||||
<strong>
|
||||
<t t-esc="day.day"/>
|
||||
<span t-esc="day.weekday"/>
|
||||
</strong>
|
||||
<span t-if="day.label and day.show_metrics" class="hrms-day-badge" t-esc="day.label"/>
|
||||
<strong t-esc="day.day"/>
|
||||
<span t-esc="day.weekday"/>
|
||||
</div>
|
||||
<div t-if="day.worked_display"
|
||||
t-att-class="'hrms-day-worked' + (day.worked_display.trim().charAt(0) === '0' && !'123456789'.includes(day.worked_display.trim().charAt(1)) ? ' zero' : '')">
|
||||
<t t-esc="day.worked_display"/>
|
||||
</div>
|
||||
<div t-if="!day.show_metrics and day.label" class="hrms-day-message">
|
||||
<span t-if="day.status === 'leave'" class="hrms-day-message-icon"><i class="fa fa-calendar-times-o"/></span>
|
||||
<span t-elif="day.status === 'holiday'" class="hrms-day-message-icon"><i class="fa fa-calendar"/></span>
|
||||
<strong t-esc="day.label || '-'"/>
|
||||
</div>
|
||||
<div t-if="day.show_metrics" t-att-class="'hrms-day-balance ' + (day.balance_hours >= 0 ? 'positive' : 'negative')">
|
||||
<t t-esc="day.balance_display"/>
|
||||
</div>
|
||||
<div t-if="day.show_metrics" class="hrms-day-metrics">
|
||||
<div>
|
||||
<span>Wrk.</span>
|
||||
<strong t-esc="day.worked_display"/>
|
||||
</div>
|
||||
<div>
|
||||
<span>Break</span>
|
||||
<strong t-esc="day.break_display"/>
|
||||
</div>
|
||||
<div>
|
||||
<span>Exp.</span>
|
||||
<strong t-esc="day.expected_display"/>
|
||||
</div>
|
||||
<div t-if="day.signals && day.signals.length" class="hrms-day-signals">
|
||||
<t t-foreach="day.signals" t-as="signal" t-key="signal.type + signal.label">
|
||||
<button t-if="signal.type === 'more'"
|
||||
type="button"
|
||||
class="hrms-day-signal more hrms-more-btn"
|
||||
t-on-click.stop="() => this.openDayDetails(day)"
|
||||
t-att-title="'Click to view all'">
|
||||
<i class="fa fa-ellipsis-h"/>
|
||||
<t t-esc="signal.label"/>
|
||||
</button>
|
||||
<span t-else="" t-att-class="'hrms-day-signal ' + signal.type">
|
||||
<i t-att-class="signal.icon"/>
|
||||
<t t-esc="signal.label"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hrms-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Leave Balance</h2>
|
||||
<button class="btn btn-primary btn-sm" t-on-click="applyLeave"><i class="fa fa-plus me-1"/>Apply Leave</button>
|
||||
</div>
|
||||
<div class="hrms-leave-summary">
|
||||
<div t-if="!state.data.leave_balances.length" class="hrms-muted">No leave balances available.</div>
|
||||
<div t-foreach="state.data.leave_balances" t-as="leave" t-key="leave.id">
|
||||
<div t-if="leave.requires_allocation === 'yes'" class="hrms-leave-tile">
|
||||
<strong t-esc="leave.name"/>
|
||||
<div>
|
||||
<span><b t-esc="leave.remaining"/> Balance</span>
|
||||
<span><b t-esc="leave.taken"/> Taken</span>
|
||||
<span><b t-esc="leave.planned"/> Confirmed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hrms-leave-graph">
|
||||
<div t-foreach="state.data.leave_balances" t-as="leave" t-key="leave.id + '-graph'">
|
||||
<div t-if="leave.requires_allocation === 'yes'" class="hrms-leave-graph-row">
|
||||
<div class="hrms-leave-graph-label">
|
||||
<strong t-esc="leave.name"/>
|
||||
<span><t t-esc="leave.allocated"/> allocated</span>
|
||||
</div>
|
||||
<div class="hrms-leave-bar" role="img" t-att-aria-label="leave.name + ': ' + leave.remaining + ' balance, ' + leave.taken + ' taken, ' + leave.planned + ' confirmed'">
|
||||
<span class="hrms-leave-bar-remaining" t-att-style="'width: ' + leave.remaining_percent + '%'"/>
|
||||
<span class="hrms-leave-bar-taken" t-att-style="'width: ' + leave.taken_percent + '%'"/>
|
||||
<span class="hrms-leave-bar-planned" t-att-style="'width: ' + leave.planned_percent + '%'"/>
|
||||
</div>
|
||||
<div class="hrms-leave-legend">
|
||||
<span><i class="remaining"/>Balance <b t-esc="leave.remaining"/></span>
|
||||
<span><i class="taken"/>Taken <b t-esc="leave.taken"/></span>
|
||||
<span><i class="planned"/>Confirmed <b t-esc="leave.planned"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hrms-panel">
|
||||
<h2>Public Holidays</h2>
|
||||
<div class="hrms-list">
|
||||
<div t-if="!state.data.public_holidays.length" class="hrms-muted">No public holidays in the selected period.</div>
|
||||
<div t-foreach="state.data.public_holidays" t-as="holiday" t-key="holiday.id" class="hrms-list-row">
|
||||
<i class="fa fa-calendar"/>
|
||||
<div><strong t-esc="holiday.name"/><span><t t-esc="holiday.date_from"/> - <t t-esc="holiday.date_to"/></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- <section class="hrms-panel wide">-->
|
||||
<!-- <div class="hrms-panel-header">-->
|
||||
<!-- <h2>Expenses</h2>-->
|
||||
<!-- <div class="hrms-panel-actions">-->
|
||||
<!-- <button class="btn btn-primary btn-sm" t-on-click="addExpense"><i class="fa fa-plus me-1"/>Add Expense</button>-->
|
||||
<!-- <button class="btn btn-light btn-sm" t-on-click="addBusinessTravel"><i class="fa fa-suitcase me-1"/>Add Business Travel</button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- -->
|
||||
<!-- <div class="hrms-two-charts">-->
|
||||
<!-- <div id="hrmsExpenseChart"/>-->
|
||||
<!-- <div id="hrmsExpenseStateChart"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </section>-->
|
||||
|
||||
<!-- <section class="hrms-panel wide">-->
|
||||
<!-- <div class="hrms-panel-header">-->
|
||||
<!-- <h2>Expenses</h2>-->
|
||||
<!-- <div class="hrms-panel-actions">-->
|
||||
<!-- <button class="btn btn-primary btn-sm"-->
|
||||
<!-- t-on-click="addExpense">-->
|
||||
<!-- <i class="fa fa-plus me-1"/>-->
|
||||
<!-- Add Expenses-->
|
||||
<!-- </button>-->
|
||||
<!-- <button class="btn btn-light btn-sm"-->
|
||||
<!-- t-on-click="addBusinessTravel">-->
|
||||
<!-- <i class="fa fa-suitcase me-1"/>-->
|
||||
<!-- Add Business Travel-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!– Charts –>-->
|
||||
<!-- <t t-if="state.expenseCount > 0">-->
|
||||
<!-- <div class="hrms-two-charts">-->
|
||||
<!-- <div id="hrmsExpenseChart"></div>-->
|
||||
<!-- <div id="hrmsExpenseStateChart"></div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </t>-->
|
||||
<!-- <!– Empty State –>-->
|
||||
<!-- <t t-else="">-->
|
||||
<!-- <div class="expense-empty-state">-->
|
||||
<!-- <i class="fa fa-receipt empty-icon"></i>-->
|
||||
<!-- <h3>No Expense Records Found</h3>-->
|
||||
<!-- <p>-->
|
||||
<!-- No expenses were found for the selected period.-->
|
||||
<!-- Click the button below to create your first expense.-->
|
||||
<!-- </p>-->
|
||||
<!-- <button class="btn btn-primary"-->
|
||||
<!-- t-on-click="addExpense">-->
|
||||
<!-- <i class="fa fa-plus me-1"></i>-->
|
||||
<!-- Add Expense-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </t>-->
|
||||
<!-- </section>-->
|
||||
<section class="hrms-panel wide">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Expenses</h2>
|
||||
|
||||
<div class="hrms-panel-actions">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
t-on-click="addExpense">
|
||||
<i class="fa fa-plus me-1"/>
|
||||
Add Expense
|
||||
</button>
|
||||
|
||||
<!-- <button class="btn btn-light btn-sm"-->
|
||||
<!-- t-on-click="addBusinessTravel">-->
|
||||
<!-- <i class="fa fa-suitcase me-1"/>-->
|
||||
<!-- Add Business Travel-->
|
||||
<!-- </button>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expense Charts -->
|
||||
<t t-if="state.expenseCount > 0">
|
||||
<div class="hrms-two-charts">
|
||||
<div id="hrmsExpenseChart"></div>
|
||||
<div id="hrmsExpenseStateChart"></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Empty State -->
|
||||
<t t-else="">
|
||||
<div class="expense-empty-state">
|
||||
|
||||
<div class="empty-icon">
|
||||
<i class="fa fa-file-invoice-dollar"/>
|
||||
</div>
|
||||
|
||||
<h3>No Expense Records Found</h3>
|
||||
|
||||
<p>
|
||||
There are no expense records available for the selected
|
||||
period. Click <strong>Add Expense</strong> to create your
|
||||
first expense claim.
|
||||
</p>
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary"
|
||||
t-on-click="addExpense">
|
||||
<i class="fa fa-plus me-1"/>
|
||||
Add Expense
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="hrms-panel wide">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Allocated Equipment</h2>
|
||||
<button class="btn btn-light btn-sm" t-on-click="() => this.openEquipment()">View All</button>
|
||||
</div>
|
||||
<div class="hrms-equipment-grid">
|
||||
<div t-if="!state.data.equipment.length" class="hrms-muted">No equipment allocated.</div>
|
||||
<button t-foreach="state.data.equipment" t-as="item" t-key="item.id" class="hrms-equipment" t-on-click="() => this.openEquipment(item.id)">
|
||||
<i class="fa fa-laptop"/>
|
||||
<strong t-esc="item.name"/>
|
||||
<span t-esc="item.category"/>
|
||||
<small><t t-esc="item.serial || '-'"/> · <t t-esc="item.assign_date || '-'"/></small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hrms-panel hrms-expense-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Recent Expenses</h2>
|
||||
<div class="hrms-panel-actions">
|
||||
<button class="btn btn-primary btn-sm" t-on-click="addExpense">
|
||||
<i class="fa fa-plus me-1"/> Add Expense
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hrms-expense-body">
|
||||
<t t-if="state.expenseCount > 0">
|
||||
<div class="hrms-two-charts">
|
||||
<div id="hrmsExpenseChart"></div>
|
||||
<div id="hrmsExpenseStateChart"></div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="expense-empty-state">
|
||||
<div class="empty-icon"><i class="fa fa-file-invoice-dollar"/></div>
|
||||
<h3>No Expense Records Found</h3>
|
||||
<p>There are no expense records available for the selected period. Click <strong>Add Expense</strong> to create your first expense claim.</p>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary" t-on-click="addExpense"><i class="fa fa-plus me-1"/> Add Expense</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="hrms-employee-side-column">
|
||||
<section class="hrms-panel hrms-leave-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Leave Balance</h2>
|
||||
<button class="btn btn-primary btn-sm" t-on-click="applyLeave"><i class="fa fa-plus me-1"/>Apply Leave</button>
|
||||
</div>
|
||||
<div class="hrms-leave-scroll">
|
||||
<div t-if="!state.data.leave_balances.length" class="hrms-empty-message">
|
||||
<i class="fa fa-info-circle"/>
|
||||
<span>Nothing to show yet</span>
|
||||
</div>
|
||||
<div class="hrms-balance-grid">
|
||||
<t t-foreach="state.data.leave_balances" t-as="leave" t-key="leave.id">
|
||||
<div t-if="leave.requires_allocation === 'yes'" class="hrms-balance-card">
|
||||
<div class="hrms-balance-card-icon">
|
||||
<i class="fa fa-calendar-check-o"/>
|
||||
</div>
|
||||
<div class="hrms-balance-card-info">
|
||||
<strong t-esc="leave.name"/>
|
||||
<span class="hrms-balance-card-days">
|
||||
<t t-esc="leave.remaining"/> Days
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-if="state.data.leave_balances.length" class="hrms-leave-divider">
|
||||
<span>My Leave Requests</span>
|
||||
</div>
|
||||
|
||||
<div class="hrms-leave-requests">
|
||||
<div t-if="!state.leaveRequests.length" class="hrms-muted hrms-leave-empty-req">No leave requests found.</div>
|
||||
<div t-foreach="state.leaveRequests" t-as="req" t-key="req.id" class="hrms-leave-req-row">
|
||||
<div class="hrms-leave-req-info">
|
||||
<strong t-esc="req.name"/>
|
||||
<span class="hrms-leave-req-dates">
|
||||
<t t-esc="req.date_from.split(' ')[0]"/>
|
||||
<t t-if="req.date_from.split(' ')[0] !== req.date_to.split(' ')[0]">
|
||||
→ <t t-esc="req.date_to.split(' ')[0]"/>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hrms-leave-req-meta">
|
||||
<span class="hrms-leave-req-duration"><t t-esc="req.duration"/>d</span>
|
||||
<span t-att-class="'hrms-leave-req-state ' + req.state" t-esc="req.state_label"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-if="state.leaveRequestsTotal > state.leaveRequestsLimit">
|
||||
<button class="hrms-load-more-btn" t-on-click="loadMoreLeaves">
|
||||
Load More (<t t-esc="state.leaveRequestsTotal - state.leaveRequestsLimit"/> remaining)
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</section>
|
||||
<section class="hrms-panel hrms-holidays-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Public Holidays</h2>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<input type="radio" class="btn-check" name="holiday_tabs" id="holiday_tab_current" value="current" t-att-checked="state.holidayTab === 'current'" t-on-change="() => this.state.holidayTab = 'current'"/>
|
||||
<label class="btn btn-outline-primary" for="holiday_tab_current">Period</label>
|
||||
<input type="radio" class="btn-check" name="holiday_tabs" id="holiday_tab_upcoming" value="upcoming" t-att-checked="state.holidayTab === 'upcoming'" t-on-change="() => this.state.holidayTab = 'upcoming'"/>
|
||||
<label class="btn btn-outline-primary" for="holiday_tab_upcoming">Upcoming</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hrms-holidays-scroll">
|
||||
<div t-if="state.holidayTab === 'current'">
|
||||
<div t-if="!state.data.current_holidays.length" class="hrms-muted">No public holidays in the selected month.</div>
|
||||
<div t-foreach="state.data.current_holidays" t-as="holiday" t-key="holiday.id" class="hrms-list-row">
|
||||
<i class="fa fa-calendar"/>
|
||||
<div><strong t-esc="holiday.name"/><span><t t-esc="holiday.date_from"/> - <t t-esc="holiday.date_to"/></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="state.holidayTab === 'upcoming'">
|
||||
<div t-if="!state.data.upcoming_holidays.length" class="hrms-muted">No upcoming public holidays.</div>
|
||||
<div t-foreach="state.data.upcoming_holidays" t-as="holiday" t-key="holiday.id" class="hrms-list-row">
|
||||
<i class="fa fa-calendar"/>
|
||||
<div><strong t-esc="holiday.name"/><span><t t-esc="holiday.date_from"/> - <t t-esc="holiday.date_to"/></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hrms-panel hrms-equipment-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Allocated Equipment</h2>
|
||||
<button class="btn btn-light btn-sm"
|
||||
t-on-click="() => this.openEquipment()">
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
<div class="hrms-equipment-timeline">
|
||||
<div t-if="!state.data.equipment.length"
|
||||
class="hrms-muted">
|
||||
No equipment allocated.
|
||||
</div>
|
||||
<div t-foreach="state.data.equipment"
|
||||
t-as="item"
|
||||
t-key="item.id"
|
||||
class="hrms-equipment-row">
|
||||
<!-- LEFT DATE -->
|
||||
<div class="hrms-equipment-date">
|
||||
<strong t-esc="item.assign_date or '-'"/>
|
||||
</div>
|
||||
<!-- TIMELINE -->
|
||||
<div class="hrms-equipment-line">
|
||||
<span class="timeline-dot"/>
|
||||
<span class="timeline-line"/>
|
||||
</div>
|
||||
<!-- CARD -->
|
||||
<button class="hrms-equipment-card"
|
||||
t-on-click="() => this.openEquipment(item.id)">
|
||||
<div class="equipment-service-count">
|
||||
<i class="fa fa-wrench"/>
|
||||
<t t-esc="item.service_open_count"/>
|
||||
</div>
|
||||
<div class="equipment-title">
|
||||
<t t-esc="item.name"/>
|
||||
</div>
|
||||
<div class="equipment-category">
|
||||
<t t-esc="item.category"/>
|
||||
</div>
|
||||
<div t-if="item.serial" class="equipment-serial">
|
||||
Asset :
|
||||
<t t-esc="item.serial or '-'"/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="state.activeTab === 'manager'" class="hrms-service-dashboard">
|
||||
<section class="hrms-service-hero manager">
|
||||
<div>
|
||||
<span>Manager Workspace</span>
|
||||
<h1 t-esc="state.data.manager_dashboard.hero.title"/>
|
||||
<p t-esc="state.data.manager_dashboard.hero.subtitle"/>
|
||||
</div>
|
||||
<button class="btn btn-light" t-on-click="() => this.openWorkAction(state.data.manager_dashboard.menus[0] && state.data.manager_dashboard.menus[0].action)">
|
||||
<i class="fa fa-users me-1"/> Open Team
|
||||
</button>
|
||||
</section>
|
||||
<section class="hrms-service-kpis">
|
||||
<article t-foreach="state.data.manager_dashboard.kpis" t-as="kpi" t-key="kpi.label" t-att-class="'hrms-service-kpi ' + kpi.tone">
|
||||
<span t-esc="kpi.label"/>
|
||||
<strong t-esc="kpi.value"/>
|
||||
</article>
|
||||
</section>
|
||||
<section class="hrms-menus-row hrms-service-menu-row">
|
||||
<article t-foreach="state.data.manager_dashboard.menus" t-as="menu" t-key="menu.key"
|
||||
t-att-class="'hrms-menu ' + menu.color"
|
||||
t-on-click="() => this.openDashboardMenu(menu)">
|
||||
<span class="hrms-menu-icon"><i t-att-class="menu.icon"/></span>
|
||||
<span class="hrms-menu-content">
|
||||
<span class="hrms-menu-title" t-esc="menu.title"/>
|
||||
<small t-esc="menu.subtitle"/>
|
||||
</span>
|
||||
<span t-if="menu.show_count" class="hrms-menu-count">
|
||||
<strong t-esc="menu.count"/>
|
||||
<small t-esc="menu.count_label"/>
|
||||
</span>
|
||||
</article>
|
||||
</section>
|
||||
<div class="hrms-service-grid">
|
||||
<section class="hrms-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Approval Queue</h2>
|
||||
<span class="hrms-panel-count"><t t-esc="state.data.manager_dashboard.approval_queue.length"/> items</span>
|
||||
</div>
|
||||
<div class="hrms-approval-list">
|
||||
<div t-if="!state.data.manager_dashboard.approval_queue.length" class="hrms-muted">No pending manager approvals.</div>
|
||||
<article t-foreach="state.data.manager_dashboard.approval_queue" t-as="item" t-key="item.model + item.id" class="hrms-approval-row">
|
||||
<button class="hrms-approval-main" t-on-click="() => this.openWorkAction(item.action)">
|
||||
<span class="hrms-approval-category" t-esc="item.category"/>
|
||||
<strong t-esc="item.employee || item.title"/>
|
||||
<small><t t-esc="item.department || '-'"/> · <t t-esc="item.date || '-'"/></small>
|
||||
</button>
|
||||
<div class="hrms-approval-actions">
|
||||
<span t-att-class="'hrms-leave-req-state ' + item.state" t-esc="item.state_label"/>
|
||||
<button t-if="item.can_approve" class="btn btn-success btn-sm" t-on-click="() => this.runApproval(item, 'approve')"><i class="fa fa-check"/></button>
|
||||
<button t-if="item.can_reject" class="btn btn-outline-danger btn-sm" t-on-click="() => this.runApproval(item, 'reject')"><i class="fa fa-times"/></button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="hrms-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Team Today</h2>
|
||||
<span class="hrms-panel-count"><t t-esc="state.data.manager_dashboard.team.length"/> people</span>
|
||||
</div>
|
||||
<div class="hrms-team-list">
|
||||
<div t-if="!state.data.manager_dashboard.team.length" class="hrms-muted">No team members reporting to you.</div>
|
||||
<button t-foreach="state.data.manager_dashboard.team" t-as="member" t-key="member.id" class="hrms-team-row" t-on-click="() => this.openWorkAction(member.action)">
|
||||
<img t-att-src="member.image_url" alt=""/>
|
||||
<span>
|
||||
<strong t-esc="member.name"/>
|
||||
<small><t t-esc="member.job || 'Employee'"/> · <t t-esc="member.department || '-'"/></small>
|
||||
</span>
|
||||
<em t-att-class="member.tone" t-esc="member.status"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="hrms-menus-row hrms-service-menu-row">
|
||||
<article t-foreach="state.data.manager_dashboard.approval_tiles" t-as="menu" t-key="menu.key"
|
||||
t-att-class="'hrms-menu ' + menu.color"
|
||||
t-on-click="() => this.openDashboardMenu(menu)">
|
||||
<span class="hrms-menu-icon"><i t-att-class="menu.icon"/></span>
|
||||
<span class="hrms-menu-content">
|
||||
<span class="hrms-menu-title" t-esc="menu.title"/>
|
||||
<small t-esc="menu.subtitle"/>
|
||||
</span>
|
||||
<span class="hrms-menu-count"><strong t-esc="menu.count"/><small t-esc="menu.count_label"/></span>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div t-if="state.activeTab === 'hr'" class="hrms-service-dashboard">
|
||||
<section class="hrms-service-hero hr">
|
||||
<div>
|
||||
<span>HR Workspace</span>
|
||||
<h1 t-esc="state.data.hr_dashboard.hero.title"/>
|
||||
<p t-esc="state.data.hr_dashboard.hero.subtitle"/>
|
||||
</div>
|
||||
<button class="btn btn-light" t-on-click="() => this.openDashboardMenu(state.data.hr_dashboard.menus[0])">
|
||||
<i class="fa fa-users me-1"/> Open Employees
|
||||
</button>
|
||||
</section>
|
||||
<section class="hrms-service-kpis">
|
||||
<article t-foreach="state.data.hr_dashboard.kpis" t-as="kpi" t-key="kpi.label" t-att-class="'hrms-service-kpi ' + kpi.tone">
|
||||
<span t-esc="kpi.label"/>
|
||||
<strong t-esc="kpi.value"/>
|
||||
</article>
|
||||
</section>
|
||||
<section class="hrms-menus-row hrms-service-menu-row">
|
||||
<article t-foreach="state.data.hr_dashboard.menus" t-as="menu" t-key="menu.key"
|
||||
t-att-class="'hrms-menu ' + menu.color"
|
||||
t-on-click="() => this.openDashboardMenu(menu)">
|
||||
<span class="hrms-menu-icon"><i t-att-class="menu.icon"/></span>
|
||||
<span class="hrms-menu-content">
|
||||
<span class="hrms-menu-title" t-esc="menu.title"/>
|
||||
<small t-esc="menu.subtitle"/>
|
||||
</span>
|
||||
<span t-if="menu.show_count" class="hrms-menu-count"><strong t-esc="menu.count"/><small t-esc="menu.count_label"/></span>
|
||||
</article>
|
||||
</section>
|
||||
<div class="hrms-service-grid">
|
||||
<section class="hrms-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>HR Approval Queue</h2>
|
||||
<span class="hrms-panel-count"><t t-esc="state.data.hr_dashboard.approval_queue.length"/> items</span>
|
||||
</div>
|
||||
<div class="hrms-approval-list">
|
||||
<div t-if="!state.data.hr_dashboard.approval_queue.length" class="hrms-muted">No pending HR approvals.</div>
|
||||
<article t-foreach="state.data.hr_dashboard.approval_queue" t-as="item" t-key="item.model + item.id" class="hrms-approval-row">
|
||||
<button class="hrms-approval-main" t-on-click="() => this.openWorkAction(item.action)">
|
||||
<span class="hrms-approval-category" t-esc="item.category"/>
|
||||
<strong t-esc="item.employee || item.title"/>
|
||||
<small><t t-esc="item.department || '-'"/> · <t t-esc="item.date || '-'"/></small>
|
||||
</button>
|
||||
<div class="hrms-approval-actions">
|
||||
<span t-att-class="'hrms-leave-req-state ' + item.state" t-esc="item.state_label"/>
|
||||
<button t-if="item.can_approve" class="btn btn-success btn-sm" t-on-click="() => this.runApproval(item, 'approve')"><i class="fa fa-check"/></button>
|
||||
<button t-if="item.can_reject" class="btn btn-outline-danger btn-sm" t-on-click="() => this.runApproval(item, 'reject')"><i class="fa fa-times"/></button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="hrms-panel">
|
||||
<div class="hrms-panel-header">
|
||||
<h2>Workforce Signals</h2>
|
||||
</div>
|
||||
<div class="hrms-workforce-list">
|
||||
<article t-foreach="state.data.hr_dashboard.workforce" t-as="signal" t-key="signal.label" class="hrms-workforce-row">
|
||||
<i t-att-class="signal.icon"/>
|
||||
<span t-esc="signal.label"/>
|
||||
<strong t-esc="signal.value"/>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="hrms-menus-row hrms-service-menu-row">
|
||||
<article t-foreach="state.data.hr_dashboard.approval_tiles" t-as="menu" t-key="menu.key"
|
||||
t-att-class="'hrms-menu ' + menu.color"
|
||||
t-on-click="() => this.openDashboardMenu(menu)">
|
||||
<span class="hrms-menu-icon"><i t-att-class="menu.icon"/></span>
|
||||
<span class="hrms-menu-content">
|
||||
<span class="hrms-menu-title" t-esc="menu.title"/>
|
||||
<small t-esc="menu.subtitle"/>
|
||||
</span>
|
||||
<span class="hrms-menu-count"><strong t-esc="menu.count"/><small t-esc="menu.count_label"/></span>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</t>
|
||||
<div t-if="state.selectedDay" class="hrms-day-modal-backdrop" t-on-click="closeDayDetails">
|
||||
<section class="hrms-day-modal" t-on-click.stop="">
|
||||
<header class="hrms-modal-header">
|
||||
<div class="hrms-modal-title-group">
|
||||
<h2>
|
||||
<t t-if="state.selectedDay.type === 'month'" t-esc="state.selectedDay.label"/>
|
||||
<t t-else=""><t t-esc="state.selectedDay.day"/> <t t-esc="state.selectedDay.weekday"/></t>
|
||||
</h2>
|
||||
<span t-att-class="'hrms-status-pill ' + state.selectedDay.status" t-esc="state.selectedDay.label || state.selectedDay.status"/>
|
||||
</div>
|
||||
<button type="button" class="hrms-modal-close" t-on-click="closeDayDetails" title="Close">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</header>
|
||||
<div class="hrms-modal-body">
|
||||
<div class="hrms-detail-chips">
|
||||
<div t-foreach="state.selectedDay.details || []" t-as="detail" t-key="detail.label" class="hrms-detail-chip">
|
||||
<span t-esc="detail.label"/>
|
||||
<strong t-esc="detail.value"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="state.selectedDay.signals && state.selectedDay.signals.length" class="hrms-modal-events">
|
||||
<span class="hrms-modal-events-label">Events & Activities</span>
|
||||
<div class="hrms-modal-events-list">
|
||||
<span t-foreach="state.selectedDay.signals" t-as="signal" t-key="signal.type + signal.label"
|
||||
t-if="signal.type !== 'more'"
|
||||
t-att-class="'hrms-event-tag ' + signal.type">
|
||||
<i t-att-class="signal.icon"/>
|
||||
<t t-esc="signal.label"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer t-if="state.selectedDay.actions && state.selectedDay.actions.length">
|
||||
<button t-foreach="state.selectedDay.actions" t-as="action" t-key="action.key"
|
||||
type="button"
|
||||
t-att-class="'btn btn-sm ' + (action.primary ? 'btn-primary' : 'btn-light')"
|
||||
t-on-click="() => this.runDayAction(action)">
|
||||
<i t-att-class="action.icon + ' me-1'"/>
|
||||
<t t-esc="action.label"/>
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_hrms_emp_dashboard" model="ir.actions.client">
|
||||
<field name="name">Employee Dashboard</field>
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="tag">hrms_emp_dashboard</field>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_hrms_emp_dashboard_root"
|
||||
name="Employee Dashboard"
|
||||
name="Dashboard"
|
||||
action="action_hrms_emp_dashboard"
|
||||
web_icon="hrms_emp_dashboard,static/description/icon.png"
|
||||
sequence="-90"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form_dashboard" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.dashboard</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="95"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@name='employees_setting_container']" position="after">
|
||||
<block title="Dashboard" name="hrms_dashboard_setting_container">
|
||||
<setting string="On-Duty Requests"
|
||||
help="Show the On-Duty request shortcut on Employee Self Service."
|
||||
id="hrms_dashboard_show_on_duty_setting">
|
||||
<field name="hrms_dashboard_show_on_duty"/>
|
||||
</setting>
|
||||
<setting string="Late Coming Requests"
|
||||
help="Show the Late Coming request shortcut on Employee Self Service."
|
||||
id="hrms_dashboard_show_late_coming_setting">
|
||||
<field name="hrms_dashboard_show_late_coming"/>
|
||||
</setting>
|
||||
<setting string="Overtime Requests"
|
||||
help="Show the Overtime request shortcut on Employee Self Service."
|
||||
id="hrms_dashboard_show_overtime_setting">
|
||||
<field name="hrms_dashboard_show_overtime"/>
|
||||
</setting>
|
||||
<setting string="Shift Swap Requests"
|
||||
help="Show the Shift Swap request shortcut on Employee Self Service."
|
||||
id="hrms_dashboard_show_shift_swap_setting">
|
||||
<field name="hrms_dashboard_show_shift_swap"/>
|
||||
</setting>
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB |
|
|
@ -8,17 +8,15 @@
|
|||
""",
|
||||
'author': 'Raman Marikanti',
|
||||
'category': 'Human Resources',
|
||||
'depends': ['base', 'hr_recruitment', 'hr_payroll', 'hr_recruitment_extended'],
|
||||
'depends': ['base', 'employee_bridge', 'hr_payroll'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/mail_template.xml',
|
||||
'views/stages.xml',
|
||||
'views/offer_letter_views.xml',
|
||||
'views/hr_applicant_offer_views.xml',
|
||||
'views/offer_response_templates.xml',
|
||||
'views/recruitment_employee_bridge.xml',
|
||||
# 'views/templates.xml',
|
||||
'views/menu_views.xml',
|
||||
'wizards/offer_release_request_wizard.xml',
|
||||
'wizards/applicant_offer_mail_wizard.xml',
|
||||
'wizards/offer_letter_reject_wizard.xml',
|
||||
'report/offer_letter_report.xml',
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
<field name="name">Applicant Offer Email Template</field>
|
||||
<field name="model_id" ref="offer_letters.model_offer_letter"/>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.main_candidate_id.email_from or '' }}</field>
|
||||
<field name="subject">Offer Letter - {{ object.position or object.main_candidate_id.job_id.name or '' }}</field>
|
||||
<field name="email_to">{{ object.recipient_email or '' }}</field>
|
||||
<field name="subject">Offer Letter - {{ object.position or '' }}</field>
|
||||
<field name="description">
|
||||
Send applicant offer mail with offer letter attachment.
|
||||
</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.main_candidate_id.partner_name or ''"/>,</p>
|
||||
<p>Dear <t t-esc="object.main_candidate_name or ''"/>,</p>
|
||||
<p>
|
||||
With reference to the interview and subsequent discussions you had with us, we are pleased to select
|
||||
you for the position of "<t t-esc="object.position or ''"/>" in our organization with the following
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
from . import stages
|
||||
from . import offer_letter
|
||||
from . import hr_applicant
|
||||
from . import hr_candidate
|
||||
from . import recruitment_employee_bridge
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
|
||||
finalized_ctc = fields.Float(string='Finalized CTC', tracking=True)
|
||||
offer_letter_ids = fields.One2many('offer.letter', 'candidate_id', string='Offer Letters')
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
'offer.letter',
|
||||
string='Current Offer Letter',
|
||||
compute='_compute_current_offer_letter',
|
||||
store=True,
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
('requested', 'Requested'),
|
||||
('sent', 'Sent to Applicant'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired'),
|
||||
],
|
||||
string='Offer Status',
|
||||
related='current_offer_letter_id.state',
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
|
||||
request_offer_release = fields.Boolean(related='recruitment_stage_id.request_offer_release')
|
||||
|
||||
@api.depends('offer_letter_ids', 'offer_letter_ids.create_date', 'offer_letter_ids.state')
|
||||
def _compute_current_offer_letter(self):
|
||||
for applicant in self:
|
||||
offer_letters = applicant.offer_letter_ids.sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string('1970-01-01 00:00:00'), offer.id)
|
||||
)
|
||||
applicant.current_offer_letter_id = offer_letters[-1] if offer_letters else False
|
||||
|
||||
def action_request_offer_release(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Request Offer Release'),
|
||||
'res_model': 'offer.release.request.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('offer_letters.view_offer_release_request_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_applicant_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_send_offer(self):
|
||||
return self.action_request_offer_release()
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HRCandidate(models.Model):
|
||||
_inherit = 'hr.candidate'
|
||||
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
'offer.letter',
|
||||
string='Current Offer Letter',
|
||||
compute='_compute_current_offer_letter',
|
||||
store=False,
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
('requested', 'Requested'),
|
||||
('sent', 'Sent to Applicant'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired'),
|
||||
],
|
||||
string='Offer Status',
|
||||
related='current_offer_letter_id.state',
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('applicant_ids.current_offer_letter_id', 'applicant_ids.current_offer_letter_id.create_date')
|
||||
def _compute_current_offer_letter(self):
|
||||
for candidate in self:
|
||||
offer_letters = candidate.applicant_ids.mapped('current_offer_letter_id').sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string('1970-01-01 00:00:00'), offer.id)
|
||||
)
|
||||
candidate.current_offer_letter_id = offer_letters[-1] if offer_letters else False
|
||||
|
|
@ -27,24 +27,46 @@ class OfferLetter(models.Model):
|
|||
default=lambda self: _('New'),
|
||||
copy=False
|
||||
)
|
||||
candidate_id = fields.Many2one( 'hr.applicant', string='Applicant', required=False,
|
||||
)
|
||||
main_candidate_id = fields.Many2one('hr.candidate',string='Candidate', readonly=False, required=True)
|
||||
|
||||
@api.onchange('candidate_id')
|
||||
def _onchange_candidate_id(self):
|
||||
if self.candidate_id:
|
||||
self.main_candidate_id = self.candidate_id.candidate_id
|
||||
|
||||
main_candidate_name = fields.Char(compute="_compute_main_candidate_name", readonly=False)
|
||||
|
||||
@api.depends('candidate_id','main_candidate_id')
|
||||
def _compute_main_candidate_name(self):
|
||||
for rec in self:
|
||||
if rec.candidate_id:
|
||||
rec.main_candidate_name = rec.candidate_id.partner_name
|
||||
elif rec.main_candidate_id:
|
||||
rec.main_candidate_name = rec.main_candidate_id.partner_name
|
||||
bridge_id = fields.Many2one(
|
||||
'recruitment.employee.bridge',
|
||||
string='Onboarding',
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
main_candidate_name = fields.Char(string="Candidate Name", compute="_compute_bridge_contact", store=True)
|
||||
recipient_email = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||
recipient_street = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||
recipient_street2 = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||
recipient_city = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||
recipient_zip = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||
recipient_country_id = fields.Many2one("res.country", compute="_compute_bridge_contact", store=True)
|
||||
|
||||
@api.depends(
|
||||
'bridge_id',
|
||||
'bridge_id.employee_name',
|
||||
'bridge_id.employee_id.name',
|
||||
'bridge_id.work_email',
|
||||
'bridge_id.private_email',
|
||||
'bridge_id.employee_id.work_email',
|
||||
'bridge_id.employee_id.private_email',
|
||||
'bridge_id.private_street',
|
||||
'bridge_id.private_street2',
|
||||
'bridge_id.private_city',
|
||||
'bridge_id.private_zip',
|
||||
'bridge_id.private_country_id',
|
||||
)
|
||||
def _compute_bridge_contact(self):
|
||||
for rec in self:
|
||||
bridge = rec.bridge_id
|
||||
employee = bridge.employee_id
|
||||
rec.main_candidate_name = bridge.employee_name or employee.name or ''
|
||||
rec.recipient_email = bridge.work_email or bridge.private_email or employee.work_email or employee.private_email or ''
|
||||
rec.recipient_street = bridge.private_street
|
||||
rec.recipient_street2 = bridge.private_street2
|
||||
rec.recipient_city = bridge.private_city
|
||||
rec.recipient_zip = bridge.private_zip
|
||||
rec.recipient_country_id = bridge.private_country_id
|
||||
requested_by_id = fields.Many2one('res.users', string='Requested By', readonly=True, tracking=True)
|
||||
request_date = fields.Datetime(string='Requested On', readonly=True, tracking=True)
|
||||
|
||||
|
|
@ -120,20 +142,20 @@ class OfferLetter(models.Model):
|
|||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
|
||||
if vals.get('candidate_id') and 'salary' in vals:
|
||||
self.env['hr.applicant'].browse(vals['candidate_id']).write({
|
||||
'finalized_ctc': vals['salary'],
|
||||
})
|
||||
return super(OfferLetter, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
result = super(OfferLetter, self).write(vals)
|
||||
if 'salary' in vals:
|
||||
for record in self.filtered('candidate_id'):
|
||||
record.candidate_id.finalized_ctc = record.salary
|
||||
return result
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
|
||||
offer_letter = super(OfferLetter, self).create(vals)
|
||||
if vals.get('bridge_id') and 'salary' in vals and 'finalized_ctc' in offer_letter.bridge_id._fields:
|
||||
offer_letter.bridge_id.finalized_ctc = offer_letter.salary
|
||||
return offer_letter
|
||||
|
||||
def write(self, vals):
|
||||
result = super(OfferLetter, self).write(vals)
|
||||
if 'salary' in vals:
|
||||
for record in self.filtered('bridge_id'):
|
||||
if 'finalized_ctc' in record.bridge_id._fields:
|
||||
record.bridge_id.finalized_ctc = record.salary
|
||||
return result
|
||||
|
||||
def action_open_send_offer_wizard(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -162,7 +184,7 @@ class OfferLetter(models.Model):
|
|||
def action_accept_offer(self):
|
||||
self.ensure_one()
|
||||
# employee = self.env['hr.employee'].create({
|
||||
# 'name': self.candidate_id.partner_name,
|
||||
# 'name': self.main_candidate_name,
|
||||
# 'job_title': self.position,
|
||||
# 'department_id': self.department_id.id,
|
||||
# 'currency_id': self.currency_id.id,
|
||||
|
|
@ -174,17 +196,18 @@ class OfferLetter(models.Model):
|
|||
})
|
||||
return True
|
||||
|
||||
def action_reject_offer(self):
|
||||
def action_reject_offer(self, reason=False):
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'rejected',
|
||||
'response_date': fields.Datetime.now()
|
||||
'response_date': fields.Datetime.now(),
|
||||
'rejection_reason': reason
|
||||
})
|
||||
return True
|
||||
|
||||
@api.onchange('candidate_id')
|
||||
def _onchange_candidate_id(self):
|
||||
self.position = self.candidate_id.job_id.name
|
||||
@api.onchange('bridge_id')
|
||||
def _onchange_bridge_id(self):
|
||||
self.position = self.bridge_id.job_id.name
|
||||
|
||||
def get_paydetailed_lines(self):
|
||||
today = fields.Date.today()
|
||||
|
|
@ -278,3 +301,15 @@ class OfferLetter(models.Model):
|
|||
|
||||
def generate_pdf_report(self):
|
||||
return self.env.ref('offer_letters.hr_offer_letters_employee_print').report_action(self)
|
||||
|
||||
|
||||
def action_open_reject_wizard(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Reject Offer"),
|
||||
"res_model": "offer.letter.reject.wizard",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": {"default_offer_letter_id": self.id},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class RecruitmentEmployeeBridge(models.Model):
|
||||
_inherit = "recruitment.employee.bridge"
|
||||
|
||||
finalized_ctc = fields.Float(string="Finalized CTC", tracking=True)
|
||||
offer_letter_ids = fields.One2many("offer.letter", "bridge_id", string="Offer Letters")
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
"offer.letter",
|
||||
string="Current Offer Letter",
|
||||
compute="_compute_current_offer_letter",
|
||||
store=True,
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
("requested", "Requested"),
|
||||
("sent", "Sent to Candidate"),
|
||||
("accepted", "Accepted"),
|
||||
("rejected", "Rejected"),
|
||||
("expired", "Expired"),
|
||||
],
|
||||
string="Offer Status",
|
||||
related="current_offer_letter_id.state",
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends("offer_letter_ids", "offer_letter_ids.create_date", "offer_letter_ids.state")
|
||||
def _compute_current_offer_letter(self):
|
||||
for bridge in self:
|
||||
offer_letters = bridge.offer_letter_ids.sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string("1970-01-01 00:00:00"), offer.id)
|
||||
)
|
||||
bridge.current_offer_letter_id = offer_letters[-1] if offer_letters else False
|
||||
|
||||
def action_open_current_offer(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Offer Letter"),
|
||||
"res_model": "offer.letter",
|
||||
"view_mode": "form",
|
||||
"res_id": self.current_offer_letter_id.id,
|
||||
}
|
||||
|
|
@ -62,26 +62,26 @@
|
|||
<div style="margin-bottom: 25px;">
|
||||
<strong>To,</strong>
|
||||
<br/>
|
||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
||||
<strong t-esc="o.main_candidate_name"/>
|
||||
<br/>
|
||||
<div t-if="o.candidate_id and o.candidate_id.private_street and o.candidate_id.private_city">
|
||||
<t t-esc="o.candidate_id.private_street"/>
|
||||
<div t-if="o.recipient_street and o.recipient_city">
|
||||
<t t-esc="o.recipient_street"/>
|
||||
<br/>
|
||||
<t t-esc="o.candidate_id.private_street2" t-if="o.candidate_id.private_street2"/>
|
||||
<t t-if="o.candidate_id.private_street2">
|
||||
<t t-esc="o.recipient_street2" t-if="o.recipient_street2"/>
|
||||
<t t-if="o.recipient_street2">
|
||||
<br/>
|
||||
</t>
|
||||
<t t-esc="o.candidate_id.private_city"/>
|
||||
<t t-esc="o.candidate_id.private_zip"/>
|
||||
<t t-esc="o.recipient_city"/>
|
||||
<t t-esc="o.recipient_zip"/>
|
||||
<br/>
|
||||
<t t-esc="o.candidate_id.private_country_id.name"/>
|
||||
<t t-esc="o.recipient_country_id.name"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEAR LINE -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<strong>Dear
|
||||
<t t-esc="o.main_candidate_id.partner_name"/>,
|
||||
<t t-esc="o.main_candidate_name"/>,
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
|
|
@ -368,7 +368,7 @@
|
|||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
||||
<strong t-esc="o.main_candidate_name"/>
|
||||
<br/>
|
||||
<strong t-esc="o.position"/>
|
||||
<br/>
|
||||
|
|
@ -486,7 +486,7 @@
|
|||
<br/>
|
||||
<br/>
|
||||
<strong>Employee Name :
|
||||
<t t-esc="o.main_candidate_id.partner_name"/>
|
||||
<t t-esc="o.main_candidate_name"/>
|
||||
</strong>
|
||||
<br/>
|
||||
<br/>
|
||||
|
|
@ -595,7 +595,7 @@
|
|||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
||||
<strong t-esc="o.main_candidate_name"/>
|
||||
<br/>
|
||||
<strong t-esc="o.position"/>
|
||||
<br/>
|
||||
|
|
@ -621,7 +621,7 @@
|
|||
Company incorporated under Indian Companies Act 1956, having registered office in
|
||||
Hyderabad,
|
||||
India ("Company")
|
||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
||||
<strong t-esc="o.main_candidate_name"/>
|
||||
(Recipient)
|
||||
</p>
|
||||
<p>Whereas "Company" wishes to explore the possibility of entering into an employment
|
||||
|
|
@ -875,7 +875,7 @@
|
|||
<strong t-esc="o.joining_date"/>
|
||||
</td>
|
||||
<td style="padding-top: 20px;">
|
||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
||||
<strong t-esc="o.main_candidate_name"/>
|
||||
<br/>
|
||||
<strong t-esc="o.position"/>
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|||
access_offer_letter_user,offer.letter.user,model_offer_letter,base.group_user,1,1,1,0
|
||||
access_offer_letter_manager,offer.letter.manager,model_offer_letter,hr.group_hr_manager,1,1,1,1
|
||||
access_applicant_offer_mail_wizard,applicant.offer.mail.wizard.user,offer_letters.model_applicant_offer_mail_wizard,base.group_user,1,1,1,1
|
||||
access_offer_release_request_wizard,offer.release.request.wizard.user,model_offer_release_request_wizard,base.group_user,1,1,1,1
|
||||
access_offer_letter_reject_wizard,offer.letter.reject.wizard.user,model_offer_letter_reject_wizard,base.group_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -23,7 +23,7 @@
|
|||
<button name="action_open_send_offer_wizard" type="object" string="Send Offer" class="oe_highlight"
|
||||
invisible="state != 'requested'" groups="hr.group_hr_manager"/>
|
||||
<button name="action_accept_offer" type="object" string="Accept Offer" invisible="state != 'sent'" class="oe_highlight"/>
|
||||
<!-- <button name="action_open_reject_wizard" type="object" string="Reject Offer" invisible="state != 'sent'" class="oe_danger"/>-->
|
||||
<button name="action_open_reject_wizard" type="object" string="Reject Offer" invisible="state != 'sent'" class="oe_danger"/>
|
||||
<button name="get_paydetailed_lines" type="object" string="Get Data" class="oe_danger"/>
|
||||
<button name="generate_pdf_report" type="object" string="Generate PDF" class="oe_highlight"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="requested,sent,accepted,rejected,expired"/>
|
||||
|
|
@ -32,9 +32,8 @@
|
|||
<group>
|
||||
<group>
|
||||
<field name="name" readonly="state != 'requested'"/>
|
||||
<field name="main_candidate_name" invisible="1"/>
|
||||
<field name="candidate_id" invisible="not candidate_id" readonly="1" force_save="1"/>
|
||||
<field name="main_candidate_id" invisible="candidate_id" force_save="1"/>
|
||||
<field name="bridge_id"/>
|
||||
<field name="main_candidate_name" readonly="1"/>
|
||||
<field name="requested_by_id" readonly="1"/>
|
||||
<field name="request_date" readonly="1"/>
|
||||
<field name="manager_id"/>
|
||||
|
|
@ -50,8 +49,9 @@
|
|||
<field name="probation_period"/>
|
||||
<field name="pay_struct_id"/>
|
||||
<field name="sent_date" readonly="1"/>
|
||||
<field name="response_date" readonly="1"/>
|
||||
</group>
|
||||
<field name="response_date" readonly="1"/>
|
||||
<field name="recipient_email" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Terms Conditions">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_employee_bridge_form_inherit_offer_letter" model="ir.ui.view">
|
||||
<field name="name">recruitment.employee.bridge.form.inherit.offer.letter</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="inherit_id" ref="employee_bridge.view_employee_bridge_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Smart Button -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
|
||||
<button
|
||||
name="action_open_current_offer"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-file-text-o"
|
||||
invisible="not current_offer_letter_id">
|
||||
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="offer_release_status"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Offer Letter
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</button>
|
||||
|
||||
</xpath>
|
||||
|
||||
<!-- Offer Letter Page -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
|
||||
<page string="Offer Letters" name="offer_letters">
|
||||
|
||||
<group>
|
||||
|
||||
<group>
|
||||
<field name="finalized_ctc"/>
|
||||
<field name="current_offer_letter_id" readonly="1"/>
|
||||
<field name="offer_release_status" readonly="1"/>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<button
|
||||
name="action_open_current_offer"
|
||||
type="object"
|
||||
string="Open Current Offer"
|
||||
class="btn-primary"
|
||||
invisible="not current_offer_letter_id"/>
|
||||
</group>
|
||||
|
||||
</group>
|
||||
|
||||
<field name="offer_letter_ids" nolabel="1">
|
||||
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="main_candidate_name"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="salary"/>
|
||||
<field name="request_date"/>
|
||||
</list>
|
||||
|
||||
</field>
|
||||
|
||||
</page>
|
||||
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="hr_recruitment_stage_offer_request_form_extended">
|
||||
<field name="name">hr.recruitment.stage.form.offer.request.extended</field>
|
||||
<field name="model">hr.recruitment.stage</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_recruitment_stage_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='fold']" position="after">
|
||||
<field name="request_offer_release"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
from . import applicant_offer_mail_wizard
|
||||
from . import offer_release_request_wizard
|
||||
from . import offer_letter_reject_wizard
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
_name = 'applicant.offer.mail.wizard'
|
||||
_description = 'Applicant Offer Mail Wizard'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string='Applicant', required=True, readonly=True)
|
||||
bridge_id = fields.Many2one('recruitment.employee.bridge', string='Onboarding', required=True, readonly=True)
|
||||
offer_letter_id = fields.Many2one('offer.letter', string='Offer Letter', readonly=True)
|
||||
template_id = fields.Many2one('mail.template', string='Email Template', required=True, readonly=True)
|
||||
generated_attachment_id = fields.Many2one('ir.attachment', string='Generated Offer Attachment', readonly=True)
|
||||
|
|
@ -44,12 +44,14 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
offer_letter = self._get_offer_letter_from_context()
|
||||
applicant = offer_letter.candidate_id if offer_letter else self._get_applicant()
|
||||
bridge = offer_letter.bridge_id if offer_letter else self._get_bridge()
|
||||
if offer_letter and not bridge:
|
||||
raise UserError(_("Link this offer letter to an onboarding record before sending it."))
|
||||
template = self.env.ref('offer_letters.applicant_offer_email_template', raise_if_not_found=False)
|
||||
offer_letter = offer_letter or self._create_offer_letter(applicant)
|
||||
offer_letter = offer_letter or self._create_offer_letter(bridge)
|
||||
|
||||
defaults.update({
|
||||
'applicant_id': applicant.id,
|
||||
'bridge_id': bridge.id,
|
||||
'offer_letter_id': offer_letter.id,
|
||||
'position': offer_letter.position,
|
||||
'salary': offer_letter.salary,
|
||||
|
|
@ -64,11 +66,13 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
defaults.update(self._prepare_mail_defaults(template, offer_letter))
|
||||
return defaults
|
||||
|
||||
def _get_applicant(self):
|
||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
||||
if not applicant.exists():
|
||||
raise UserError(_("The applicant does not exist or is not accessible."))
|
||||
return applicant
|
||||
def _get_bridge(self):
|
||||
bridge = self.env['recruitment.employee.bridge'].browse(
|
||||
self.env.context.get('default_bridge_id') or self.env.context.get('active_id')
|
||||
)
|
||||
if not bridge.exists():
|
||||
raise UserError(_("The onboarding record does not exist or is not accessible."))
|
||||
return bridge
|
||||
|
||||
def _get_offer_letter_from_context(self):
|
||||
if self.env.context.get('active_model') != 'offer.letter':
|
||||
|
|
@ -78,29 +82,29 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
raise UserError(_("The offer letter does not exist or is not accessible."))
|
||||
return offer_letter
|
||||
|
||||
def _get_default_pay_structure(self, applicant):
|
||||
company = applicant.company_id or self.env.company
|
||||
def _get_default_pay_structure(self, bridge):
|
||||
company = bridge.company_id or self.env.company
|
||||
return self.env['hr.payroll.structure'].search([
|
||||
'|',
|
||||
('company_id', '=', company.id),
|
||||
('company_id', '=', False),
|
||||
], limit=1)
|
||||
|
||||
def _get_default_manager(self, applicant):
|
||||
return applicant.user_id.employee_id or self.env.user.employee_id
|
||||
def _get_default_manager(self, bridge):
|
||||
return self.env.user.employee_id
|
||||
|
||||
def _create_offer_letter(self, applicant):
|
||||
pay_structure = self._get_default_pay_structure(applicant)
|
||||
def _create_offer_letter(self, bridge):
|
||||
pay_structure = self._get_default_pay_structure(bridge)
|
||||
if not pay_structure:
|
||||
raise UserError(_("Please configure at least one salary structure before sending an offer."))
|
||||
|
||||
offer_letter = self.env['offer.letter'].create({
|
||||
'candidate_id': applicant.id,
|
||||
'position': applicant.job_id.name or applicant.hr_job_recruitment.job_id.name or applicant.partner_name or applicant.display_name,
|
||||
'salary': applicant.finalized_ctc or applicant.salary_expected or applicant.current_ctc or 0.0,
|
||||
'bridge_id': bridge.id,
|
||||
'position': bridge.job_id.name or bridge.employee_name or bridge.display_name,
|
||||
'salary': bridge.finalized_ctc if 'finalized_ctc' in bridge._fields else 0.0,
|
||||
'joining_date': fields.Date.today(),
|
||||
'pay_struct_id': pay_structure.id,
|
||||
'manager_id': self._get_default_manager(applicant).id,
|
||||
'manager_id': self._get_default_manager(bridge).id,
|
||||
})
|
||||
offer_letter.get_paydetailed_lines()
|
||||
return offer_letter
|
||||
|
|
@ -108,7 +112,7 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
def _update_offer_letter(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
'candidate_id': self.applicant_id.id,
|
||||
'bridge_id': self.bridge_id.id,
|
||||
'position': self.position,
|
||||
'salary': self.salary,
|
||||
'joining_date': self.joining_date,
|
||||
|
|
@ -125,7 +129,7 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
report = self.env.ref('offer_letters.hr_offer_letters_employee_print')
|
||||
from odoo import _ as translate
|
||||
pdf_content, _ = report.sudo()._render_qweb_pdf(report, offer_letter.id)
|
||||
attachment_name = translate('Offer Letter - %s.pdf') % (offer_letter.candidate_id.partner_name or offer_letter.name)
|
||||
attachment_name = translate('Offer Letter - %s.pdf') % (offer_letter.main_candidate_name or offer_letter.name)
|
||||
return self.env['ir.attachment'].create({
|
||||
'name': attachment_name,
|
||||
'datas': base64.b64encode(pdf_content),
|
||||
|
|
@ -159,8 +163,8 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
|||
attachment_ids = (other_attachments | attachment).ids if attachment else other_attachments.ids
|
||||
|
||||
return {
|
||||
'email_from': generated_values.get('email_from') or offer_letter.candidate_id.user_id.email or self.env.user.email,
|
||||
'email_to': generated_values.get('email_to') or offer_letter.candidate_id.email_from or '',
|
||||
'email_from': generated_values.get('email_from') or self.env.user.email,
|
||||
'email_to': generated_values.get('email_to') or offer_letter.recipient_email or '',
|
||||
'email_cc': generated_values.get('email_cc', ''),
|
||||
'email_subject': generated_values.get('subject', ''),
|
||||
'email_body': generated_values.get('body_html', ''),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<form string="Send Offer">
|
||||
<group>
|
||||
<field name="applicant_id" readonly="1"/>
|
||||
<field name="bridge_id" readonly="1"/>
|
||||
<field name="template_id" options="{'no_create': True}" readonly="1"/>
|
||||
<field name="generated_attachment_id" invisible="1"/>
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class OfferReleaseRequestWizard(models.TransientModel):
|
||||
_name = 'offer.release.request.wizard'
|
||||
_description = 'Offer Release Request Wizard'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string='Applicant', required=True, readonly=True)
|
||||
position = fields.Char(string='Position', readonly=True)
|
||||
finalized_ctc = fields.Float(string='Finalized CTC', required=True)
|
||||
email_from = fields.Char(string='Email From', required=True)
|
||||
email_to = fields.Char(string='Mail To', required=True)
|
||||
email_cc = fields.Text(string='Mail CC')
|
||||
email_subject = fields.Char(string='Subject', required=True)
|
||||
email_body = fields.Html(
|
||||
string='Mail Body',
|
||||
render_engine='qweb',
|
||||
render_options={'post_process': True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize='email_outgoing',
|
||||
required=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
applicant = self.env['hr.applicant'].browse(
|
||||
self.env.context.get('default_applicant_id') or self.env.context.get('active_id')
|
||||
)
|
||||
if not applicant.exists():
|
||||
raise UserError(_("The applicant does not exist or is not accessible."))
|
||||
|
||||
recruiter_name = applicant.user_id.name or self.env.user.name
|
||||
candidate_name = applicant.partner_name or applicant.candidate_id.partner_name or applicant.display_name
|
||||
position = applicant.job_id.name or applicant.hr_job_recruitment.job_id.name or applicant.display_name
|
||||
finalized_ctc = applicant.finalized_ctc or applicant.salary_expected or applicant.current_ctc or 0.0
|
||||
|
||||
defaults.update({
|
||||
'applicant_id': applicant.id,
|
||||
'position': position,
|
||||
'finalized_ctc': finalized_ctc,
|
||||
'email_from': self.env.user.email or self.env.company.email or '',
|
||||
'email_to': self._get_hr_email_to(),
|
||||
'email_subject': _('Offer Release Request - %s') % candidate_name,
|
||||
'email_body': (
|
||||
f"<p>Dear HR Team,</p>"
|
||||
f"<p>Please release the offer letter for <strong>{candidate_name}</strong>.</p>"
|
||||
f"<p><strong>Position:</strong> {position}<br/>"
|
||||
f"<strong>Finalized CTC:</strong> {finalized_ctc:.2f}<br/>"
|
||||
f"<strong>Requested By:</strong> {recruiter_name}</p>"
|
||||
f"<p>Please review the request and release the offer letter to the applicant.</p>"
|
||||
),
|
||||
})
|
||||
return defaults
|
||||
|
||||
def _get_hr_email_to(self):
|
||||
hr_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_hr_id')
|
||||
hr_manager_id = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False
|
||||
users = hr_manager_id
|
||||
if not hr_id:
|
||||
group = self.env.ref('hr.group_hr_manager')
|
||||
users = self.env['res.users'].sudo().search([
|
||||
('groups_id', 'in', group.ids),
|
||||
('email', '!=', False),
|
||||
])
|
||||
emails = users.mapped('email')
|
||||
return ','.join(emails) or self.env.company.email or ''
|
||||
|
||||
def _get_default_pay_structure(self, applicant):
|
||||
company = applicant.company_id or self.env.company
|
||||
return self.env['hr.payroll.structure'].sudo().search([
|
||||
], limit=1)
|
||||
|
||||
def _get_default_manager(self, applicant):
|
||||
return applicant.user_id.employee_id or self.env.user.employee_id
|
||||
|
||||
def action_submit_request(self):
|
||||
self.ensure_one()
|
||||
applicant = self.applicant_id
|
||||
pay_structure = self._get_default_pay_structure(applicant)
|
||||
if not pay_structure:
|
||||
raise UserError(_("Please configure at least one salary structure before requesting an offer release."))
|
||||
|
||||
applicant.finalized_ctc = self.finalized_ctc
|
||||
offer_letter = self.env['offer.letter'].create({
|
||||
'candidate_id': applicant.id,
|
||||
'position': self.position,
|
||||
'salary': self.finalized_ctc,
|
||||
'joining_date': fields.Date.today(),
|
||||
'pay_struct_id': pay_structure.id,
|
||||
'manager_id': self._get_default_manager(applicant).id,
|
||||
'requested_by_id': self.env.user.id,
|
||||
'request_date': fields.Datetime.now(),
|
||||
'state': 'requested',
|
||||
})
|
||||
offer_letter.get_paydetailed_lines()
|
||||
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
offer_url = f"{base_url}/web#id={offer_letter.id}&model=offer.letter&view_type=form"
|
||||
body_html = (
|
||||
f"{self.email_body}"
|
||||
f"<p><a href=\"{offer_url}\">Review Offer Letter Request</a></p>"
|
||||
)
|
||||
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'email_from': self.email_from,
|
||||
'email_to': self.email_to,
|
||||
'email_cc': self.email_cc,
|
||||
'subject': self.email_subject,
|
||||
'body_html': body_html,
|
||||
'auto_delete': False,
|
||||
'model': 'offer.letter',
|
||||
'res_id': offer_letter.id,
|
||||
})
|
||||
mail.sudo().send()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_offer_release_request_wizard_form" model="ir.ui.view">
|
||||
<field name="name">offer.release.request.wizard.form</field>
|
||||
<field name="model">offer.release.request.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Request Offer Release">
|
||||
<group>
|
||||
<field name="applicant_id" readonly="1"/>
|
||||
<field name="position" readonly="1"/>
|
||||
<field name="finalized_ctc"/>
|
||||
</group>
|
||||
<group string="Email Details">
|
||||
<field name="email_from" placeholder="Sender email"/>
|
||||
<field name="email_to" placeholder="Recipient email(s)"/>
|
||||
<field name="email_cc" placeholder="Comma-separated CC recipients"/>
|
||||
<field name="email_subject" placeholder="Email subject"/>
|
||||
</group>
|
||||
<group string="Mail Body">
|
||||
<field name="email_body" widget="html_mail" class="oe-bordered-editor"
|
||||
options="{'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_submit_request" type="object" string="Submit Request" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import wizards
|
||||
from .hooks import post_init_hook
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Recruitment Onboarding",
|
||||
"summary": "Bridge recruitment, JOD and employee onboarding without direct links",
|
||||
"description": """
|
||||
Keeps recruitment and employee records separated by routing onboarding,
|
||||
JOD, validation and employee creation through a dedicated bridge master.
|
||||
""",
|
||||
"author": "FTPROTECH",
|
||||
"website": "https://www.ftprotech.com",
|
||||
"category": "Human Resources",
|
||||
"version": "1.0",
|
||||
"depends": [
|
||||
"employee_bridge",
|
||||
"offer_letters",
|
||||
"hr_employee_extended",
|
||||
"hr_recruitment_extended",
|
||||
"hr_recruitment",
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"data.xml",
|
||||
"views/stages.xml",
|
||||
"views/recruitment_employee_bridge_views.xml",
|
||||
"views/hr_applicant_views.xml",
|
||||
"views/hr_candidate_views.xml",
|
||||
"views/hr_applicant_offer_views.xml",
|
||||
"wizards/offer_release_request_wizard.xml",
|
||||
],
|
||||
"license": "LGPL-3",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"post_init_hook": "post_init_hook",
|
||||
}
|
||||
|
|
@ -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">Recruitment Onboarding</field>
|
||||
<field name="code">recruitment.employee.bridge</field>
|
||||
<field name="prefix">REB/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
|
||||
def post_init_hook(env):
|
||||
"""Backfill bridge records for already hired recruitment records."""
|
||||
env = api.Environment(env.cr, SUPERUSER_ID, {})
|
||||
Bridge = env["recruitment.employee.bridge"].sudo().with_context(active_test=False)
|
||||
Applicant = env["hr.applicant"].sudo().with_context(active_test=False)
|
||||
Candidate = env["hr.candidate"].sudo().with_context(active_test=False)
|
||||
|
||||
def _bridge_vals(applicant=False, candidate=False, employee=False):
|
||||
company = (
|
||||
(employee and employee.company_id)
|
||||
or (applicant and applicant.company_id)
|
||||
or (candidate and candidate.company_id)
|
||||
or env.company
|
||||
)
|
||||
return {
|
||||
"applicant_id": applicant.id if applicant else False,
|
||||
"candidate_id": candidate.id if candidate else False,
|
||||
"employee_id": employee.id if employee else False,
|
||||
"company_id": company.id,
|
||||
"source": "recruitment",
|
||||
"state": "draft",
|
||||
}
|
||||
|
||||
def _sync_employee_applicant(employee, applicant):
|
||||
if employee and applicant and "applicant_id" in employee._fields and not employee.applicant_id:
|
||||
employee.write({"applicant_id": applicant.id})
|
||||
|
||||
def _get_or_create_bridge(applicant=False, candidate=False, employee=False):
|
||||
bridge = Bridge
|
||||
if employee:
|
||||
bridge = Bridge.search([("employee_id", "=", employee.id)], limit=1, order="id desc")
|
||||
if not bridge and applicant:
|
||||
bridge = Bridge.search([("applicant_id", "=", applicant.id)], limit=1, order="id desc")
|
||||
if not bridge and candidate:
|
||||
bridge = Bridge.search([("candidate_id", "=", candidate.id)], limit=1, order="id desc")
|
||||
|
||||
vals = _bridge_vals(applicant, candidate, employee)
|
||||
if bridge:
|
||||
write_vals = {}
|
||||
for field_name, value in vals.items():
|
||||
if value and not bridge[field_name]:
|
||||
write_vals[field_name] = value
|
||||
if write_vals:
|
||||
bridge.write(write_vals)
|
||||
return bridge
|
||||
|
||||
return Bridge.create(vals)
|
||||
|
||||
applicants = Applicant.search([("employee_id", "!=", False)], order="id")
|
||||
for applicant in applicants:
|
||||
candidate = applicant.candidate_id
|
||||
employee = applicant.employee_id
|
||||
if not candidate or not employee:
|
||||
continue
|
||||
bridge = _get_or_create_bridge(applicant=applicant, candidate=candidate, employee=employee)
|
||||
if candidate.employee_id != employee:
|
||||
candidate.write({"employee_id": employee.id})
|
||||
_sync_employee_applicant(employee, bridge.applicant_id)
|
||||
|
||||
candidates = Candidate.search([("employee_id", "!=", False)], order="id")
|
||||
for candidate in candidates:
|
||||
employee = candidate.employee_id
|
||||
applicant = candidate.applicant_ids[:1]
|
||||
bridge = _get_or_create_bridge(applicant=applicant, candidate=candidate, employee=employee)
|
||||
_sync_employee_applicant(employee, bridge.applicant_id)
|
||||
|
||||
env.cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'offer_letter'
|
||||
AND column_name IN ('bridge_id', 'candidate_id', 'main_candidate_id', 'salary')
|
||||
""")
|
||||
offer_columns = {row[0] for row in env.cr.fetchall()}
|
||||
if "bridge_id" in offer_columns and "candidate_id" in offer_columns:
|
||||
env.cr.execute("""
|
||||
UPDATE offer_letter offer
|
||||
SET bridge_id = bridge.id
|
||||
FROM recruitment_employee_bridge bridge
|
||||
WHERE offer.bridge_id IS NULL
|
||||
AND offer.candidate_id IS NOT NULL
|
||||
AND bridge.applicant_id = offer.candidate_id
|
||||
""")
|
||||
if "bridge_id" in offer_columns and "main_candidate_id" in offer_columns:
|
||||
env.cr.execute("""
|
||||
UPDATE offer_letter offer
|
||||
SET bridge_id = bridge.id
|
||||
FROM recruitment_employee_bridge bridge
|
||||
WHERE offer.bridge_id IS NULL
|
||||
AND offer.main_candidate_id IS NOT NULL
|
||||
AND bridge.candidate_id = offer.main_candidate_id
|
||||
""")
|
||||
if {"bridge_id", "salary"}.issubset(offer_columns):
|
||||
env.cr.execute("""
|
||||
UPDATE recruitment_employee_bridge bridge
|
||||
SET finalized_ctc = offer.salary
|
||||
FROM offer_letter offer
|
||||
WHERE offer.bridge_id = bridge.id
|
||||
AND COALESCE(bridge.finalized_ctc, 0) = 0
|
||||
AND COALESCE(offer.salary, 0) != 0
|
||||
""")
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from . import recruitment_employee_bridge
|
||||
from . import hr_applicant
|
||||
from . import hr_candidate
|
||||
from . import hr_employee
|
||||
from . import offer_recruitment
|
||||
from . import stages
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrApplicant(models.Model):
|
||||
_inherit = "hr.applicant"
|
||||
|
||||
bridge_ids = fields.One2many("recruitment.employee.bridge", "applicant_id", string="Onboarding Records")
|
||||
bridge_count = fields.Integer(compute="_compute_bridge_count")
|
||||
bridge_employee_id = fields.Many2one(
|
||||
"hr.employee",
|
||||
compute="_compute_bridge_employee",
|
||||
string="Onboarding Employee",
|
||||
)
|
||||
bridge_employee_name = fields.Char(related="bridge_employee_id.name", string="Employee")
|
||||
bridge_employee_code = fields.Char(related="bridge_employee_id.employee_id", string="Employee Code")
|
||||
|
||||
def _compute_bridge_count(self):
|
||||
data = self.env["recruitment.employee.bridge"].read_group(
|
||||
[("applicant_id", "in", self.ids), ("active", "=", True)],
|
||||
["applicant_id"],
|
||||
["applicant_id"],
|
||||
)
|
||||
counts = {item["applicant_id"][0]: item["applicant_id_count"] for item in data}
|
||||
for applicant in self:
|
||||
applicant.bridge_count = counts.get(applicant.id, 0)
|
||||
|
||||
def _compute_bridge_employee(self):
|
||||
bridges = self.env["recruitment.employee.bridge"].search(
|
||||
[("applicant_id", "in", self.ids), ("active", "=", True)],
|
||||
order="id desc",
|
||||
)
|
||||
bridge_by_applicant = {}
|
||||
for bridge in bridges:
|
||||
bridge_by_applicant.setdefault(bridge.applicant_id.id, bridge)
|
||||
for applicant in self:
|
||||
bridge = bridge_by_applicant.get(applicant.id)
|
||||
applicant.bridge_employee_id = bridge.employee_id if bridge else False
|
||||
|
||||
def _get_bridge(self, create=False, employee=False, source="recruitment"):
|
||||
self.ensure_one()
|
||||
vals = self._get_recruitment_bridge_vals(source=source)
|
||||
|
||||
bridge = self.env["recruitment.employee.bridge"].search(
|
||||
['|','|',('work_email','=',vals['work_email']),('work_phone','=',vals['work_phone']),("applicant_id", "=", self.id), ("active", "=", True)],
|
||||
limit=1,
|
||||
order="id desc",
|
||||
)
|
||||
if not bridge and create:
|
||||
vals.update({
|
||||
"applicant_id": self.id,
|
||||
"candidate_id": self.candidate_id.id,
|
||||
})
|
||||
if employee:
|
||||
vals.update({
|
||||
"employee_id": employee.id,
|
||||
"company_id": employee.company_id.id or vals["company_id"],
|
||||
"state": "draft",
|
||||
})
|
||||
bridge = self.env["recruitment.employee.bridge"].create(vals)
|
||||
return bridge
|
||||
|
||||
def _get_recruitment_bridge_vals(self, source="recruitment"):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"employee_name": self.partner_name or self.candidate_id.partner_name,
|
||||
"work_email": self.email_from or self.candidate_id.email_from,
|
||||
"work_phone": self.partner_phone or self.candidate_id.partner_phone,
|
||||
"job_id": self.job_id.id,
|
||||
"company_id": self.company_id.id or self.candidate_id.company_id.id or self.env.company.id,
|
||||
"source": source,
|
||||
"state": "draft",
|
||||
"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_no": self.passport_no,
|
||||
"passport_start_date": self.passport_start_date,
|
||||
"passport_end_date": self.passport_end_date,
|
||||
"passport_issued_location": self.passport_issued_location,
|
||||
}
|
||||
|
||||
def _get_bridge_employee_required(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_bridge()
|
||||
if not bridge or not bridge.employee_id:
|
||||
raise ValidationError(_("Create or select the bridge employee before validating this section."))
|
||||
return bridge.employee_id
|
||||
|
||||
def create_employee_from_applicant(self):
|
||||
self.ensure_one()
|
||||
self.candidate_id._check_interviewer_access()
|
||||
bridge = self._get_bridge(create=True, source="recruitment")
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
||||
def action_open_bridge_employee(self):
|
||||
self.ensure_one()
|
||||
employee = self._get_bridge_employee_required()
|
||||
return {
|
||||
"name": _("Employee"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.employee",
|
||||
"view_mode": "form",
|
||||
"res_id": employee.id,
|
||||
}
|
||||
|
||||
def action_open_bridge(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_bridge()
|
||||
if not bridge:
|
||||
raise ValidationError(_("No bridge found for this applicant."))
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
||||
def action_validate_personal_details(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
vals = {
|
||||
"doj": rec.doj,
|
||||
"gender": rec.gender,
|
||||
"birthday": rec.birthday,
|
||||
"blood_group": rec.blood_group,
|
||||
"marital": rec.marital,
|
||||
"marriage_anniversary_date": rec.marriage_anniversary_date,
|
||||
"image_1920": rec.candidate_image,
|
||||
}
|
||||
vals = {key: value for key, value in vals.items() if value not in ["", False, 0]}
|
||||
if not vals:
|
||||
raise ValidationError(_("No values to validate"))
|
||||
employee.write(vals)
|
||||
rec.personal_details_status = "validated"
|
||||
|
||||
def action_validate_contact_details(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
vals = {
|
||||
"private_street": rec.private_street,
|
||||
"private_street2": rec.private_street2,
|
||||
"private_city": rec.private_city,
|
||||
"private_state_id": rec.private_state_id.id,
|
||||
"private_zip": rec.private_zip,
|
||||
"private_country_id": rec.private_country_id.id,
|
||||
"permanent_street": rec.permanent_street,
|
||||
"permanent_street2": rec.permanent_street2,
|
||||
"permanent_city": rec.permanent_city,
|
||||
"permanent_state_id": rec.permanent_state_id.id,
|
||||
"permanent_zip": rec.permanent_zip,
|
||||
"permanent_country_id": rec.permanent_country_id.id,
|
||||
}
|
||||
vals = {key: value for key, value in vals.items() if value not in ["", False, 0]}
|
||||
if not vals:
|
||||
raise ValidationError(_("No values to validate"))
|
||||
employee.write(vals)
|
||||
rec.contact_details_status = "validated"
|
||||
|
||||
def action_validate_bank_details(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
if not all([rec.full_name_as_in_bank, rec.bank_name, rec.bank_branch, rec.bank_account_no, rec.bank_ifsc_code]):
|
||||
raise ValidationError(_("Please Provide all the Bank Related Details"))
|
||||
account_no = self.env["res.partner.bank"].sudo().search([("acc_number", "=", rec.bank_account_no)], limit=1)
|
||||
if account_no:
|
||||
employee.bank_account_id = account_no.id
|
||||
else:
|
||||
bank = self.env["res.bank"].sudo().search([("bic", "=", rec.bank_ifsc_code)], limit=1)
|
||||
if not bank:
|
||||
bank = self.env["res.bank"].sudo().create({
|
||||
"name": rec.bank_name,
|
||||
"bic": rec.bank_ifsc_code,
|
||||
"branch": rec.bank_branch,
|
||||
})
|
||||
partner = employee.work_contact_id or employee.user_id.partner_id
|
||||
partner_bank = rec.env["res.partner.bank"].sudo().create({
|
||||
"acc_number": rec.bank_account_no,
|
||||
"bank_id": bank.id,
|
||||
"full_name": rec.full_name_as_in_bank,
|
||||
"partner_id": partner.id,
|
||||
})
|
||||
employee.bank_account_id = partner_bank.id
|
||||
rec.bank_details_status = "validated"
|
||||
|
||||
def action_validate_passport_details(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
vals = {
|
||||
"passport_id": rec.passport_no,
|
||||
"passport_start_date": rec.passport_start_date,
|
||||
"passport_end_date": rec.passport_end_date,
|
||||
"passport_issued_location": rec.passport_issued_location,
|
||||
}
|
||||
vals = {key: value for key, value in vals.items() if value not in ["", False, 0]}
|
||||
if not vals:
|
||||
raise ValidationError(_("No values to validate"))
|
||||
employee.write(vals)
|
||||
rec.passport_details_status = "validated"
|
||||
|
||||
def action_validate_authentication_details(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
vals = {
|
||||
"pan_no": rec.pan_no,
|
||||
"identification_id": rec.identification_id,
|
||||
"previous_company_pf_no": rec.previous_company_pf_no,
|
||||
"previous_company_uan_no": rec.previous_company_uan_no,
|
||||
}
|
||||
vals = {key: value for key, value in vals.items() if value not in ["", False, 0]}
|
||||
if not vals:
|
||||
raise ValidationError(_("No values to validate"))
|
||||
employee.write(vals)
|
||||
rec.authentication_details_status = "validated"
|
||||
|
||||
def action_validate_family_education_employer_details(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
if not (rec.education_history or rec.employer_history or rec.family_details):
|
||||
raise ValidationError(_("No Data to Validate"))
|
||||
rec.education_history.write({"employee_id": employee.id})
|
||||
rec.employer_history.write({"employee_id": employee.id})
|
||||
rec.family_details.write({"employee_id": employee.id})
|
||||
rec.family_education_employer_details_status = "validated"
|
||||
bridge = rec._get_bridge()
|
||||
if bridge:
|
||||
bridge.state = "validated"
|
||||
|
||||
def action_validate_attachments(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
if not rec.joining_attachment_ids:
|
||||
raise ValidationError(_("No Data to Validate"))
|
||||
rec.joining_attachment_ids.write({"employee_id": employee.id})
|
||||
rec.attachments_validation_status = "validated"
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
for rec in self:
|
||||
employee = rec._get_bridge_employee_required()
|
||||
if not employee.employee_id:
|
||||
raise ValidationError(_("Employee Code for the Employee (%s) is missing") % employee.name)
|
||||
bridge = rec._get_bridge()
|
||||
if bridge:
|
||||
return bridge.action_send_jod_form()
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
|
||||
bridge_ids = fields.One2many("recruitment.employee.bridge", "candidate_id", string="Onboarding Records")
|
||||
bridge_count = fields.Integer(compute="_compute_bridge_count")
|
||||
bridge_employee_id = fields.Many2one(
|
||||
"hr.employee",
|
||||
compute="_compute_bridge_employee",
|
||||
string="Onboarding Employee",
|
||||
)
|
||||
bridge_employee_name = fields.Char(related="bridge_employee_id.name", string="Employee")
|
||||
|
||||
def _compute_bridge_count(self):
|
||||
data = self.env["recruitment.employee.bridge"].read_group(
|
||||
[("candidate_id", "in", self.ids), ("active", "=", True)],
|
||||
["candidate_id"],
|
||||
["candidate_id"],
|
||||
)
|
||||
counts = {item["candidate_id"][0]: item["candidate_id_count"] for item in data}
|
||||
for candidate in self:
|
||||
candidate.bridge_count = counts.get(candidate.id, 0)
|
||||
|
||||
def _compute_bridge_employee(self):
|
||||
bridges = self.env["recruitment.employee.bridge"].search(
|
||||
[("candidate_id", "in", self.ids), ("active", "=", True)],
|
||||
order="id desc",
|
||||
)
|
||||
bridge_by_candidate = {}
|
||||
for bridge in bridges:
|
||||
bridge_by_candidate.setdefault(bridge.candidate_id.id, bridge)
|
||||
for candidate in self:
|
||||
candidate.bridge_employee_id = bridge_by_candidate.get(candidate.id).employee_id if bridge_by_candidate.get(candidate.id) else False
|
||||
|
||||
def _get_bridge_employee_create_vals(self):
|
||||
self.ensure_one()
|
||||
vals = dict(self._get_employee_create_vals())
|
||||
vals.pop("candidate_id", None)
|
||||
return vals
|
||||
|
||||
def create_employee_from_candidate(self):
|
||||
self.ensure_one()
|
||||
self._check_interviewer_access()
|
||||
|
||||
bridge = self.env["recruitment.employee.bridge"].search(
|
||||
[("candidate_id", "=", self.id), ("active", "=", True)],
|
||||
limit=1,
|
||||
order="id desc",
|
||||
)
|
||||
if bridge:
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
||||
bridge = self.env["recruitment.employee.bridge"].create({
|
||||
"candidate_id": self.id,
|
||||
"employee_name": self.partner_name,
|
||||
"work_email": self.email_from,
|
||||
"work_phone": self.partner_phone,
|
||||
"company_id": self.company_id.id or self.env.company.id,
|
||||
"source": "recruitment",
|
||||
"state": "draft",
|
||||
})
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
||||
def action_open_bridge_employee(self):
|
||||
self.ensure_one()
|
||||
bridge = self.env["recruitment.employee.bridge"].search(
|
||||
[("candidate_id", "=", self.id), ("active", "=", True)],
|
||||
limit=1,
|
||||
order="id desc",
|
||||
)
|
||||
if not bridge:
|
||||
raise UserError(_("No bridge found."))
|
||||
return {
|
||||
"name": _("Onboarding"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "recruitment.employee.bridge",
|
||||
"view_mode": "form",
|
||||
"res_id": bridge.id,
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
def _find_existing_recruitment_records_for_bridge(self):
|
||||
self.ensure_one()
|
||||
applicant = self.env["hr.applicant"].sudo()
|
||||
candidate = self.env["hr.candidate"].sudo()
|
||||
|
||||
contact_domain = []
|
||||
if self.work_phone:
|
||||
contact_domain.append(("partner_phone", "=", self.work_phone))
|
||||
if self.work_email:
|
||||
contact_domain.append(("email_from", "=", self.work_email))
|
||||
if not contact_domain:
|
||||
return applicant, candidate
|
||||
|
||||
domain = contact_domain[0] if len(contact_domain) == 1 else ["|"] + contact_domain
|
||||
applicant = applicant.search(domain, limit=1, order="id desc")
|
||||
if applicant:
|
||||
return applicant, applicant.candidate_id
|
||||
|
||||
candidate = candidate.search(domain, limit=1, order="id desc")
|
||||
return candidate.applicant_ids[:1], candidate
|
||||
|
||||
def _get_or_create_employee_bridge(self):
|
||||
bridge = super()._get_or_create_employee_bridge()
|
||||
for employee in self:
|
||||
if bridge.applicant_id or bridge.candidate_id:
|
||||
continue
|
||||
applicant, candidate = employee._find_existing_recruitment_records_for_bridge()
|
||||
vals = {}
|
||||
if applicant:
|
||||
vals["applicant_id"] = applicant.id
|
||||
vals["candidate_id"] = applicant.candidate_id.id
|
||||
elif candidate:
|
||||
vals["candidate_id"] = candidate.id
|
||||
if vals:
|
||||
bridge.write(vals)
|
||||
return bridge
|
||||
|
||||
def send_jod_form_to_employee(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_or_create_employee_bridge()
|
||||
return bridge.action_send_jod_form()
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrApplicant(models.Model):
|
||||
_inherit = "hr.applicant"
|
||||
|
||||
finalized_ctc = fields.Float(string="Finalized CTC", tracking=True)
|
||||
offer_letter_ids = fields.Many2many(
|
||||
"offer.letter",
|
||||
string="Offer Letters",
|
||||
compute="_compute_offer_letters",
|
||||
)
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
"offer.letter",
|
||||
string="Current Offer Letter",
|
||||
compute="_compute_offer_letters",
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
("requested", "Requested"),
|
||||
("sent", "Sent to Applicant"),
|
||||
("accepted", "Accepted"),
|
||||
("rejected", "Rejected"),
|
||||
("expired", "Expired"),
|
||||
],
|
||||
string="Offer Status",
|
||||
related="current_offer_letter_id.state",
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
request_offer_release = fields.Boolean(related="recruitment_stage_id.request_offer_release")
|
||||
|
||||
@api.depends("bridge_ids.offer_letter_ids", "bridge_ids.offer_letter_ids.create_date", "bridge_ids.offer_letter_ids.state")
|
||||
def _compute_offer_letters(self):
|
||||
for applicant in self:
|
||||
offers = applicant.bridge_ids.mapped("offer_letter_ids").sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string("1970-01-01 00:00:00"), offer.id)
|
||||
)
|
||||
applicant.offer_letter_ids = offers
|
||||
applicant.current_offer_letter_id = offers[-1] if offers else False
|
||||
|
||||
def action_request_offer_release(self):
|
||||
self.ensure_one()
|
||||
bridge = self._get_bridge(create=True, source="recruitment")
|
||||
if self.finalized_ctc and "finalized_ctc" in bridge._fields:
|
||||
bridge.finalized_ctc = self.finalized_ctc
|
||||
return bridge.action_request_offer_release()
|
||||
|
||||
def action_send_offer(self):
|
||||
return self.action_request_offer_release()
|
||||
|
||||
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
"offer.letter",
|
||||
string="Current Offer Letter",
|
||||
compute="_compute_current_offer_letter",
|
||||
store=False,
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
("requested", "Requested"),
|
||||
("sent", "Sent to Applicant"),
|
||||
("accepted", "Accepted"),
|
||||
("rejected", "Rejected"),
|
||||
("expired", "Expired"),
|
||||
],
|
||||
string="Offer Status",
|
||||
related="current_offer_letter_id.state",
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends("bridge_ids.offer_letter_ids", "bridge_ids.offer_letter_ids.create_date", "bridge_ids.offer_letter_ids.state")
|
||||
def _compute_current_offer_letter(self):
|
||||
for candidate in self:
|
||||
offer_letters = candidate.bridge_ids.mapped("offer_letter_ids").sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string("1970-01-01 00:00:00"), offer.id)
|
||||
)
|
||||
candidate.current_offer_letter_id = offer_letters[-1] if offer_letters else False
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class RecruitmentEmployeeBridge(models.Model):
|
||||
_inherit = "recruitment.employee.bridge"
|
||||
|
||||
applicant_id = fields.Many2one(
|
||||
"hr.applicant",
|
||||
string="Applicant",
|
||||
ondelete="restrict",
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
candidate_id = fields.Many2one(
|
||||
"hr.candidate",
|
||||
string="Candidate",
|
||||
ondelete="restrict",
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
applicant_email = fields.Char(related="applicant_id.email_from", string="Applicant Email")
|
||||
applicant_phone = fields.Char(related="applicant_id.partner_phone", string="Applicant Phone")
|
||||
personal_details_status = fields.Selection(related="applicant_id.personal_details_status", readonly=True)
|
||||
contact_details_status = fields.Selection(related="applicant_id.contact_details_status", readonly=True)
|
||||
bank_details_status = fields.Selection(related="applicant_id.bank_details_status", readonly=True)
|
||||
passport_details_status = fields.Selection(related="applicant_id.passport_details_status", readonly=True)
|
||||
authentication_details_status = fields.Selection(related="applicant_id.authentication_details_status", readonly=True)
|
||||
family_education_employer_details_status = fields.Selection(
|
||||
related="applicant_id.family_education_employer_details_status",
|
||||
readonly=True,
|
||||
)
|
||||
attachments_validation_status = fields.Selection(related="applicant_id.attachments_validation_status", readonly=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("applicant_id") and not vals.get("candidate_id"):
|
||||
applicant = self.env["hr.applicant"].browse(vals["applicant_id"])
|
||||
vals["candidate_id"] = applicant.candidate_id.id
|
||||
if vals.get("candidate_id") and not vals.get("employee_id"):
|
||||
candidate = self.env["hr.candidate"].browse(vals["candidate_id"])
|
||||
vals["employee_id"] = candidate.employee_id.id
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_open_applicant(self):
|
||||
self.ensure_one()
|
||||
if not self.applicant_id:
|
||||
raise ValidationError(_("No applicant is linked to this bridge."))
|
||||
return {
|
||||
"name": _("Applicant"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.applicant",
|
||||
"view_mode": "form",
|
||||
"res_id": self.applicant_id.id,
|
||||
}
|
||||
|
||||
def action_open_candidate(self):
|
||||
self.ensure_one()
|
||||
if not self.candidate_id:
|
||||
raise ValidationError(_("No candidate is linked to this bridge."))
|
||||
return {
|
||||
"name": _("Candidate"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.candidate",
|
||||
"view_mode": "form",
|
||||
"res_id": self.candidate_id.id,
|
||||
}
|
||||
|
||||
def _get_applicant_required(self):
|
||||
self.ensure_one()
|
||||
if not self.applicant_id:
|
||||
raise ValidationError(_("No applicant is linked to this bridge."))
|
||||
return self.applicant_id
|
||||
|
||||
def action_validate_personal_details(self):
|
||||
return self._get_applicant_required().action_validate_personal_details()
|
||||
|
||||
def action_validate_contact_details(self):
|
||||
return self._get_applicant_required().action_validate_contact_details()
|
||||
|
||||
def action_validate_bank_details(self):
|
||||
return self._get_applicant_required().action_validate_bank_details()
|
||||
|
||||
def action_validate_passport_details(self):
|
||||
return self._get_applicant_required().action_validate_passport_details()
|
||||
|
||||
def action_validate_authentication_details(self):
|
||||
return self._get_applicant_required().action_validate_authentication_details()
|
||||
|
||||
def action_validate_family_education_employer_details(self):
|
||||
return self._get_applicant_required().action_validate_family_education_employer_details()
|
||||
|
||||
def action_validate_attachments(self):
|
||||
return self._get_applicant_required().action_validate_attachments()
|
||||
|
||||
def action_validate_employee_details(self):
|
||||
res = super().action_validate_employee_details()
|
||||
for bridge in self.filtered(lambda item: item.employee_id and item.applicant_id and item.applicant_id.candidate_image):
|
||||
bridge.employee_id.image_1920 = bridge.applicant_id.candidate_image
|
||||
return res
|
||||
|
||||
def action_request_offer_release(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Request Offer Release"),
|
||||
"res_model": "offer.release.request.wizard",
|
||||
"view_mode": "form",
|
||||
"view_id": self.env.ref("recruitment_employee_bridge.view_offer_release_request_wizard_form").id,
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_bridge_id": self.id,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class RecruitmentStage(models.Model):
|
||||
_inherit = 'hr.recruitment.stage'
|
||||
_inherit = "hr.recruitment.stage"
|
||||
|
||||
request_offer_release = fields.Boolean(string="Request Offer Release")
|
||||
request_offer_release = fields.Boolean(string="Request Offer Release")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_recruitment_employee_bridge_user,recruitment.employee.bridge.user,model_recruitment_employee_bridge,base.group_user,1,1,1,0
|
||||
access_recruitment_employee_bridge_manager,recruitment.employee.bridge.manager,model_recruitment_employee_bridge,hr.group_hr_manager,1,1,1,1
|
||||
access_offer_release_request_wizard,offer.release.request.wizard.user,model_offer_release_request_wizard,base.group_user,1,1,1,1
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_applicant_view_form_offer_letters_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.offer.letters.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -35,8 +36,8 @@
|
|||
<field name="current_offer_letter_id" readonly="1"/>
|
||||
<field name="offer_release_status" widget="badge" readonly="1"/>
|
||||
</group>
|
||||
<field name="offer_letter_ids" nolabel="1">
|
||||
<list>
|
||||
<field name="offer_letter_ids" nolabel="1" options="{'no_create': True, 'no_open': True}">
|
||||
<list create="0" delete="0" no_open="True">
|
||||
<field name="name"/>
|
||||
<field name="requested_by_id"/>
|
||||
<field name="request_date"/>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="hr_applicant_view_form_bridge" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.bridge</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='employee_id']" position="after">
|
||||
<field name="bridge_employee_id" invisible="1"/>
|
||||
<field name="bridge_count" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='create_employee_from_applicant']" position="attributes">
|
||||
<attribute name="invisible">bridge_count or not active or not date_closed or is_on_hold</attribute>
|
||||
<attribute name="string">Create Onboarding</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_open_employee']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_bridge_employee"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-id-card-o"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not bridge_employee_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="bridge_employee_name" readonly="1"/></span>
|
||||
<span class="o_stat_text">Onboarding Employee</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_open_bridge"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-link"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not bridge_count">
|
||||
<field name="bridge_count" widget="statinfo" string="Onboarding"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='recruitment_stage_id']" position="attributes">
|
||||
<attribute name="invisible">not active and not bridge_employee_id</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='Attachments']//button[@name='action_validate_attachments']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_applicant_additional_info_bridge" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.additional.info.bridge</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_additional_info_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_validate_personal_details'][contains(@class, 'btn-success')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_personal_details'][contains(@class, 'btn-danger')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_contact_details'][contains(@class, 'btn-success')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_contact_details'][contains(@class, 'btn-danger')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_bank_details'][contains(@class, 'btn-success')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_bank_details'][contains(@class, 'btn-danger')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_passport_details'][contains(@class, 'btn-success')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_passport_details'][contains(@class, 'btn-danger')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_authentication_details'][contains(@class, 'btn-success')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_authentication_details'][contains(@class, 'btn-danger')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_family_education_employer_details'][contains(@class, 'btn-success')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_validate_family_education_employer_details'][contains(@class, 'btn-danger')]" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='additional_info']" position="attributes">
|
||||
<attribute name="invisible">not bridge_employee_id</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="hr_candidate_view_form_bridge" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.form.bridge</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='employee_id']" position="after">
|
||||
<field name="bridge_employee_id" invisible="1"/>
|
||||
<field name="bridge_count" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='create_employee_from_candidate']" position="attributes">
|
||||
<attribute name="invisible">bridge_count or not active</attribute>
|
||||
<attribute name="string">Create Onboarding</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_open_employee']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_bridge_employee"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-link"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not bridge_count">
|
||||
<field name="bridge_count" widget="statinfo" string="Onboarding"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_recruitment_employee_bridge_list" model="ir.ui.view">
|
||||
<field name="name">recruitment.employee.bridge.list</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_code"/>
|
||||
<field name="joining_form_link" optional="show"/>
|
||||
<field name="applicant_id"/>
|
||||
<field name="candidate_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="source"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_recruitment_employee_bridge_form" model="ir.ui.view">
|
||||
<field name="name">recruitment.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-secondary" invisible="state == 'validated'"/>
|
||||
<button string="Create Employee" name="action_create_employee" type="object" class="btn-secondary" invisible="employee_id"/>
|
||||
<button string="Validate Into Employee" name="action_validate_employee_details" type="object" class="btn-primary" invisible="state == 'validated' or not employee_id"/>
|
||||
<button string="Request Offer Release" name="action_request_offer_release" type="object" class="btn-primary" invisible="state != 'validated'"/>
|
||||
<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_applicant" type="object" class="oe_stat_button" icon="fa-pencil" invisible="not applicant_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Applicant</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_open_candidate" type="object" class="oe_stat_button" icon="fa-user" invisible="not candidate_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Candidate</span>
|
||||
</div>
|
||||
</button>
|
||||
<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>
|
||||
<button name="action_open_current_offer" type="object" class="oe_stat_button" icon="fa-envelope-open-o" invisible="not current_offer_letter_id">
|
||||
<field name="offer_release_status" widget="statinfo" string="Offer"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<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"/>
|
||||
<field name="finalized_ctc"/>
|
||||
<field name="current_offer_letter_id" readonly="1"/>
|
||||
<field name="offer_release_status" widget="badge" readonly="1"/>
|
||||
<field name="applicant_id"/>
|
||||
<field name="candidate_id"/>
|
||||
<field name="job_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Validation" name="validation">
|
||||
<group string="Personal Details">
|
||||
<group>
|
||||
<field name="personal_details_status" widget="badge"/>
|
||||
<field name="doj"/>
|
||||
<field name="gender"/>
|
||||
<field name="birthday"/>
|
||||
<field name="blood_group"/>
|
||||
<field name="marital"/>
|
||||
<field name="marriage_anniversary_date" invisible="marital == 'single'"/>
|
||||
</group>
|
||||
<group>
|
||||
<button string="Validate Personal Details" name="action_validate_personal_details" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Contact Details">
|
||||
<group>
|
||||
<field name="contact_details_status" widget="badge"/>
|
||||
<label for="private_street" string="Current Address"/>
|
||||
<div class="o_address_format">
|
||||
<field name="private_street" placeholder="Street..." class="o_address_street"/>
|
||||
<field name="private_street2" placeholder="Street 2..." class="o_address_street"/>
|
||||
<field name="private_city" placeholder="City" class="o_address_city"/>
|
||||
<field name="private_state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}" context="{'default_country_id': private_country_id}"/>
|
||||
<field name="private_zip" placeholder="ZIP" class="o_address_zip"/>
|
||||
<field name="private_country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<button string="Validate Contact Details" name="action_validate_contact_details" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
<label for="permanent_street" string="Permanent Address"/>
|
||||
<div class="o_address_format">
|
||||
<field name="permanent_street" placeholder="Street..." class="o_address_street"/>
|
||||
<field name="permanent_street2" placeholder="Street 2..." class="o_address_street"/>
|
||||
<field name="permanent_city" placeholder="City" class="o_address_city"/>
|
||||
<field name="permanent_state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}" context="{'default_country_id': permanent_country_id}"/>
|
||||
<field name="permanent_zip" placeholder="ZIP" class="o_address_zip"/>
|
||||
<field name="permanent_country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Bank Details">
|
||||
<group>
|
||||
<field name="bank_details_status" widget="badge"/>
|
||||
<field name="full_name_as_in_bank"/>
|
||||
<field name="bank_name"/>
|
||||
<field name="bank_branch"/>
|
||||
<field name="bank_account_no"/>
|
||||
<field name="bank_ifsc_code"/>
|
||||
</group>
|
||||
<group>
|
||||
<button string="Validate Bank Details" name="action_validate_bank_details" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Passport Details">
|
||||
<group>
|
||||
<field name="passport_details_status" widget="badge"/>
|
||||
<field name="passport_no"/>
|
||||
<field name="passport_start_date"/>
|
||||
<field name="passport_end_date"/>
|
||||
<field name="passport_issued_location"/>
|
||||
</group>
|
||||
<group>
|
||||
<button string="Validate Passport Details" name="action_validate_passport_details" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Authentication Details">
|
||||
<group>
|
||||
<field name="authentication_details_status" widget="badge"/>
|
||||
<field name="pan_no"/>
|
||||
<field name="identification_id"/>
|
||||
<field name="previous_company_pf_no"/>
|
||||
<field name="previous_company_uan_no"/>
|
||||
</group>
|
||||
<group>
|
||||
<button string="Validate Authentication Details" name="action_validate_authentication_details" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Onboarding Details">
|
||||
<group>
|
||||
<field name="family_education_employer_details_status" widget="badge"/>
|
||||
<field name="attachments_validation_status" widget="badge"/>
|
||||
</group>
|
||||
<group>
|
||||
<button string="Validate Education, Employer and Family" name="action_validate_family_education_employer_details" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
<button string="Validate Attachments" name="action_validate_attachments" type="object" class="btn-primary" invisible="not employee_id or not applicant_id"/>
|
||||
</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>
|
||||
<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>
|
||||
<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" options="{'download': true}" 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>
|
||||
<page string="Offer Letters" name="offer_letters">
|
||||
<field name="offer_letter_ids" nolabel="1" options="{'no_create': True, 'no_open': True}">
|
||||
<list create="0" delete="0" no_open="True">
|
||||
<field name="name"/>
|
||||
<field name="requested_by_id"/>
|
||||
<field name="request_date"/>
|
||||
<field name="salary"/>
|
||||
<field name="state"/>
|
||||
<field name="sent_date"/>
|
||||
<field name="response_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_recruitment_employee_bridge_search" model="ir.ui.view">
|
||||
<field name="name">recruitment.employee.bridge.search</field>
|
||||
<field name="model">recruitment.employee.bridge</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="applicant_id"/>
|
||||
<field name="candidate_id"/>
|
||||
<field name="job_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'}"/>
|
||||
<filter name="group_job" string="Job" context="{'group_by': 'job_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_recruitment_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_recruitment_employee_bridge"
|
||||
name="Onboarding"
|
||||
parent="hr_recruitment.menu_crm_case_categ0_act_job"
|
||||
action="action_recruitment_employee_bridge"
|
||||
sequence="4"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="hr_recruitment_stage_offer_request_form_extended">
|
||||
<field name="name">hr.recruitment.stage.form.offer.request.extended</field>
|
||||
<field name="model">hr.recruitment.stage</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_recruitment_stage_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='fold']" position="after">
|
||||
<field name="request_offer_release"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import offer_release_request_wizard
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class OfferReleaseRequestWizard(models.TransientModel):
|
||||
_name = "offer.release.request.wizard"
|
||||
_description = "Offer Release Request Wizard"
|
||||
|
||||
bridge_id = fields.Many2one("recruitment.employee.bridge", string="Onboarding", required=True, readonly=True)
|
||||
position = fields.Char(string="Position", readonly=True)
|
||||
finalized_ctc = fields.Float(string="Finalized CTC", required=True)
|
||||
email_from = fields.Char(string="Email From", required=True)
|
||||
email_to = fields.Char(string="Mail To", required=True)
|
||||
email_cc = fields.Text(string="Mail CC")
|
||||
email_subject = fields.Char(string="Subject", required=True)
|
||||
email_body = fields.Html(
|
||||
string="Mail Body",
|
||||
render_engine="qweb",
|
||||
render_options={"post_process": True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize="email_outgoing",
|
||||
required=True,
|
||||
)
|
||||
|
||||
@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("active_id")
|
||||
)
|
||||
if not bridge.exists():
|
||||
raise UserError(_("The onboarding record does not exist or is not accessible."))
|
||||
|
||||
candidate_name = bridge.employee_name or bridge.employee_id.name or bridge.display_name
|
||||
position = bridge.job_id.name or candidate_name
|
||||
finalized_ctc = bridge.finalized_ctc or 0.0
|
||||
|
||||
defaults.update({
|
||||
"bridge_id": bridge.id,
|
||||
"position": position,
|
||||
"finalized_ctc": finalized_ctc,
|
||||
"email_from": self.env.user.email or self.env.company.email or "",
|
||||
"email_to": self._get_hr_email_to(),
|
||||
"email_subject": _("Offer Release Request - %s") % candidate_name,
|
||||
"email_body": (
|
||||
f"<p>Dear HR Team,</p>"
|
||||
f"<p>Please release the offer letter for <strong>{candidate_name}</strong>.</p>"
|
||||
f"<p><strong>Position:</strong> {position}<br/>"
|
||||
f"<strong>Finalized CTC:</strong> {finalized_ctc:.2f}<br/>"
|
||||
f"<strong>Requested By:</strong> {self.env.user.name}</p>"
|
||||
f"<p>Please review the onboarding record and release the offer letter.</p>"
|
||||
),
|
||||
})
|
||||
return defaults
|
||||
|
||||
def _get_hr_email_to(self):
|
||||
hr_id = self.env["ir.config_parameter"].sudo().get_param("requisitions.requisition_hr_id")
|
||||
users = self.env["res.users"].sudo().browse(int(hr_id)) if hr_id else self.env["res.users"]
|
||||
if not users:
|
||||
group = self.env.ref("hr.group_hr_manager")
|
||||
users = self.env["res.users"].sudo().search([
|
||||
("groups_id", "in", group.ids),
|
||||
("email", "!=", False),
|
||||
])
|
||||
emails = users.mapped("email")
|
||||
return ",".join(emails) or self.env.company.email or ""
|
||||
|
||||
def _get_default_pay_structure(self):
|
||||
return self.env["hr.payroll.structure"].sudo().search([], limit=1)
|
||||
|
||||
def _get_default_manager(self):
|
||||
return self.env.user.employee_id
|
||||
|
||||
def action_submit_request(self):
|
||||
self.ensure_one()
|
||||
bridge = self.bridge_id
|
||||
pay_structure = self._get_default_pay_structure()
|
||||
if not pay_structure:
|
||||
raise UserError(_("Please configure at least one salary structure before requesting an offer release."))
|
||||
|
||||
bridge.finalized_ctc = self.finalized_ctc
|
||||
offer_letter = self.env["offer.letter"].create({
|
||||
"bridge_id": bridge.id,
|
||||
"position": self.position,
|
||||
"salary": self.finalized_ctc,
|
||||
"joining_date": fields.Date.today(),
|
||||
"pay_struct_id": pay_structure.id,
|
||||
"manager_id": self._get_default_manager().id,
|
||||
"requested_by_id": self.env.user.id,
|
||||
"request_date": fields.Datetime.now(),
|
||||
"state": "requested",
|
||||
})
|
||||
offer_letter.get_paydetailed_lines()
|
||||
|
||||
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||
offer_url = f"{base_url}/web#id={offer_letter.id}&model=offer.letter&view_type=form"
|
||||
body_html = (
|
||||
f"{self.email_body}"
|
||||
f"<p><a href=\"{offer_url}\">Review Offer Letter Request</a></p>"
|
||||
)
|
||||
|
||||
mail = self.env["mail.mail"].sudo().create({
|
||||
"email_from": self.email_from,
|
||||
"email_to": self.email_to,
|
||||
"email_cc": self.email_cc,
|
||||
"subject": self.email_subject,
|
||||
"body_html": body_html,
|
||||
"auto_delete": False,
|
||||
"model": "offer.letter",
|
||||
"res_id": offer_letter.id,
|
||||
})
|
||||
mail.sudo().send()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_offer_release_request_wizard_form" model="ir.ui.view">
|
||||
<field name="name">offer.release.request.wizard.form</field>
|
||||
<field name="model">offer.release.request.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Request Offer Release">
|
||||
<group>
|
||||
<field name="bridge_id" readonly="1"/>
|
||||
<field name="position" readonly="1"/>
|
||||
<field name="finalized_ctc"/>
|
||||
</group>
|
||||
<group string="Notification Email">
|
||||
<field name="email_from" placeholder="Sender email"/>
|
||||
<field name="email_to" placeholder="HR email"/>
|
||||
<field name="email_cc" placeholder="CC emails"/>
|
||||
<field name="email_subject"/>
|
||||
<field name="email_body" widget="html"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_submit_request" string="Submit Request" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" special="cancel" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -34,9 +34,24 @@ class ShiftSwapRequest(models.Model):
|
|||
store=True
|
||||
)
|
||||
|
||||
def _validate_roster_date_not_past(self):
|
||||
today = fields.Date.context_today(self)
|
||||
|
||||
for rec in self:
|
||||
if rec.roster_date and rec.roster_date < today:
|
||||
raise UserError(
|
||||
_("Shift Swap cannot be requested for past dates.")
|
||||
)
|
||||
|
||||
@api.constrains('roster_date')
|
||||
def _check_roster_date_not_past(self):
|
||||
self._validate_roster_date_not_past()
|
||||
|
||||
def action_submit(self):
|
||||
for rec in self:
|
||||
|
||||
rec._validate_roster_date_not_past()
|
||||
|
||||
if not rec.reason:
|
||||
raise UserError(
|
||||
_("Please enter reason.")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue