Recruitment and hrms dashboard changes
This commit is contained in:
parent
b12c76d315
commit
e3ed288016
|
|
@ -269,9 +269,6 @@ RULES:
|
||||||
- Scan the entire document carefully before answering.
|
- Scan the entire document carefully before answering.
|
||||||
- Extract ONLY what exists in text.
|
- Extract ONLY what exists in text.
|
||||||
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
|
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
|
||||||
- For list fields, always return an array. Return [] when no explicit entries exist.
|
|
||||||
- For list fields that describe objects, return one object per real document entry, not a sentence blob.
|
|
||||||
- Do not skip education, employer, project, certification, or family sections if they appear later in the document.
|
|
||||||
|
|
||||||
FIELD RULES:
|
FIELD RULES:
|
||||||
- If "skills" exists, extract only explicit technical skills written in the document.
|
- If "skills" exists, extract only explicit technical skills written in the document.
|
||||||
|
|
@ -286,9 +283,6 @@ FIELD RULES:
|
||||||
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
|
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
|
||||||
- If "phone" exists, return the most complete phone number found.
|
- If "phone" exists, return the most complete phone number found.
|
||||||
- If "experience" exists, return only clearly supported numeric values.
|
- If "experience" exists, return only clearly supported numeric values.
|
||||||
- If "education_history" exists, extract every education row/section with education_type, specialization, university, start_year, end_year, and marks_or_grade.
|
|
||||||
- If "employer_history" exists, extract every employer/project/work-experience entry with company_name, designation, date_of_joining, last_working_day, ctc, and work_description.
|
|
||||||
- If "family_details" exists, extract only explicitly written family members with relation_type, name, contact_no, dob, and location.
|
|
||||||
|
|
||||||
Schema:
|
Schema:
|
||||||
{schema_text}
|
{schema_text}
|
||||||
|
|
@ -307,7 +301,7 @@ Document:
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
"max_tokens": 4000,
|
"max_tokens": 1500,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)
|
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
|
from . import wizards
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
"name": "Onboarding",
|
||||||
|
"summary": "Employee-side onboarding bridge for JOD and onboarding links",
|
||||||
|
"description": """
|
||||||
|
Provides an employee-owned bridge record that can be used without
|
||||||
|
installing recruitment or ATS modules.
|
||||||
|
""",
|
||||||
|
"author": "FTPROTECH",
|
||||||
|
"website": "https://www.ftprotech.com",
|
||||||
|
"category": "Human Resources",
|
||||||
|
"version": "1.0",
|
||||||
|
"depends": [
|
||||||
|
"hr_employee_extended",
|
||||||
|
"mail",
|
||||||
|
"website",
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"data/ir_sequence_data.xml",
|
||||||
|
"data/onboarding_attachment_data.xml",
|
||||||
|
"data/mail_template.xml",
|
||||||
|
"data/template.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"wizards/employee_bridge_attachment_wizard.xml",
|
||||||
|
"views/recruitment_employee_bridge_views.xml",
|
||||||
|
"views/onboarding_attachment_views.xml",
|
||||||
|
"views/hr_employee_views.xml",
|
||||||
|
],
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"assets": {
|
||||||
|
"web.assets_frontend": [
|
||||||
|
"employee_bridge/static/src/js/post_onboarding_form.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"application": False,
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.addons.website.controllers.main import Website # <-- ADD THIS IMPORT
|
||||||
|
|
||||||
|
|
||||||
|
# CHANGE INHERITANCE: http.Controller -> Website
|
||||||
|
class EmployeeBridgeController(Website):
|
||||||
|
|
||||||
|
def _get_bridge(self, token):
|
||||||
|
return request.env["recruitment.employee.bridge"].sudo().search([
|
||||||
|
("access_token", "=", token),
|
||||||
|
("active", "=", True),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
def _render_form(self, bridge):
|
||||||
|
# Removed the 'message' variable as we will use a separate template for success
|
||||||
|
return request.render("employee_bridge.employee_bridge_jod_form_template", {
|
||||||
|
"bridge": bridge,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route("/employee_bridge/jod/<string:token>", type="http", auth="public", csrf=False, website=True)
|
||||||
|
def employee_bridge_jod(self, token, **post):
|
||||||
|
bridge = self._get_bridge(token)
|
||||||
|
if not bridge:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
if request.httprequest.method == "POST":
|
||||||
|
bridge.write_from_public_form(post)
|
||||||
|
|
||||||
|
# RENDER A SEPARATE THANK YOU TEMPLATE INSTEAD OF THE FORM
|
||||||
|
return request.render("employee_bridge.employee_bridge_thank_you_template", {
|
||||||
|
"bridge": bridge,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self._render_form(bridge)
|
||||||
|
|
||||||
|
@http.route("/employee_bridge/fetch_related_state_ids", type="json", auth="public", website=True)
|
||||||
|
def fetch_related_state_ids(self, country_id=None):
|
||||||
|
states = request.env["res.country.state"].sudo()
|
||||||
|
return {
|
||||||
|
state.id: state.name
|
||||||
|
for state in states.search([("country_id", "=?", country_id)])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="seq_recruitment_employee_bridge" model="ir.sequence">
|
||||||
|
<field name="name">Onboarding</field>
|
||||||
|
<field name="code">recruitment.employee.bridge</field>
|
||||||
|
<field name="prefix">EB/</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="email_template_jod_form" model="mail.template">
|
||||||
|
<field name="name">JOD Form Email</field>
|
||||||
|
<field name="model_id" ref="employee_bridge.model_recruitment_employee_bridge"/>
|
||||||
|
<field name="email_from">{{ user.email_formatted or object.company_id.email or '' }}</field>
|
||||||
|
<field name="email_to">{{ object.work_email or object.private_email or object.employee_id.work_email or '' }}</field>
|
||||||
|
<field name="subject">Joining Onboarding Form - {{ object.employee_name or object.employee_id.name or '' }}</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin:0; padding:0; font-size:13px; line-height:1.7;">
|
||||||
|
<p>Dear <t t-esc="object.employee_name or object.employee_id.name or 'Candidate'"/>,</p>
|
||||||
|
<p>Welcome to <t t-esc="object.employee_id.company_id.name"/>.</p>
|
||||||
|
<p>
|
||||||
|
Please submit your employee joining details using the link below.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a t-att-href="ctx.get('joining_form_link')" target="_blank"
|
||||||
|
style="background-color:#005580; color:#ffffff; padding:10px 18px; text-decoration:none; border-radius:4px; display:inline-block;">
|
||||||
|
Open Employee Joining Form
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
|
||||||
|
<p><strong>Attachments to Request</strong></p>
|
||||||
|
<t t-if="ctx.get('personal_docs')">
|
||||||
|
<p><strong>Personal Documents:</strong> <t t-esc="', '.join(ctx.get('personal_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="ctx.get('education_docs')">
|
||||||
|
<p><strong>Education Documents:</strong> <t t-esc="', '.join(ctx.get('education_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="ctx.get('previous_employer_docs')">
|
||||||
|
<p><strong>Previous Employer Documents:</strong> <t t-esc="', '.join(ctx.get('previous_employer_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="ctx.get('other_docs')">
|
||||||
|
<p><strong>Other Documents:</strong> <t t-esc="', '.join(ctx.get('other_docs'))"/></p>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<p>Regards,<br/>HR Team</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="onboarding_attachment_aadhaar" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Aadhaar Card</field>
|
||||||
|
<field name="attachment_type">personal</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_pan" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">PAN Card</field>
|
||||||
|
<field name="attachment_type">personal</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_photo" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Passport Size Photo</field>
|
||||||
|
<field name="attachment_type">personal</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_education" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Education Certificates</field>
|
||||||
|
<field name="attachment_type">education</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_experience" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Experience / Relieving Letter</field>
|
||||||
|
<field name="attachment_type">previous_employer</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_payslip" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Last Three Months Payslips</field>
|
||||||
|
<field name="attachment_type">previous_employer</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="onboarding_attachment_bank" model="employee.bridge.requested.attachment">
|
||||||
|
<field name="name">Cancelled Cheque / Bank Proof</field>
|
||||||
|
<field name="attachment_type">others</field>
|
||||||
|
<field name="is_default">1</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import recruitment_employee_bridge
|
||||||
|
from . import hr_employee
|
||||||
|
from . import onboarding_documents
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
from odoo import _, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class HrEmployee(models.Model):
|
||||||
|
_inherit = "hr.employee"
|
||||||
|
|
||||||
|
recruitment_bridge_ids = fields.One2many(
|
||||||
|
"recruitment.employee.bridge",
|
||||||
|
"employee_id",
|
||||||
|
string="Onboarding Records",
|
||||||
|
)
|
||||||
|
recruitment_bridge_count = fields.Integer(compute="_compute_recruitment_bridge_count")
|
||||||
|
|
||||||
|
def _compute_recruitment_bridge_count(self):
|
||||||
|
data = self.env["recruitment.employee.bridge"].read_group(
|
||||||
|
[("employee_id", "in", self.ids), ("active", "=", True)],
|
||||||
|
["employee_id"],
|
||||||
|
["employee_id"],
|
||||||
|
)
|
||||||
|
counts = {item["employee_id"][0]: item["employee_id_count"] for item in data}
|
||||||
|
for employee in self:
|
||||||
|
employee.recruitment_bridge_count = counts.get(employee.id, 0)
|
||||||
|
|
||||||
|
def _get_or_create_employee_bridge(self):
|
||||||
|
self.ensure_one()
|
||||||
|
bridge = self.env["recruitment.employee.bridge"].search(
|
||||||
|
[("employee_id", "=", self.id), ("active", "=", True)],
|
||||||
|
limit=1,
|
||||||
|
order="id desc",
|
||||||
|
)
|
||||||
|
if not bridge:
|
||||||
|
bridge = self.env["recruitment.employee.bridge"].create({
|
||||||
|
"employee_id": self.id,
|
||||||
|
"company_id": self.company_id.id or self.env.company.id,
|
||||||
|
"source": "jod",
|
||||||
|
"state": "draft",
|
||||||
|
})
|
||||||
|
return bridge
|
||||||
|
|
||||||
|
def send_jod_form_to_employee(self):
|
||||||
|
self.ensure_one()
|
||||||
|
bridge = self._get_or_create_employee_bridge()
|
||||||
|
bridge.action_send_jod_form()
|
||||||
|
return {
|
||||||
|
"name": _("Onboarding"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "recruitment.employee.bridge",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": bridge.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_recruitment_bridge(self):
|
||||||
|
self.ensure_one()
|
||||||
|
bridge = self._get_or_create_employee_bridge()
|
||||||
|
if not bridge:
|
||||||
|
raise ValidationError(_("No onboarding record found."))
|
||||||
|
return {
|
||||||
|
"name": _("Onboarding"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "recruitment.employee.bridge",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": bridge.id,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBridgeRequestedAttachment(models.Model):
|
||||||
|
_name = "employee.bridge.requested.attachment"
|
||||||
|
_description = "Onboarding Requested Attachment"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
attachment_type = fields.Selection([
|
||||||
|
("personal", "Personal Documents"),
|
||||||
|
("education", "Education Documents"),
|
||||||
|
("previous_employer", "Previous Employer"),
|
||||||
|
("others", "Others"),
|
||||||
|
], default="others", required=True)
|
||||||
|
is_default = fields.Boolean(string="Is Default")
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBridgeAttachment(models.Model):
|
||||||
|
_name = "employee.bridge.attachment"
|
||||||
|
_description = "Onboarding Attachment"
|
||||||
|
_rec_name = "name"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, ondelete="cascade")
|
||||||
|
employee_id = fields.Many2one("hr.employee")
|
||||||
|
requested_attachment_id = fields.Many2one("employee.bridge.requested.attachment")
|
||||||
|
attachment_type = fields.Selection(related="requested_attachment_id.attachment_type")
|
||||||
|
file = fields.Binary(required=True)
|
||||||
|
file_name = fields.Char()
|
||||||
|
review_status = fields.Selection([
|
||||||
|
("draft", "Under Review"),
|
||||||
|
("pass", "PASS"),
|
||||||
|
("fail", "FAIL"),
|
||||||
|
], default="draft")
|
||||||
|
review_comments = fields.Char()
|
||||||
|
|
||||||
|
def action_preview_file(self):
|
||||||
|
self.ensure_one()
|
||||||
|
attachment = self.env["ir.attachment"].sudo().create({
|
||||||
|
"name": self.file_name or self.name,
|
||||||
|
"datas": self.file,
|
||||||
|
"res_model": self._name,
|
||||||
|
"res_id": self.id,
|
||||||
|
"type": "binary",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"name": "File Preview",
|
||||||
|
"type": "ir.actions.act_url",
|
||||||
|
"url": f"/web/content/{attachment.id}?download=false",
|
||||||
|
"target": "current",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyDetails(models.Model):
|
||||||
|
_inherit = "family.details"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||||
|
|
||||||
|
|
||||||
|
class EducationHistory(models.Model):
|
||||||
|
_inherit = "education.history"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||||
|
|
||||||
|
|
||||||
|
class EmployerHistory(models.Model):
|
||||||
|
_inherit = "employer.history"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", ondelete="cascade")
|
||||||
|
|
@ -0,0 +1,559 @@
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class RecruitmentEmployeeBridge(models.Model):
|
||||||
|
_name = "recruitment.employee.bridge"
|
||||||
|
_description = "Onboarding"
|
||||||
|
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||||
|
_rec_name = "name"
|
||||||
|
_order = "id desc"
|
||||||
|
|
||||||
|
@api.depends('name', 'employee_id', 'employee_name')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for record in self:
|
||||||
|
employee_name = record.employee_name or record.employee_id.name or ''
|
||||||
|
|
||||||
|
if employee_name:
|
||||||
|
record.display_name = f"{record.name} - {employee_name}"
|
||||||
|
else:
|
||||||
|
record.display_name = record.name
|
||||||
|
|
||||||
|
name = fields.Char(default="/", copy=False, readonly=True, tracking=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
"hr.employee",
|
||||||
|
string="Employee",
|
||||||
|
ondelete="restrict",
|
||||||
|
tracking=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company",
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
source = fields.Selection(
|
||||||
|
[
|
||||||
|
("jod", "JOD"),
|
||||||
|
("manual", "Manual"),
|
||||||
|
("recruitment", "Recruitment"),
|
||||||
|
],
|
||||||
|
default="jod",
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
state = fields.Selection(
|
||||||
|
[
|
||||||
|
("draft", "Draft"),
|
||||||
|
("jod_sent", "JOD Sent"),
|
||||||
|
("jod_received", "JOD Received"),
|
||||||
|
("validated", "Validated"),
|
||||||
|
],
|
||||||
|
default="draft",
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
employee_name = fields.Char(string="Employee Name", tracking=True)
|
||||||
|
employee_code = fields.Char(string="Employee Code", tracking=True)
|
||||||
|
candidate_image = fields.Image(string="Photo")
|
||||||
|
work_email = fields.Char(string="Work Email", tracking=True)
|
||||||
|
private_email = fields.Char(string="Private Email")
|
||||||
|
alternate_phone = fields.Char(string="Alternate Mobile")
|
||||||
|
work_phone = fields.Char(string="Work Phone")
|
||||||
|
mobile_phone = fields.Char(string="Mobile Phone")
|
||||||
|
department_id = fields.Many2one("hr.department", string="Department")
|
||||||
|
job_id = fields.Many2one("hr.job", string="Job Position")
|
||||||
|
emp_type = fields.Many2one("hr.contract.type", string="Employment Type")
|
||||||
|
doj = fields.Date(string="Date of Joining")
|
||||||
|
gender = fields.Selection([
|
||||||
|
("male", "Male"),
|
||||||
|
("female", "Female"),
|
||||||
|
("other", "Other"),
|
||||||
|
])
|
||||||
|
birthday = fields.Date()
|
||||||
|
blood_group = fields.Selection([
|
||||||
|
("A+", "A+"),
|
||||||
|
("A-", "A-"),
|
||||||
|
("B+", "B+"),
|
||||||
|
("B-", "B-"),
|
||||||
|
("O+", "O+"),
|
||||||
|
("O-", "O-"),
|
||||||
|
("AB+", "AB+"),
|
||||||
|
("AB-", "AB-"),
|
||||||
|
], string="Blood Group")
|
||||||
|
marital = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("single", "Single"),
|
||||||
|
("married", "Married"),
|
||||||
|
("cohabitant", "Legal Cohabitant"),
|
||||||
|
("widower", "Widower"),
|
||||||
|
("divorced", "Divorced"),
|
||||||
|
],
|
||||||
|
default="single",
|
||||||
|
string="Marital Status",
|
||||||
|
)
|
||||||
|
marriage_anniversary_date = fields.Date(string="Anniversary Date")
|
||||||
|
private_street = fields.Char(string="Private Street")
|
||||||
|
private_street2 = fields.Char(string="Private Street2")
|
||||||
|
private_city = fields.Char(string="Private City")
|
||||||
|
private_state_id = fields.Many2one("res.country.state", string="Private State")
|
||||||
|
private_zip = fields.Char(string="Private Zip")
|
||||||
|
private_country_id = fields.Many2one("res.country", string="Private Country")
|
||||||
|
permanent_street = fields.Char(string="Permanent Street")
|
||||||
|
permanent_street2 = fields.Char(string="Permanent Street2")
|
||||||
|
permanent_city = fields.Char(string="Permanent City")
|
||||||
|
permanent_state_id = fields.Many2one("res.country.state", string="Permanent State")
|
||||||
|
permanent_zip = fields.Char(string="Permanent Zip")
|
||||||
|
permanent_country_id = fields.Many2one("res.country", string="Permanent Country")
|
||||||
|
pan_no = fields.Char(string="PAN No")
|
||||||
|
identification_id = fields.Char(string="Aadhar No")
|
||||||
|
previous_company_pf_no = fields.Char(string="Previous Company PF No")
|
||||||
|
previous_company_uan_no = fields.Char(string="Previous Company UAN No")
|
||||||
|
passport_no = fields.Char(string="Passport No")
|
||||||
|
passport_start_date = fields.Date(string="Passport Issued Date")
|
||||||
|
passport_end_date = fields.Date(string="Passport End Date")
|
||||||
|
passport_issued_location = fields.Char(string="Passport Issued Location")
|
||||||
|
full_name_as_in_bank = fields.Char(string="Name as per Bank")
|
||||||
|
bank_name = fields.Char()
|
||||||
|
bank_branch = fields.Char()
|
||||||
|
bank_account_no = fields.Char()
|
||||||
|
bank_ifsc_code = fields.Char()
|
||||||
|
requested_attachment_ids = fields.Many2many(
|
||||||
|
"employee.bridge.requested.attachment",
|
||||||
|
"employee_bridge_requested_attachment_rel",
|
||||||
|
"bridge_id",
|
||||||
|
"attachment_id",
|
||||||
|
string="Attachments to Request",
|
||||||
|
)
|
||||||
|
joining_attachment_ids = fields.One2many(
|
||||||
|
"employee.bridge.attachment",
|
||||||
|
"bridge_id",
|
||||||
|
string="Attachments",
|
||||||
|
)
|
||||||
|
education_history = fields.One2many("education.history", "bridge_id", string="Education Details")
|
||||||
|
employer_history = fields.One2many("employer.history", "bridge_id", string="Employer History")
|
||||||
|
family_details = fields.One2many("family.details", "bridge_id", string="Family Details")
|
||||||
|
joining_form_link = fields.Char(string="Joining Form Link", copy=False, readonly=True)
|
||||||
|
access_token = fields.Char(copy=False, readonly=True)
|
||||||
|
total_exp = fields.Float(string="Total Exp")
|
||||||
|
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
"unique_bridge_employee",
|
||||||
|
"unique(employee_id)",
|
||||||
|
"This employee is already connected to an employee bridge.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_base_url(self):
|
||||||
|
return self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||||
|
|
||||||
|
def _ensure_access_token(self):
|
||||||
|
for bridge in self:
|
||||||
|
if not bridge.access_token:
|
||||||
|
bridge.access_token = token_urlsafe(32)
|
||||||
|
|
||||||
|
def _get_joining_form_link(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self._ensure_access_token()
|
||||||
|
return "%s/employee_bridge/jod/%s" % (self._get_base_url(), self.access_token)
|
||||||
|
|
||||||
|
def _group_requested_attachments(self, attachments):
|
||||||
|
return {
|
||||||
|
"personal_docs": attachments.filtered(lambda a: a.attachment_type == "personal").mapped("name"),
|
||||||
|
"education_docs": attachments.filtered(lambda a: a.attachment_type == "education").mapped("name"),
|
||||||
|
"previous_employer_docs": attachments.filtered(lambda a: a.attachment_type == "previous_employer").mapped("name"),
|
||||||
|
"other_docs": attachments.filtered(lambda a: a.attachment_type == "others").mapped("name"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _send_jod_mail(self, req_attachment_ids=False, email_values=None, email_body=False):
|
||||||
|
for bridge in self:
|
||||||
|
email_to = bridge.work_email or bridge.private_email or bridge.employee_id.work_email
|
||||||
|
if not email_to:
|
||||||
|
raise ValidationError(_("Add an email on the bridge before sending the JOD form."))
|
||||||
|
link = bridge._get_joining_form_link()
|
||||||
|
bridge.write({
|
||||||
|
"joining_form_link": link,
|
||||||
|
"state": "jod_sent",
|
||||||
|
})
|
||||||
|
attachments = req_attachment_ids or self.env["employee.bridge.requested.attachment"].browse()
|
||||||
|
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||||
|
context = {
|
||||||
|
"joining_form_link": link,
|
||||||
|
**bridge._group_requested_attachments(attachments),
|
||||||
|
}
|
||||||
|
values = dict(email_values or {})
|
||||||
|
values.setdefault("email_from", self.env.company.email or self.env.user.email)
|
||||||
|
values.setdefault("email_to", email_to)
|
||||||
|
if email_body:
|
||||||
|
values["body_html"] = email_body
|
||||||
|
if template:
|
||||||
|
rendered_subject = template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id]
|
||||||
|
rendered_body = template.with_context(**context)._render_field(
|
||||||
|
"body_html", [bridge.id], compute_lang=True
|
||||||
|
)[bridge.id]
|
||||||
|
values.setdefault("subject", rendered_subject)
|
||||||
|
values.setdefault("body_html", rendered_body)
|
||||||
|
template.sudo().with_context(**context).send_mail(bridge.id, email_values=values, force_send=True)
|
||||||
|
else:
|
||||||
|
values.setdefault("subject", _("Joining Onboarding Details"))
|
||||||
|
values.setdefault("body_html", _(
|
||||||
|
"<p>Hello %(name)s,</p>"
|
||||||
|
"<p>Please submit your joining onboarding details using this link:</p>"
|
||||||
|
"<p><a href='%(link)s'>Open Joining Form</a></p>"
|
||||||
|
) % {
|
||||||
|
"name": bridge.employee_name or bridge.employee_id.name or _("Employee"),
|
||||||
|
"link": link,
|
||||||
|
})
|
||||||
|
self.env["mail.mail"].sudo().create(values).send()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_send_jod_form(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"name": _("Select Attachments"),
|
||||||
|
"res_model": "employee.bridge.attachment.wizard",
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
"context": {
|
||||||
|
"default_bridge_id": self.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_employee(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.employee_id:
|
||||||
|
raise ValidationError(_("No employee is linked to this bridge."))
|
||||||
|
return {
|
||||||
|
"name": _("Employee"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "hr.employee",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": self.employee_id.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_validate_employee_details(self):
|
||||||
|
for bridge in self:
|
||||||
|
if not bridge.employee_id:
|
||||||
|
raise ValidationError(_("Link or create an employee before validating the JOD details."))
|
||||||
|
bridge.employee_id.write(bridge._get_employee_update_vals())
|
||||||
|
bridge.education_history.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.employer_history.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.family_details.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.joining_attachment_ids.write({"employee_id": bridge.employee_id.id})
|
||||||
|
bridge.state = "validated"
|
||||||
|
|
||||||
|
def _get_employee_create_vals(self):
|
||||||
|
self.ensure_one()
|
||||||
|
vals = {
|
||||||
|
"name": self.employee_name,
|
||||||
|
"employee_id": self.employee_code,
|
||||||
|
"work_email": self.work_email,
|
||||||
|
"private_email": self.private_email,
|
||||||
|
"work_phone": self.work_phone,
|
||||||
|
"mobile_phone": self.mobile_phone,
|
||||||
|
"image_1920": self.candidate_image,
|
||||||
|
"department_id": self.department_id.id,
|
||||||
|
"job_id": self.job_id.id,
|
||||||
|
"job_title": self.job_id.name,
|
||||||
|
"emp_type": self.emp_type.id,
|
||||||
|
"company_id": self.company_id.id,
|
||||||
|
}
|
||||||
|
vals = {key: value for key, value in vals.items() if value not in (False, "", 0)}
|
||||||
|
if not vals.get("name"):
|
||||||
|
raise ValidationError(_("Employee Name is required to create an employee."))
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def _get_employee_update_vals(self):
|
||||||
|
self.ensure_one()
|
||||||
|
vals = {
|
||||||
|
"name": self.employee_name,
|
||||||
|
"employee_id": self.employee_code,
|
||||||
|
"work_email": self.work_email,
|
||||||
|
"private_email": self.private_email,
|
||||||
|
"work_phone": self.work_phone,
|
||||||
|
"mobile_phone": self.mobile_phone,
|
||||||
|
"image_1920": self.candidate_image,
|
||||||
|
"department_id": self.department_id.id,
|
||||||
|
"job_id": self.job_id.id,
|
||||||
|
"job_title": self.job_id.name,
|
||||||
|
"emp_type": self.emp_type.id,
|
||||||
|
"doj": self.doj,
|
||||||
|
"gender": self.gender,
|
||||||
|
"birthday": self.birthday,
|
||||||
|
"blood_group": self.blood_group,
|
||||||
|
"marital": self.marital,
|
||||||
|
"marriage_anniversary_date": self.marriage_anniversary_date,
|
||||||
|
"private_street": self.private_street,
|
||||||
|
"private_street2": self.private_street2,
|
||||||
|
"private_city": self.private_city,
|
||||||
|
"private_state_id": self.private_state_id.id,
|
||||||
|
"private_zip": self.private_zip,
|
||||||
|
"private_country_id": self.private_country_id.id,
|
||||||
|
"permanent_street": self.permanent_street,
|
||||||
|
"permanent_street2": self.permanent_street2,
|
||||||
|
"permanent_city": self.permanent_city,
|
||||||
|
"permanent_state_id": self.permanent_state_id.id,
|
||||||
|
"permanent_zip": self.permanent_zip,
|
||||||
|
"permanent_country_id": self.permanent_country_id.id,
|
||||||
|
"pan_no": self.pan_no,
|
||||||
|
"identification_id": self.identification_id,
|
||||||
|
"previous_company_pf_no": self.previous_company_pf_no,
|
||||||
|
"previous_company_uan_no": self.previous_company_uan_no,
|
||||||
|
"passport_id": self.passport_no,
|
||||||
|
"passport_start_date": self.passport_start_date,
|
||||||
|
"passport_end_date": self.passport_end_date,
|
||||||
|
"passport_issued_location": self.passport_issued_location,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in vals.items()
|
||||||
|
if key in self.employee_id._fields and value not in (False, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_create_employee(self):
|
||||||
|
for bridge in self:
|
||||||
|
if bridge.employee_id:
|
||||||
|
continue
|
||||||
|
bridge.employee_id = self.env["hr.employee"].sudo().create(bridge._get_employee_create_vals())
|
||||||
|
return self.action_open_employee()
|
||||||
|
|
||||||
|
def _sync_from_employee(self):
|
||||||
|
for bridge in self.filtered("employee_id"):
|
||||||
|
employee = bridge.employee_id
|
||||||
|
bridge.write({
|
||||||
|
"employee_name": bridge.employee_name or employee.name,
|
||||||
|
"employee_code": bridge.employee_code or employee.employee_id,
|
||||||
|
"work_email": bridge.work_email or employee.work_email,
|
||||||
|
"private_email": bridge.private_email or employee.private_email,
|
||||||
|
"work_phone": bridge.work_phone or employee.work_phone,
|
||||||
|
"mobile_phone": bridge.mobile_phone or employee.mobile_phone,
|
||||||
|
"department_id": bridge.department_id.id or employee.department_id.id,
|
||||||
|
"job_id": bridge.job_id.id or employee.job_id.id,
|
||||||
|
"emp_type": bridge.emp_type.id or employee.emp_type.id,
|
||||||
|
"company_id": employee.company_id.id or bridge.company_id.id,
|
||||||
|
"doj": bridge.doj or employee.doj,
|
||||||
|
"gender": bridge.gender or employee.gender,
|
||||||
|
"birthday": bridge.birthday or employee.birthday,
|
||||||
|
"blood_group": bridge.blood_group or employee.blood_group,
|
||||||
|
"marital": bridge.marital or employee.marital,
|
||||||
|
"marriage_anniversary_date": bridge.marriage_anniversary_date or employee.marriage_anniversary_date,
|
||||||
|
"private_street": bridge.private_street or employee.private_street,
|
||||||
|
"private_street2": bridge.private_street2 or employee.private_street2,
|
||||||
|
"private_city": bridge.private_city or employee.private_city,
|
||||||
|
"private_state_id": bridge.private_state_id.id or employee.private_state_id.id,
|
||||||
|
"private_zip": bridge.private_zip or employee.private_zip,
|
||||||
|
"private_country_id": bridge.private_country_id.id or employee.private_country_id.id,
|
||||||
|
"permanent_street": bridge.permanent_street or employee.permanent_street,
|
||||||
|
"permanent_street2": bridge.permanent_street2 or employee.permanent_street2,
|
||||||
|
"permanent_city": bridge.permanent_city or employee.permanent_city,
|
||||||
|
"permanent_state_id": bridge.permanent_state_id.id or employee.permanent_state_id.id,
|
||||||
|
"permanent_zip": bridge.permanent_zip or employee.permanent_zip,
|
||||||
|
"permanent_country_id": bridge.permanent_country_id.id or employee.permanent_country_id.id,
|
||||||
|
"pan_no": bridge.pan_no or employee.pan_no,
|
||||||
|
"identification_id": bridge.identification_id or employee.identification_id,
|
||||||
|
"previous_company_pf_no": bridge.previous_company_pf_no or employee.previous_company_pf_no,
|
||||||
|
"previous_company_uan_no": bridge.previous_company_uan_no or employee.previous_company_uan_no,
|
||||||
|
"passport_no": bridge.passport_no or employee.passport_id,
|
||||||
|
"passport_start_date": bridge.passport_start_date or employee.passport_start_date,
|
||||||
|
"passport_end_date": bridge.passport_end_date or employee.passport_end_date,
|
||||||
|
"passport_issued_location": bridge.passport_issued_location or employee.passport_issued_location,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.onchange("employee_id")
|
||||||
|
def _onchange_employee_id(self):
|
||||||
|
for bridge in self:
|
||||||
|
employee = bridge.employee_id
|
||||||
|
if not employee:
|
||||||
|
continue
|
||||||
|
bridge.employee_name = bridge.employee_name or employee.name
|
||||||
|
bridge.employee_code = bridge.employee_code or employee.employee_id
|
||||||
|
bridge.work_email = bridge.work_email or employee.work_email
|
||||||
|
bridge.private_email = bridge.private_email or employee.private_email
|
||||||
|
bridge.work_phone = bridge.work_phone or employee.work_phone
|
||||||
|
bridge.mobile_phone = bridge.mobile_phone or employee.mobile_phone
|
||||||
|
bridge.department_id = bridge.department_id or employee.department_id
|
||||||
|
bridge.job_id = bridge.job_id or employee.job_id
|
||||||
|
bridge.emp_type = bridge.emp_type or employee.emp_type
|
||||||
|
|
||||||
|
def write_from_public_form(self, values):
|
||||||
|
self.ensure_one()
|
||||||
|
private_state = self.env["res.country.state"].sudo().browse(int(values.get("present_state") or 0))
|
||||||
|
permanent_state = self.env["res.country.state"].sudo().browse(int(values.get("permanent_state") or 0))
|
||||||
|
mapped_values = dict(values)
|
||||||
|
mapped_values.update({
|
||||||
|
"candidate_image": values.get("candidate_image_base64"),
|
||||||
|
"private_email": values.get("email_from"),
|
||||||
|
"mobile_phone": values.get("partner_phone"),
|
||||||
|
"alternate_phone": values.get("alternate_phone"),
|
||||||
|
"private_street": values.get("present_street"),
|
||||||
|
"private_street2": values.get("present_street2"),
|
||||||
|
"private_city": values.get("present_city"),
|
||||||
|
"private_zip": values.get("present_zip"),
|
||||||
|
"private_state_id": private_state.id if private_state else False,
|
||||||
|
"private_country_id": private_state.country_id.id if private_state else False,
|
||||||
|
"permanent_state_id": permanent_state.id if permanent_state else False,
|
||||||
|
"permanent_country_id": permanent_state.country_id.id if permanent_state else False,
|
||||||
|
})
|
||||||
|
allowed_fields = {
|
||||||
|
"employee_name",
|
||||||
|
"candidate_image",
|
||||||
|
"private_email",
|
||||||
|
"mobile_phone",
|
||||||
|
"alternate_phone",
|
||||||
|
"doj",
|
||||||
|
"gender",
|
||||||
|
"birthday",
|
||||||
|
"blood_group",
|
||||||
|
"marital",
|
||||||
|
"marriage_anniversary_date",
|
||||||
|
"private_street",
|
||||||
|
"private_street2",
|
||||||
|
"private_city",
|
||||||
|
"private_state_id",
|
||||||
|
"private_country_id",
|
||||||
|
"private_zip",
|
||||||
|
"permanent_street",
|
||||||
|
"permanent_street2",
|
||||||
|
"permanent_city",
|
||||||
|
"permanent_state_id",
|
||||||
|
"permanent_country_id",
|
||||||
|
"permanent_zip",
|
||||||
|
"full_name_as_in_bank",
|
||||||
|
"bank_name",
|
||||||
|
"bank_branch",
|
||||||
|
"bank_account_no",
|
||||||
|
"bank_ifsc_code",
|
||||||
|
"pan_no",
|
||||||
|
"identification_id",
|
||||||
|
"previous_company_pf_no",
|
||||||
|
"previous_company_uan_no",
|
||||||
|
"passport_no",
|
||||||
|
"passport_start_date",
|
||||||
|
"passport_end_date",
|
||||||
|
"passport_issued_location",
|
||||||
|
}
|
||||||
|
bridge_vals = {
|
||||||
|
field_name: value or False
|
||||||
|
for field_name, value in mapped_values.items()
|
||||||
|
if field_name in allowed_fields and field_name in self._fields
|
||||||
|
}
|
||||||
|
if bridge_vals:
|
||||||
|
self.sudo().write(bridge_vals)
|
||||||
|
self._write_history_from_public_form(values)
|
||||||
|
self._replace_joining_attachments_from_public_form(values)
|
||||||
|
self.state = "jod_received"
|
||||||
|
|
||||||
|
def _safe_date(self, value):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, "%Y-%m-%d").date() if value else False
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _safe_int(self, value):
|
||||||
|
try:
|
||||||
|
return int(value) if value else False
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _json_list(self, values, key):
|
||||||
|
raw = values.get(key, "[]")
|
||||||
|
try:
|
||||||
|
data = json.loads(raw) if raw else []
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
data = []
|
||||||
|
return data if isinstance(data, list) else []
|
||||||
|
|
||||||
|
def _write_history_from_public_form(self, values):
|
||||||
|
self.ensure_one()
|
||||||
|
family_values = []
|
||||||
|
for member in self._json_list(values, "family_data_json"):
|
||||||
|
if member.get("name") and member.get("relation"):
|
||||||
|
family_values.append((0, 0, {
|
||||||
|
"relation_type": member.get("relation"),
|
||||||
|
"name": member.get("name"),
|
||||||
|
"contact_no": member.get("contact"),
|
||||||
|
"dob": self._safe_date(member.get("dob")),
|
||||||
|
"location": member.get("location"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
education_values = []
|
||||||
|
for education in self._json_list(values, "education_data_json"):
|
||||||
|
if education.get("specialization"):
|
||||||
|
education_values.append((0, 0, {
|
||||||
|
"education_type": education.get("education_type") or "additional",
|
||||||
|
"name": education.get("specialization"),
|
||||||
|
"university": education.get("university"),
|
||||||
|
"start_year": self._safe_int(education.get("start_year")),
|
||||||
|
"end_year": self._safe_int(education.get("end_year")),
|
||||||
|
"marks_or_grade": education.get("marks_or_grade"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
employer_values = []
|
||||||
|
for employer in self._json_list(values, "employer_history_data_json"):
|
||||||
|
if employer.get("company_name"):
|
||||||
|
employer_values.append((0, 0, {
|
||||||
|
"company_name": employer.get("company_name"),
|
||||||
|
"designation": employer.get("designation"),
|
||||||
|
"date_of_joining": self._safe_date(employer.get("date_of_joining")),
|
||||||
|
"last_working_day": self._safe_date(employer.get("last_working_day")),
|
||||||
|
"ctc": employer.get("ctc"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
write_vals = {}
|
||||||
|
if family_values:
|
||||||
|
write_vals["family_details"] = [(5, 0, 0)] + family_values
|
||||||
|
if education_values:
|
||||||
|
write_vals["education_history"] = [(5, 0, 0)] + education_values
|
||||||
|
if employer_values:
|
||||||
|
write_vals["employer_history"] = [(5, 0, 0)] + employer_values
|
||||||
|
if write_vals:
|
||||||
|
self.sudo().write(write_vals)
|
||||||
|
|
||||||
|
def _replace_joining_attachments_from_public_form(self, values):
|
||||||
|
self.ensure_one()
|
||||||
|
attachment_values = []
|
||||||
|
seen_files = set()
|
||||||
|
for item in self._json_list(values, "attachments_data_json"):
|
||||||
|
attachment = self.env["employee.bridge.requested.attachment"].sudo().browse(
|
||||||
|
int(item.get("attachment_rec_id") or 0)
|
||||||
|
)
|
||||||
|
file_content = item.get("file_content")
|
||||||
|
file_name = item.get("file_name")
|
||||||
|
file_key = (attachment.id, file_name, file_content)
|
||||||
|
if attachment.exists() and file_content and file_key not in seen_files:
|
||||||
|
seen_files.add(file_key)
|
||||||
|
attachment_values.append((0, 0, {
|
||||||
|
"name": attachment.name,
|
||||||
|
"requested_attachment_id": attachment.id,
|
||||||
|
"file": file_content,
|
||||||
|
"file_name": file_name,
|
||||||
|
}))
|
||||||
|
self.sudo().write({"joining_attachment_ids": [(5, 0, 0)] + attachment_values})
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get("name", "/") == "/":
|
||||||
|
vals["name"] = self.env["ir.sequence"].next_by_code("recruitment.employee.bridge") or "/"
|
||||||
|
if vals.get("employee_id") and not vals.get("company_id"):
|
||||||
|
employee = self.env["hr.employee"].browse(vals["employee_id"])
|
||||||
|
vals["company_id"] = employee.company_id.id or self.env.company.id
|
||||||
|
bridges = super().create(vals_list)
|
||||||
|
bridges._sync_from_employee()
|
||||||
|
return bridges
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if "employee_id" in vals:
|
||||||
|
self._sync_from_employee()
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_employee_bridge_user,employee.bridge.user,model_recruitment_employee_bridge,base.group_user,1,1,1,0
|
||||||
|
access_employee_bridge_manager,employee.bridge.manager,model_recruitment_employee_bridge,hr.group_hr_manager,1,1,1,1
|
||||||
|
access_employee_bridge_requested_attachment_user,employee.bridge.requested.attachment.user,model_employee_bridge_requested_attachment,base.group_user,1,1,1,0
|
||||||
|
access_employee_bridge_requested_attachment_manager,employee.bridge.requested.attachment.manager,model_employee_bridge_requested_attachment,hr.group_hr_manager,1,1,1,1
|
||||||
|
access_employee_bridge_attachment_user,employee.bridge.attachment.user,model_employee_bridge_attachment,base.group_user,1,1,1,0
|
||||||
|
access_employee_bridge_attachment_manager,employee.bridge.attachment.manager,model_employee_bridge_attachment,hr.group_hr_manager,1,1,1,1
|
||||||
|
access_employee_bridge_attachment_wizard,employee.bridge.attachment.wizard.user,model_employee_bridge_attachment_wizard,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,754 @@
|
||||||
|
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
import { assets, loadCSS, loadJS } from "@web/core/assets";
|
||||||
|
|
||||||
|
publicWidget.registry.employeeBridgeJod = publicWidget.Widget.extend({
|
||||||
|
selector: "#post_onboarding_form",
|
||||||
|
events: {
|
||||||
|
"change [name='candidate_image']": "previewApplicantPhoto",
|
||||||
|
"click #delete-photo-btn": "deleteCandidatePhoto",
|
||||||
|
"click #preview-photo-btn": "previewFullImage",
|
||||||
|
"click #add-education-row": "addEducationRow", // Ensure button click event is correctly bound
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'change .attachment-input': 'handleAttachmentUpload',
|
||||||
|
'click .upload-new-btn': 'handleUploadNewFile',
|
||||||
|
'click .delete-file-btn': 'handleDeleteUploadedFile',
|
||||||
|
'input .file-name-input': 'handleFileNameChange',
|
||||||
|
"click .remove-file": "removeFile",
|
||||||
|
"click .preview-file": "previewFile",
|
||||||
|
"click .view-attachments-btn": "openAttachmentModal", // Opens modal with files
|
||||||
|
"click .close-modal-btn": "closeAttachmentModal", // Close modal
|
||||||
|
"submit": "handleFormSubmit",
|
||||||
|
"change [name='experience']": "onChangeExperience",
|
||||||
|
"change [name='marital']": "onChangeMarital",
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.uploadedFiles = {}; // Store files per attachment ID for this form only.
|
||||||
|
this.isSubmitting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getFileSignature(file) {
|
||||||
|
return [file.name, file.size, file.lastModified, file.type].join("|");
|
||||||
|
},
|
||||||
|
|
||||||
|
addUploadedFileRow(attachmentId, file, base64String) {
|
||||||
|
console.log("addUploadedFileRow", attachmentId, file.name);
|
||||||
|
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
||||||
|
|
||||||
|
if (!this.uploadedFiles[attachmentId]) {
|
||||||
|
this.uploadedFiles[attachmentId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSignature = this._getFileSignature(file);
|
||||||
|
const isDuplicate = this.uploadedFiles[attachmentId].some(
|
||||||
|
uploadedFile => uploadedFile.signature === fileSignature
|
||||||
|
);
|
||||||
|
if (isDuplicate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique file ID using attachmentId and a timestamp
|
||||||
|
const fileId = `${attachmentId}-${Date.now()}-${this.uploadedFiles[attachmentId].length}`;
|
||||||
|
|
||||||
|
const fileRecord = {
|
||||||
|
attachment_rec_id : attachmentId,
|
||||||
|
id: fileId, // Unique file ID
|
||||||
|
name: file.name,
|
||||||
|
base64: base64String,
|
||||||
|
type: file.type,
|
||||||
|
signature: fileSignature,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uploadedFiles[attachmentId].push(fileRecord);
|
||||||
|
|
||||||
|
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
|
||||||
|
const previewImageId = `preview_image_${fileId}`;
|
||||||
|
const fileNameInputId = `file_name_input_${fileId}`;
|
||||||
|
let previewContent = '';
|
||||||
|
let previewClickHandler = '';
|
||||||
|
|
||||||
|
// Check if the file is an image or PDF and set preview content accordingly
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||||
|
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
|
||||||
|
</div>`;
|
||||||
|
previewClickHandler = () => {
|
||||||
|
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
||||||
|
this.$('#modal_attachment_photo_preview').show();
|
||||||
|
this.$('#modal_attachment_pdf_preview').hide(); // Hide PDF preview
|
||||||
|
this.$('#attachmentPreviewModal').modal('show');
|
||||||
|
};
|
||||||
|
} else if (file.type === 'application/pdf') {
|
||||||
|
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||||
|
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
|
||||||
|
</div>`;
|
||||||
|
previewClickHandler = () => {
|
||||||
|
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
||||||
|
this.$('#modal_attachment_pdf_preview').show();
|
||||||
|
this.$('#modal_attachment_photo_preview').hide(); // Hide image preview
|
||||||
|
this.$('#attachmentPreviewModal').modal('show');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new row to the table with a preview and buttons
|
||||||
|
tableBody.append(`
|
||||||
|
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control file-name-input" id="${fileNameInputId}" value="${file.name}"/>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="d-flex flex-column align-items-center justify-content-center gap-2">
|
||||||
|
${previewContent}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm delete-file-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm preview-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
|
||||||
|
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
||||||
|
|
||||||
|
// Attach click handler for preview (image or PDF)
|
||||||
|
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
|
||||||
|
|
||||||
|
// Attach click handler for the preview button (to trigger modal)
|
||||||
|
this.$(`.preview-btn[data-attachment-id="${attachmentId}"][data-file-id="${fileId}"]`).on('click', previewClickHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDeleteUploadedFile(ev) {
|
||||||
|
const button = ev.currentTarget;
|
||||||
|
const attachmentId = $(button).data('attachment-id');
|
||||||
|
const fileId = $(button).data('file-id');
|
||||||
|
|
||||||
|
// Find the index of the file to delete based on unique file ID
|
||||||
|
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (fileIndex !== -1) {
|
||||||
|
this.uploadedFiles[attachmentId].splice(fileIndex, 1); // Remove from array
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the row from DOM
|
||||||
|
this.$(`tr[data-file-id="${fileId}"]`).remove();
|
||||||
|
|
||||||
|
// Hide table if no files left
|
||||||
|
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||||
|
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
handleAttachmentUpload(ev) {
|
||||||
|
console.log("handleAttachmentUpload");
|
||||||
|
const input = ev.target;
|
||||||
|
const attachmentId = $(input).data('attachment-id');
|
||||||
|
|
||||||
|
if (input.files.length > 0) {
|
||||||
|
Array.from(input.files).forEach((file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64String = e.target.result.split(',')[1];
|
||||||
|
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$(input).val('');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUploadNewFile(ev) {
|
||||||
|
const button = $(ev.currentTarget);
|
||||||
|
const attachmentId = button.data('attachment-id');
|
||||||
|
const index = button.data('index');
|
||||||
|
const hiddenInput = this.$(`.upload-new-file-input[data-attachment-id='${attachmentId}']`);
|
||||||
|
|
||||||
|
hiddenInput.off('change').on('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const base64String = event.target.result.split(',')[1];
|
||||||
|
this.uploadedFiles[attachmentId][index] = {
|
||||||
|
name: file.name,
|
||||||
|
base64: base64String,
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageId = `preview_image_${attachmentId}_${index}`;
|
||||||
|
const filePreviewWrapperId = `preview_wrapper_${attachmentId}_${index}`;
|
||||||
|
const fileType = file.type;
|
||||||
|
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
this.$(`#${filePreviewWrapperId}`).html(`
|
||||||
|
<img id="${imageId}" src="data:image/png;base64,${base64String}" class="img-thumbnail"
|
||||||
|
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;" />
|
||||||
|
`);
|
||||||
|
} else if (fileType === 'application/pdf') {
|
||||||
|
this.$(`#${filePreviewWrapperId}`).html(`
|
||||||
|
<iframe src="data:application/pdf;base64,${base64String}" width="80" height="80" class="img-thumbnail" style="border: none; cursor: pointer;"></iframe>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNameInputId = `file_name_input_${attachmentId}_${index}`;
|
||||||
|
this.$(`#${fileNameInputId}`).val(file.name);
|
||||||
|
|
||||||
|
// CRITICAL FIX: Clear input after reading
|
||||||
|
$(e.target).val('');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
hiddenInput.trigger('click');
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
handleFileNameChange(event) {
|
||||||
|
const attachmentId = $(event.target).closest('tr').data('attachment-id');
|
||||||
|
const fileId = $(event.target).closest('tr').data('file-id');
|
||||||
|
const newFileName = event.target.value;
|
||||||
|
|
||||||
|
if (!attachmentId || !fileId) {
|
||||||
|
console.error('Missing attachmentId or fileId');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileList = this.uploadedFiles[attachmentId];
|
||||||
|
|
||||||
|
if (!fileList) {
|
||||||
|
console.error(`No files found for attachmentId: ${attachmentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileRecord = fileList.find(file => file.id === fileId);
|
||||||
|
|
||||||
|
if (!fileRecord) {
|
||||||
|
console.error(`File with ID ${fileId} not found under attachment ${attachmentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileRecord.name = newFileName;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
renderFilePreview(attachmentId) {
|
||||||
|
const container = this.$(`#preview_container_${attachmentId}`);
|
||||||
|
container.empty();
|
||||||
|
|
||||||
|
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||||
|
container.addClass("d-none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.removeClass("d-none");
|
||||||
|
|
||||||
|
this.uploadedFiles[attachmentId].forEach((file, index) => {
|
||||||
|
const fileHtml = $(`
|
||||||
|
<div class="d-flex flex-column align-items-center">
|
||||||
|
<div class="position-relative">
|
||||||
|
<img src="${file.base64}" class="rounded-circle shadow-sm" style="width: 80px; height: 80px; object-fit: cover; border: 1px solid #ddd; cursor: pointer;" data-index="${index}" data-attachment-id="${attachmentId}" />
|
||||||
|
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 remove-file" data-attachment-id="${attachmentId}" data-index="${index}" style="transform: translate(50%, -50%);"><i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
<small>${file.name}</small>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
fileHtml.find("img").on("click", this.previewAttachmentImage.bind(this));
|
||||||
|
fileHtml.find(".remove-file").on("click", this.removeFile.bind(this));
|
||||||
|
|
||||||
|
container.append(fileHtml);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
previewAttachmentImage(ev) {
|
||||||
|
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||||
|
const index = $(ev.currentTarget).data("index");
|
||||||
|
const fileData = this.uploadedFiles[attachmentId][index];
|
||||||
|
|
||||||
|
this.$("#attachment_modal_preview").attr("src", fileData.base64);
|
||||||
|
this.$("#attachmentPreviewModal").modal("show");
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFile(ev) {
|
||||||
|
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||||
|
const index = $(ev.currentTarget).data("index");
|
||||||
|
|
||||||
|
this.uploadedFiles[attachmentId].splice(index, 1);
|
||||||
|
this.renderFilePreview(attachmentId);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
previewFile(ev) {
|
||||||
|
const fileUrl = ev.currentTarget.dataset.fileUrl;
|
||||||
|
const modal = this.$("#photoPreviewModal");
|
||||||
|
|
||||||
|
this.$("#modal_photo_preview").attr("src", fileUrl);
|
||||||
|
modal.modal("show");
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeExperience(event) {
|
||||||
|
const selectedValue = $(event.currentTarget).val();
|
||||||
|
const employerHistorySection = $("#employer_history_data");
|
||||||
|
|
||||||
|
if (selectedValue === "experienced") {
|
||||||
|
employerHistorySection.show();
|
||||||
|
} else {
|
||||||
|
employerHistorySection.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeMarital(event) {
|
||||||
|
const selectedValue = $(event.currentTarget).val();
|
||||||
|
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
|
||||||
|
const family_details_data_spouse = this.$('#family_details_data_spouse')
|
||||||
|
const family_details_data_kid1 = this.$('#family_details_data_kid1')
|
||||||
|
const family_details_data_kid2 = this.$('#family_details_data_kid2')
|
||||||
|
|
||||||
|
|
||||||
|
if (selectedValue === "married") {
|
||||||
|
marriageAnniversarySection.show();
|
||||||
|
// Show rows for spouse, kid1, and kid2
|
||||||
|
family_details_data_spouse.show();
|
||||||
|
family_details_data_kid1.show();
|
||||||
|
family_details_data_kid2.show();
|
||||||
|
} else {
|
||||||
|
marriageAnniversarySection.hide();
|
||||||
|
// Hide rows for spouse, kid1, and kid2
|
||||||
|
family_details_data_spouse.hide();
|
||||||
|
family_details_data_kid1.hide();
|
||||||
|
family_details_data_kid2.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open modal and display uploaded files
|
||||||
|
*/
|
||||||
|
openAttachmentModal(event) {
|
||||||
|
console.log("openAttachmentModal");
|
||||||
|
const rowId = $(event.currentTarget).closest("tr").index(); // Get the row index
|
||||||
|
this.currentRowId = rowId; // Store rowId for reference
|
||||||
|
this.renderAttachmentModal(rowId); // Render the modal for the row
|
||||||
|
},
|
||||||
|
|
||||||
|
renderAttachmentModal(rowId) {
|
||||||
|
const fileList = this.uploadedFiles && this.uploadedFiles[rowId] ? this.uploadedFiles[rowId] : [];
|
||||||
|
|
||||||
|
let modalHtml = `
|
||||||
|
<div id="attachmentModal" class="modal fade show" tabindex="-1" style="display: block; background: rgba(0,0,0,0.5);" aria-modal="true">
|
||||||
|
<div class="modal-dialog modal-lg" style="max-width: 600px;">
|
||||||
|
<div class="modal-content" style="border-radius: 8px; box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);">
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="modal-header" style="background: #143d5d; color: white; border-bottom: 2px solid #dee2e6; font-weight: bold; padding: 12px 15px;">
|
||||||
|
<h5 class="modal-title" style="margin: 0;">Uploaded Attachments</h5>
|
||||||
|
<button type="button" class="close close-modal-btn" data-dismiss="modal" aria-label="Close" style="background: none; border: none; font-size: 20px; color: white; cursor: pointer;">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="modal-body" style="padding: 15px;">
|
||||||
|
<ul class="attachment-list" style="list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto;">
|
||||||
|
${fileList.length ? fileList.map((file, index) => `
|
||||||
|
<li style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa;">
|
||||||
|
<span style="font-weight: 500; flex-grow: 1;">${file.name}</span>
|
||||||
|
<button type="button" class="remove-file" data-index="${index}"
|
||||||
|
style="background: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;">
|
||||||
|
<i style="margin-right: 5px;">🗑️</i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
`).join("") : `<p style="color: #6c757d; text-align: center; font-size: 16px; padding: 10px;">No files uploaded.</p>`}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 12px;">
|
||||||
|
<button type="button" class="btn close-modal-btn" data-dismiss="modal"
|
||||||
|
style="background: #6c757d; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Remove old modal and append new one
|
||||||
|
$("#attachmentModal").remove();
|
||||||
|
$("body").append(modalHtml);
|
||||||
|
|
||||||
|
// Attach remove event to new modal content
|
||||||
|
$("#attachmentModal").on("click", ".remove-file", this.removeFile.bind(this));
|
||||||
|
$("#attachmentModal").on("click", ".close-modal-btn", this.closeAttachmentModal.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close attachment modal
|
||||||
|
*/
|
||||||
|
closeAttachmentModal() {
|
||||||
|
console.log("Closing modal");
|
||||||
|
$("#attachmentModal").remove(); // Remove the modal from the DOM
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Function to preview the uploaded image
|
||||||
|
previewApplicantPhoto(ev) {
|
||||||
|
const input = ev.currentTarget;
|
||||||
|
const preview = this.$("#photo_preview");
|
||||||
|
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
|
||||||
|
preview.attr("src", e.target.result);
|
||||||
|
|
||||||
|
// Store the base64 in a hidden input field
|
||||||
|
this.$("input[name='candidate_image_base64']").val(base64String);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to delete the uploaded image
|
||||||
|
deleteCandidatePhoto() {
|
||||||
|
const preview = this.$("#photo_preview");
|
||||||
|
const inputFile = this.$("input[name='candidate_image']");
|
||||||
|
|
||||||
|
preview.attr("src", "data:image/png;base64,"); // Reset preview
|
||||||
|
inputFile.val(""); // Reset file input
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to preview full image inside a modal
|
||||||
|
previewFullImage() {
|
||||||
|
const previewSrc = this.$("#photo_preview").attr("src");
|
||||||
|
if (previewSrc) {
|
||||||
|
this.$("#modal_photo_preview").attr("src", previewSrc);
|
||||||
|
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to add a new education row dynamically
|
||||||
|
addEducationRow(ev) {
|
||||||
|
ev.preventDefault(); // Prevent default behavior
|
||||||
|
|
||||||
|
let newRow = `
|
||||||
|
<tr class="predefined-row">
|
||||||
|
<td class="education-relation-col">
|
||||||
|
<select name="education_type" class="form-control">
|
||||||
|
<option value="10">10th</option>
|
||||||
|
<option value="inter">Inter</option>
|
||||||
|
<option value="graduation">Graduation</option>
|
||||||
|
<option value="post_graduation">Post Graduation</option>
|
||||||
|
<option value="additional">Additional Qualification</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td><input type="text" name="specialization" class="form-control" placeholder="Enter Specialization"/></td>
|
||||||
|
<td><input type="text" name="university" class="form-control" placeholder="Enter University"/></td>
|
||||||
|
<td><input type="number" name="start_year" class="form-control" placeholder="Start Year"/></td>
|
||||||
|
<td><input type="number" name="end_year" class="form-control" placeholder="End Year"/></td>
|
||||||
|
<td><input type="text" name="marks_grade" class="form-control" placeholder="Marks/Grade"/></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.$("#education_details_data tbody").append(newRow); // Append new row inside the table
|
||||||
|
console.log("New education row added!");
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
validateFamilyDetails() {
|
||||||
|
let familyDetailsFilled = false;
|
||||||
|
let educationDetailsFilled = false;
|
||||||
|
const family_rows = document.querySelectorAll('#family_details_data tbody tr');
|
||||||
|
const education_rows = document.querySelectorAll('#education_details_data tbody tr');
|
||||||
|
|
||||||
|
|
||||||
|
family_rows.forEach(row => {
|
||||||
|
const inputs = row.querySelectorAll('input[type="text"], input[type="date"]');
|
||||||
|
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
|
||||||
|
if (isRowFilled) {
|
||||||
|
familyDetailsFilled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
education_rows.forEach(row => {
|
||||||
|
const inputs = row.querySelectorAll('input[type="text"]');
|
||||||
|
const isRowFilled = Array.from(inputs).some(input => input.value.trim() !== "");
|
||||||
|
if (isRowFilled) {
|
||||||
|
educationDetailsFilled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!familyDetailsFilled) {
|
||||||
|
alert('Please fill at least one family member detail.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!educationDetailsFilled) {
|
||||||
|
alert('Please fill at least one Education details')
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFormSubmit(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (this.isSubmitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateFamilyDetails()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSubmitting = true;
|
||||||
|
this.$("#submit-btn").prop("disabled", true);
|
||||||
|
|
||||||
|
let employerHistoryData = [];
|
||||||
|
this.$("#employer_history_data tbody tr").each((index, row) => {
|
||||||
|
let rowData = {
|
||||||
|
company_name: this.$(row).find("[name$='company_name']").val()?.trim() || "",
|
||||||
|
designation: this.$(row).find("[name$='designation']").val()?.trim() || "",
|
||||||
|
date_of_joining: this.$(row).find("[name$='doj']").val()?.trim() || "",
|
||||||
|
last_working_day: this.$(row).find("[name$='lwd']").val()?.trim() || "",
|
||||||
|
ctc: this.$(row).find("[name$='ctc']").val()?.trim() || "",
|
||||||
|
};
|
||||||
|
if (Object.values(rowData).some(value => value)) {
|
||||||
|
employerHistoryData.push(rowData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let familyData = [];
|
||||||
|
this.$("#family_details_data tbody tr").each((index, row) => {
|
||||||
|
let rowData = {
|
||||||
|
relation: this.$(row).find(".relation-col").attr("value")?.trim(),
|
||||||
|
name: this.$(row).find("[name$='_name']").val()?.trim() || "",
|
||||||
|
contact: this.$(row).find("[name$='_contact']").val()?.trim() || "",
|
||||||
|
dob: this.$(row).find("[name$='_dob']").val()?.trim() || "",
|
||||||
|
location: this.$(row).find("[name$='_location']").val()?.trim() || "",
|
||||||
|
};
|
||||||
|
if (Object.entries(rowData).some(([key, value]) => key !== 'relation' && value)) {
|
||||||
|
familyData.push(rowData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let educationData = [];
|
||||||
|
this.$("#education_details_data tbody tr").each((index, row) => {
|
||||||
|
let educationCell = this.$(row).find(".education-relation-col");
|
||||||
|
let educationType = educationCell.find("select").val()?.trim() || educationCell.attr("value")?.trim();
|
||||||
|
|
||||||
|
let rowData = {
|
||||||
|
education_type: educationType,
|
||||||
|
specialization: this.$(row).find("[name='specialization']").val()?.trim() || "",
|
||||||
|
university: this.$(row).find("[name='university']").val()?.trim() || "",
|
||||||
|
start_year: this.$(row).find("[name='start_year']").val()?.trim() || "",
|
||||||
|
end_year: this.$(row).find("[name='end_year']").val()?.trim() || "",
|
||||||
|
marks_or_grade: this.$(row).find("[name='marks_grade']").val()?.trim() || "",
|
||||||
|
};
|
||||||
|
if (Object.entries(rowData).some(([key, value]) => key !== 'education_type' && value)) {
|
||||||
|
educationData.push(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
let attachments = [];
|
||||||
|
let fileReadPromises = [];
|
||||||
|
let attachmentInputs = this.uploadedFiles; // your object {1: Array(1), 2: Array(2)}
|
||||||
|
|
||||||
|
Object.keys(attachmentInputs).forEach(key => {
|
||||||
|
let filesArray = attachmentInputs[key];
|
||||||
|
if (filesArray && Array.isArray(filesArray)) {
|
||||||
|
filesArray.forEach(file => {
|
||||||
|
// Only push if base64 actually exists (prevents ghost/empty data)
|
||||||
|
if (file && file.base64) {
|
||||||
|
attachments.push({
|
||||||
|
attachment_rec_id: file.attachment_rec_id,
|
||||||
|
file_name: file.name,
|
||||||
|
file_content: file.base64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Promise.all(fileReadPromises).then(() => {
|
||||||
|
this.$("#family_data_json").val(JSON.stringify(familyData));
|
||||||
|
this.$("#education_data_json").val(JSON.stringify(educationData));
|
||||||
|
this.$("#employer_history_data_json").val(JSON.stringify(employerHistoryData));
|
||||||
|
this.$("#attachments_data_json").val(JSON.stringify(attachments));
|
||||||
|
|
||||||
|
let formElement = this.$el.is("form") ? this.$el[0] : this.$el.find("form")[0];
|
||||||
|
if (formElement) {
|
||||||
|
formElement.submit();
|
||||||
|
} else {
|
||||||
|
console.error("Form element not found.");
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Error reading files:", error);
|
||||||
|
this.isSubmitting = false;
|
||||||
|
this.$("#submit-btn").prop("disabled", false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Enforce required fields when any field in a row is filled
|
||||||
|
enforceRowValidation() {
|
||||||
|
// Education Details Validation - Make fields required when typing
|
||||||
|
this.$("#education_details_data").on("input", "tbody tr input", function () {
|
||||||
|
let row = $(this).closest("tr");
|
||||||
|
let inputs = row.find("input");
|
||||||
|
|
||||||
|
// Check if at least one field has a value
|
||||||
|
let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
|
||||||
|
|
||||||
|
if (isFilled) {
|
||||||
|
inputs.attr("required", true);
|
||||||
|
} else {
|
||||||
|
inputs.removeAttr("required"); // Remove required if all fields are empty
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove empty rows only when the user leaves the last input field
|
||||||
|
this.$("#education_details_data").on("blur", "tbody tr input", function () {
|
||||||
|
let row = $(this).closest("tr");
|
||||||
|
let inputs = row.find("input");
|
||||||
|
|
||||||
|
if (row.hasClass("predefined-row")) {
|
||||||
|
let isEmpty = inputs.toArray().every(input => $(input).val().trim() === "");
|
||||||
|
if (isEmpty) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Family Details Validation
|
||||||
|
// this.$("#family_details_data").on("input", "tbody tr input", function () {
|
||||||
|
// let row = $(this).closest("tr");
|
||||||
|
// let inputs = row.find("input");
|
||||||
|
//
|
||||||
|
// let isFilled = inputs.toArray().some(input => $(input).val().trim() !== "");
|
||||||
|
//
|
||||||
|
// if (isFilled) {
|
||||||
|
// inputs.attr("required", true);
|
||||||
|
// } else {
|
||||||
|
// inputs.removeAttr("required");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Remove row functionality (Manual delete using button)
|
||||||
|
this.$("#education_details_data").on("click", ".remove-edu-row", function () {
|
||||||
|
$(this).closest("tr").remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$("#family_details_data").on("click", ".remove-family-row", function () {
|
||||||
|
$(this).closest("tr").remove();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async _renderStateIds() {
|
||||||
|
console.log("Fetching States...");
|
||||||
|
|
||||||
|
const country_id = $('#present_state_ids_container').data('country_id');
|
||||||
|
|
||||||
|
const state_ids = await rpc("/employee_bridge/fetch_related_state_ids", {
|
||||||
|
country_id: country_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const present_state_ids_container = $("#present_state_ids_container");
|
||||||
|
const permanent_state_ids_container = $("#permanent_state_ids_container");
|
||||||
|
|
||||||
|
present_state_ids_container.empty();
|
||||||
|
permanent_state_ids_container.empty();
|
||||||
|
|
||||||
|
console.log(state_ids);
|
||||||
|
|
||||||
|
if (typeof state_ids === 'object' && !Array.isArray(state_ids)) {
|
||||||
|
const stateOptions = Object.entries(state_ids).map(([id, name]) => `
|
||||||
|
<option value="${id}">${name}</option>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const stateHtml = `
|
||||||
|
<select name='permanent_state' id="permanent_state" class="form-control" required>
|
||||||
|
<option value="" disabled selected>Select State</option>
|
||||||
|
${stateOptions}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const presentStateHtml = `
|
||||||
|
<select name='present_state' id="present_state" class="form-control" required>
|
||||||
|
<option value="" disabled selected>Select State</option>
|
||||||
|
${stateOptions}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
|
||||||
|
permanent_state_ids_container.append(stateHtml);
|
||||||
|
present_state_ids_container.append(presentStateHtml);
|
||||||
|
} else {
|
||||||
|
console.error("Expected an object like {id: name}, but got:", state_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Hello World");
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this._renderStateIds();
|
||||||
|
|
||||||
|
// Ensure form submit event is properly bound
|
||||||
|
// this.$el.on("submit", this.handleFormSubmit.bind(this));
|
||||||
|
|
||||||
|
// Bind validation enforcement
|
||||||
|
this.enforceRowValidation();
|
||||||
|
const selectedExperience = this.$("[name='experience']:checked").val();
|
||||||
|
const selectedMarital = this.$("[name='marital']:checked").val();
|
||||||
|
|
||||||
|
const employerHistorySection = this.$("#employer_history_data");
|
||||||
|
const marriageAnniversarySection = this.$('#marriage_anniversary_date_div')
|
||||||
|
|
||||||
|
const family_details_data_spouse = this.$('#family_details_data_spouse')
|
||||||
|
const family_details_data_kid1 = this.$('#family_details_data_kid1')
|
||||||
|
const family_details_data_kid2 = this.$('#family_details_data_kid2')
|
||||||
|
|
||||||
|
|
||||||
|
if (selectedExperience === "experienced") {
|
||||||
|
employerHistorySection.show();
|
||||||
|
} else {
|
||||||
|
employerHistorySection.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMarital === 'married') {
|
||||||
|
marriageAnniversarySection.show();
|
||||||
|
// Show rows for spouse, kid1, and kid2 if married
|
||||||
|
family_details_data_spouse.show();
|
||||||
|
family_details_data_kid1.show();
|
||||||
|
family_details_data_kid2.show();
|
||||||
|
} else {
|
||||||
|
marriageAnniversarySection.hide();
|
||||||
|
// Show rows for spouse, kid1, and kid2 if married
|
||||||
|
family_details_data_spouse.hide();
|
||||||
|
family_details_data_kid1.hide();
|
||||||
|
family_details_data_kid2.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_employee_view_form_bridge" model="ir.ui.view">
|
||||||
|
<field name="name">hr.employee.view.form.bridge</field>
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="action_open_recruitment_bridge"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-link"
|
||||||
|
groups="hr.group_hr_user"
|
||||||
|
invisible="not recruitment_bridge_count">
|
||||||
|
<field name="recruitment_bridge_count" widget="statinfo" string="Onboarding"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//label[@for='user_id']" position="before">
|
||||||
|
<button string="Send JOD Form"
|
||||||
|
name="send_jod_form_to_employee"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
groups="hr.group_hr_user"
|
||||||
|
invisible="not work_email"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_bridge_requested_attachment_list" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.requested.attachment.list</field>
|
||||||
|
<field name="model">employee.bridge.requested.attachment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attachment_type"/>
|
||||||
|
<field name="is_default"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_employee_bridge_requested_attachment_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.requested.attachment.form</field>
|
||||||
|
<field name="model">employee.bridge.requested.attachment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attachment_type"/>
|
||||||
|
<field name="is_default"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_employee_bridge_requested_attachment" model="ir.actions.act_window">
|
||||||
|
<field name="name">Onboarding Documents</field>
|
||||||
|
<field name="res_model">employee.bridge.requested.attachment</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_bridge_requested_attachment"
|
||||||
|
name="Onboarding Documents"
|
||||||
|
parent="menu_employee_bridge"
|
||||||
|
action="action_employee_bridge_requested_attachment"
|
||||||
|
sequence="20"
|
||||||
|
groups="hr.group_hr_user"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_bridge_list" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.list</field>
|
||||||
|
<field name="model">recruitment.employee.bridge</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="employee_code"/>
|
||||||
|
<field name="work_email"/>
|
||||||
|
<field name="joining_form_link" optional="show"/>
|
||||||
|
<field name="source"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_employee_bridge_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.form</field>
|
||||||
|
<field name="model">recruitment.employee.bridge</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<header>
|
||||||
|
<button string="Send JOD Form" name="action_send_jod_form" type="object" class="btn-primary" invisible="state == 'validated'"/>
|
||||||
|
<button string="Create Employee" name="action_create_employee" type="object" class="btn-secondary" invisible="employee_id"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,jod_sent,jod_received,validated"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_open_employee" type="object" class="oe_stat_button" icon="fa-id-card-o" invisible="not employee_id">
|
||||||
|
<div class="o_field_widget o_stat_info">
|
||||||
|
<span class="o_stat_value"><field name="employee_code" readonly="1"/></span>
|
||||||
|
<span class="o_stat_text">Employee</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<group string="Onboarding">
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="source"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="employee_code"/>
|
||||||
|
<field name="joining_form_link" widget="url"/>
|
||||||
|
<field name="requested_attachment_ids" widget="many2many_tags"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<button string="Validate Into Employee" name="action_validate_employee_details" type="object" class="btn-primary" invisible="state == 'validated' or not employee_id"/>
|
||||||
|
<notebook>
|
||||||
|
<page string="Basic Details" name="basic_details">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_name"/>
|
||||||
|
<field name="candidate_image" widget="image" class="oe_avatar"/>
|
||||||
|
<field name="employee_code"/>
|
||||||
|
<field name="work_email" widget="email"/>
|
||||||
|
<field name="private_email" widget="email"/>
|
||||||
|
<field name="work_phone" widget="phone"/>
|
||||||
|
<field name="mobile_phone" widget="phone"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="job_id"/>
|
||||||
|
<field name="emp_type"/>
|
||||||
|
<field name="total_exp"/>
|
||||||
|
<field name="doj"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="JOD Details" name="jod_details">
|
||||||
|
<group string="Personal Details">
|
||||||
|
<group>
|
||||||
|
<field name="gender"/>
|
||||||
|
<field name="birthday"/>
|
||||||
|
<field name="blood_group"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="marital"/>
|
||||||
|
<field name="marriage_anniversary_date" invisible="marital == 'single'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Current Address">
|
||||||
|
<group>
|
||||||
|
<field name="private_street"/>
|
||||||
|
<field name="private_street2"/>
|
||||||
|
<field name="private_city"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="private_state_id"/>
|
||||||
|
<field name="private_zip"/>
|
||||||
|
<field name="private_country_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Permanent Address">
|
||||||
|
<group>
|
||||||
|
<field name="permanent_street"/>
|
||||||
|
<field name="permanent_street2"/>
|
||||||
|
<field name="permanent_city"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="permanent_state_id"/>
|
||||||
|
<field name="permanent_zip"/>
|
||||||
|
<field name="permanent_country_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Authentication">
|
||||||
|
<group>
|
||||||
|
<field name="pan_no"/>
|
||||||
|
<field name="identification_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="previous_company_pf_no"/>
|
||||||
|
<field name="previous_company_uan_no"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Passport">
|
||||||
|
<group>
|
||||||
|
<field name="passport_no"/>
|
||||||
|
<field name="passport_start_date"/>
|
||||||
|
<field name="passport_end_date"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="passport_issued_location"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Family Details" name="family_details">
|
||||||
|
<field name="family_details" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="relation_type"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="contact_no"/>
|
||||||
|
<field name="dob"/>
|
||||||
|
<field name="location"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Employer History" name="employer_history">
|
||||||
|
<field name="employer_history" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="company_name"/>
|
||||||
|
<field name="designation"/>
|
||||||
|
<field name="date_of_joining"/>
|
||||||
|
<field name="last_working_day"/>
|
||||||
|
<field name="ctc"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Education Details" name="education_history">
|
||||||
|
<field name="education_history" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="education_type"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="university"/>
|
||||||
|
<field name="start_year"/>
|
||||||
|
<field name="end_year"/>
|
||||||
|
<field name="marks_or_grade"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Attachments" name="attachments">
|
||||||
|
<field name="joining_attachment_ids" nolabel="1">
|
||||||
|
<list editable="bottom" default_group_by="requested_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
|
||||||
|
<field name="requested_attachment_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attachment_type"/>
|
||||||
|
<field name="file" widget="binary" filename="file_name"/>
|
||||||
|
<field name="file_name" optional="hide"/>
|
||||||
|
<button name="action_preview_file" type="object" string="Preview" class="oe_highlight" icon="fa-eye"/>
|
||||||
|
<field name="review_status"/>
|
||||||
|
<field name="review_comments" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_employee_bridge_search" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.search</field>
|
||||||
|
<field name="model">recruitment.employee.bridge</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<filter name="jod_sent" string="JOD Sent" domain="[('state', '=', 'jod_sent')]"/>
|
||||||
|
<filter name="jod_received" string="JOD Received" domain="[('state', '=', 'jod_received')]"/>
|
||||||
|
<filter name="validated" string="Validated" domain="[('state', '=', 'validated')]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
|
||||||
|
<filter name="group_source" string="Source" context="{'group_by': 'source'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_employee_bridge" model="ir.actions.act_window">
|
||||||
|
<field name="name">Onboarding</field>
|
||||||
|
<field name="res_model">recruitment.employee.bridge</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_bridge"
|
||||||
|
name="Onboarding"
|
||||||
|
parent="hr.menu_hr_root"
|
||||||
|
sequence="90"
|
||||||
|
groups="hr.group_hr_user"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_bridge_onboarding"
|
||||||
|
name="Onboarding"
|
||||||
|
parent="menu_employee_bridge"
|
||||||
|
action="action_employee_bridge"
|
||||||
|
sequence="10"
|
||||||
|
groups="hr.group_hr_user"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import employee_bridge_attachment_wizard
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBridgeAttachmentWizard(models.TransientModel):
|
||||||
|
_name = "employee.bridge.attachment.wizard"
|
||||||
|
_description = "Onboarding Attachment Request Wizard"
|
||||||
|
|
||||||
|
bridge_id = fields.Many2one("recruitment.employee.bridge", required=True, readonly=True)
|
||||||
|
req_attachment_ids = fields.Many2many(
|
||||||
|
comodel_name="employee.bridge.requested.attachment",
|
||||||
|
relation="employee_bridge_req_attachment_rel", # relation table name
|
||||||
|
column1="request_id", # column referring to this model
|
||||||
|
column2="attachment_id", # column referring to the comodel
|
||||||
|
string="Attachments to Request",
|
||||||
|
)
|
||||||
|
attachment_ids = fields.Many2many("ir.attachment")
|
||||||
|
template_id = fields.Many2one(
|
||||||
|
"mail.template",
|
||||||
|
default=lambda self: self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False),
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
email_from = fields.Char("Email From")
|
||||||
|
email_to = fields.Char("Email To")
|
||||||
|
email_cc = fields.Text("Email CC")
|
||||||
|
email_subject = fields.Char()
|
||||||
|
email_body = fields.Html(
|
||||||
|
"Body",
|
||||||
|
render_engine="qweb",
|
||||||
|
render_options={"post_process": True},
|
||||||
|
prefetch=True,
|
||||||
|
translate=True,
|
||||||
|
sanitize="email_outgoing",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
defaults = super().default_get(fields_list)
|
||||||
|
bridge = self.env["recruitment.employee.bridge"].browse(
|
||||||
|
self.env.context.get("default_bridge_id") or self.env.context.get("bridge_id")
|
||||||
|
)
|
||||||
|
if bridge:
|
||||||
|
attachments = bridge.requested_attachment_ids or self.env["employee.bridge.requested.attachment"].search([
|
||||||
|
("is_default", "=", True),
|
||||||
|
])
|
||||||
|
defaults.update({
|
||||||
|
"bridge_id": bridge.id,
|
||||||
|
"req_attachment_ids": [(6, 0, attachments.ids)],
|
||||||
|
"email_from": self.env.company.email or self.env.user.email,
|
||||||
|
"email_to": bridge.work_email or bridge.private_email or bridge.employee_id.work_email,
|
||||||
|
})
|
||||||
|
template = self.env.ref("employee_bridge.email_template_jod_form", raise_if_not_found=False)
|
||||||
|
if template:
|
||||||
|
context = {
|
||||||
|
"joining_form_link": bridge._get_joining_form_link(),
|
||||||
|
**bridge._group_requested_attachments(attachments),
|
||||||
|
}
|
||||||
|
defaults.update({
|
||||||
|
"template_id": template.id,
|
||||||
|
"email_subject": template.with_context(**context)._render_field("subject", [bridge.id])[bridge.id],
|
||||||
|
"email_body": template.with_context(**context)._render_field(
|
||||||
|
"body_html", [bridge.id], compute_lang=True
|
||||||
|
)[bridge.id],
|
||||||
|
})
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
@api.onchange("req_attachment_ids")
|
||||||
|
def _onchange_req_attachment_ids(self):
|
||||||
|
if not self.bridge_id or not self.template_id:
|
||||||
|
return
|
||||||
|
context = {
|
||||||
|
"joining_form_link": self.bridge_id._get_joining_form_link(),
|
||||||
|
**self.bridge_id._group_requested_attachments(self.req_attachment_ids),
|
||||||
|
}
|
||||||
|
self.email_body = self.template_id.with_context(**context)._render_field(
|
||||||
|
"body_html", [self.bridge_id.id], compute_lang=True
|
||||||
|
)[self.bridge_id.id]
|
||||||
|
|
||||||
|
def action_confirm(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self.bridge_id.requested_attachment_ids = [(6, 0, self.req_attachment_ids.ids)]
|
||||||
|
self.bridge_id._send_jod_mail(
|
||||||
|
req_attachment_ids=self.req_attachment_ids,
|
||||||
|
email_values={
|
||||||
|
"email_from": self.email_from,
|
||||||
|
"email_to": self.email_to,
|
||||||
|
"email_cc": self.email_cc,
|
||||||
|
"subject": self.email_subject,
|
||||||
|
"attachment_ids": [(6, 0, self.attachment_ids.ids)],
|
||||||
|
},
|
||||||
|
email_body=self.email_body,
|
||||||
|
)
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_bridge_attachment_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.bridge.attachment.wizard.form</field>
|
||||||
|
<field name="model">employee.bridge.attachment.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Select Attachments">
|
||||||
|
<group>
|
||||||
|
<field name="bridge_id" invisible="1"/>
|
||||||
|
<field name="req_attachment_ids" widget="many2many_tags" force_save="1"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page name="attachment" string="Attachments (Binary)">
|
||||||
|
<field name="attachment_ids" widget="many2many_binary" domain="[('mimetype', 'not ilike', 'image')]"/>
|
||||||
|
</page>
|
||||||
|
<page name="email" string="Email">
|
||||||
|
<group>
|
||||||
|
<field name="template_id" options="{'no_create': True}" readonly="1" force_save="1"/>
|
||||||
|
<field name="email_from" force_save="1"/>
|
||||||
|
<field name="email_to" force_save="1"/>
|
||||||
|
<field name="email_cc"/>
|
||||||
|
<field name="email_subject" force_save="1"/>
|
||||||
|
<field name="email_body" widget="html_mail" class="oe-bordered-editor" options="{'codeview': true}" force_save="1"/>
|
||||||
|
<field name="attachment_ids" widget="many2many_tags" string="Attachments" force_save="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
<footer>
|
||||||
|
<button name="action_confirm" type="object" string="Send JOD Form" class="btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -10,12 +10,10 @@
|
||||||
""",
|
""",
|
||||||
'author': 'FTPROTECH',
|
'author': 'FTPROTECH',
|
||||||
'website': 'https://ftprotech.in',
|
'website': 'https://ftprotech.in',
|
||||||
'depends': ['hr', 'mail','hr_employee_extended','hr_recruitment_extended'],
|
'depends': ['hr', 'mail', 'hr_employee_extended', 'employee_bridge'],
|
||||||
'data': [
|
'data': [
|
||||||
'data/data.xml',
|
'data/actions.xml',
|
||||||
'data/actions.xml',
|
'data/template.xml',
|
||||||
'data/template.xml',
|
],
|
||||||
'views/emp_jod.xml',
|
|
||||||
],
|
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,22 @@
|
||||||
from odoo import http, _
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.hr_recruitment_extended.controllers.controllers import website_hr_recruitment_applications
|
from odoo.tools import misc
|
||||||
from odoo.http import content_disposition
|
|
||||||
|
|
||||||
import logging
|
class EmployeeJodController(http.Controller):
|
||||||
|
@http.route("/download/employee_jod/<int:employee_id>", type="http", auth="user")
|
||||||
from odoo.tools import misc
|
def download_employee_jod_form(self, employee_id, **kwargs):
|
||||||
|
employee = request.env["hr.employee"].sudo().browse(employee_id)
|
||||||
_logger = logging.getLogger(__name__)
|
if not employee.exists():
|
||||||
|
return request.not_found()
|
||||||
class website_hr_recruitment_applications_extended(website_hr_recruitment_applications):
|
|
||||||
|
template = request.env.ref("employee_jod.emp_joining_form_template", raise_if_not_found=False)
|
||||||
@http.route(['/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
if not template:
|
||||||
website=True)
|
return "Error: Template not found"
|
||||||
def post_onboarding_form(self, applicant_id, **kwargs):
|
|
||||||
"""Renders the website form for applicants to submit additional details."""
|
return request.env["ir.qweb"]._render(template.id, {
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
"docs": employee,
|
||||||
if not applicant.exists():
|
"doc": employee,
|
||||||
return request.not_found()
|
"time": misc.datetime,
|
||||||
if applicant:
|
"user": request.env.user,
|
||||||
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)}"
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,8 @@
|
||||||
<field name="report_file">employee_jod.emp_joining_form_template</field>
|
<field name="report_file">employee_jod.emp_joining_form_template</field>
|
||||||
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
||||||
<field name="print_report_name">'JOD - %s' % (object.display_name)</field>
|
<field name="print_report_name">'JOD - %s' % (object.display_name)</field>
|
||||||
<field name="paperformat_id" ref="hr_recruitment_extended.custom_paper_format"/>
|
<field name="binding_type">report</field>
|
||||||
<field name="binding_type">report</field>
|
</record>
|
||||||
</record>
|
|
||||||
|
|
||||||
</data>
|
</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>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <template id="thank_you_template_inherit" inherit_id="hr_recruitment_exteded.thank_you_template" name="Thank You Template Extended">-->
|
</odoo>
|
||||||
<!-- <xpath expr="//div[@class='container mt-5 text-center']" position="inside">-->
|
|
||||||
<!-- <div t-if="applicant.post_onboarding_form_status == 'done'" style="margin-top: 20px;">-->
|
|
||||||
<!-- <a t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </xpath>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
|
|
|
||||||
|
|
@ -1,165 +1,8 @@
|
||||||
from odoo import api, fields, models, _
|
from odoo import models
|
||||||
from odoo.exceptions import UserError
|
|
||||||
|
|
||||||
|
class HrEmployee(models.Model):
|
||||||
class HRApplicant(models.Model):
|
_inherit = "hr.employee"
|
||||||
_inherit = 'hr.applicant'
|
|
||||||
|
def send_jod_form_to_employee(self):
|
||||||
joining_form_link = fields.Char()
|
return super().send_jod_form_to_employee()
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -28,7 +28,10 @@
|
||||||
'data/data.xml',
|
'data/data.xml',
|
||||||
'data/sequence.xml',
|
'data/sequence.xml',
|
||||||
'data/mail_template.xml',
|
'data/mail_template.xml',
|
||||||
'data/templates.xml',
|
# 'data/templates.xml',
|
||||||
|
'views/res_config_settings.xml',
|
||||||
|
'views/survey_survey.xml',
|
||||||
|
'views/hr_recruitment_category.xml',
|
||||||
'views/submission_share_history.xml',
|
'views/submission_share_history.xml',
|
||||||
'views/job_category.xml',
|
'views/job_category.xml',
|
||||||
'views/hr_location.xml',
|
'views/hr_location.xml',
|
||||||
|
|
@ -64,7 +67,7 @@
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
# 'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,8 +156,11 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@http.route(['/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
@http.route([
|
||||||
website=True)
|
'/SRIVYNPLATFORMS/JoiningForm/<int:applicant_id>',
|
||||||
|
'/FTPROTECH/JoiningForm/<int:applicant_id>',
|
||||||
|
], type='http', auth="public",
|
||||||
|
website=True)
|
||||||
def post_onboarding_form(self, applicant_id, **kwargs):
|
def post_onboarding_form(self, applicant_id, **kwargs):
|
||||||
"""Renders the website form for applicants to submit additional details."""
|
"""Renders the website form for applicants to submit additional details."""
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
|
|
@ -174,8 +177,11 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
|
|
||||||
|
|
||||||
@http.route(['/SRIVYNPLATFORMS/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
@http.route([
|
||||||
methods=['POST'], website=True, csrf=False)
|
'/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):
|
def process_employee_joining_form(self,applicant_id,**post):
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
if not applicant.exists():
|
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)))
|
private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0)))
|
||||||
|
|
||||||
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
|
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
|
||||||
|
employee_id = post.get('employee_id')
|
||||||
applicant_data = {
|
applicant_data = {
|
||||||
'applicant_id': int(post.get('applicant_id', 0)),
|
'applicant_id': int(post.get('applicant_id', 0)),
|
||||||
'employee_id': int(post.get('employee_id', 0)),
|
'employee_id': int(employee_id) if employee_id else False,
|
||||||
'candidate_image': post.get('candidate_image_base64', ''),
|
'candidate_image': post.get('candidate_image_base64', ''),
|
||||||
'doj': datetime.strptime(post.get('doj'), '%Y-%m-%d').date() if post.get('doj', None) else '',
|
'doj': datetime.strptime(post.get('doj'), '%Y-%m-%d').date() if post.get('doj', None) else '',
|
||||||
'email_from': post.get('email_from', ''),
|
'email_from': post.get('email_from', ''),
|
||||||
|
|
@ -276,10 +282,17 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
attachments_data_json = post.get('attachments_data_json', '[]')
|
attachments_data_json = post.get('attachments_data_json', '[]')
|
||||||
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
||||||
|
|
||||||
applicant.write(applicant_data)
|
applicant.write(applicant_data)
|
||||||
applicant.replace_joining_attachments(attachments_data)
|
applicant.replace_joining_attachments(attachments_data)
|
||||||
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
if 'recruitment.employee.bridge' in request.env.registry:
|
||||||
raise_if_not_found=False)
|
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')
|
group = request.env.ref('hr.group_hr_manager')
|
||||||
users = request.env['res.users'].sudo().search([
|
users = request.env['res.users'].sudo().search([
|
||||||
('groups_id', 'in', group.ids),
|
('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 submission_share_history
|
||||||
from . import hr_recruitment
|
from . import hr_recruitment
|
||||||
from . import hr_job_recruitment
|
from . import hr_job_recruitment
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class CandidateExperience(models.Model):
|
||||||
experience_code = fields.Char('Experience Code')
|
experience_code = fields.Char('Experience Code')
|
||||||
experience_from = fields.Integer(string="Experience From (Years)")
|
experience_from = fields.Integer(string="Experience From (Years)")
|
||||||
experience_to = fields.Integer(string="Experience To (Years)")
|
experience_to = fields.Integer(string="Experience To (Years)")
|
||||||
|
company_id = fields.Many2one('res.company','Company', default=lambda self: self.env.company)
|
||||||
# display_name = fields.Char(string="Display Name")
|
# display_name = fields.Char(string="Display Name")
|
||||||
# active = fields.Boolean()
|
# active = fields.Boolean()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,7 @@ class RecruitmentCategory(models.Model):
|
||||||
|
|
||||||
category_name = fields.Char(string="Category Name")
|
category_name = fields.Char(string="Category Name")
|
||||||
default_user = fields.Many2one('res.users')
|
default_user = fields.Many2one('res.users')
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.company ,string='Company', ondelete='cascade')
|
||||||
|
|
||||||
|
|
||||||
class ApplicationsStageStatus(models.Model):
|
class ApplicationsStageStatus(models.Model):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from odoo import fields, models, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicantCategory(models.Model):
|
||||||
|
_inherit = "hr.applicant.category"
|
||||||
|
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
allow_cross_company_candidates = fields.Boolean(
|
||||||
|
string="Allow Cross-Company Candidate Access",config_parameter='hr_recruitment_extended.allow_cross_company_candidates',
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_values(self):
|
||||||
|
res = super().set_values()
|
||||||
|
rule = self.env.ref("hr_recruitment.hr_candidate_comp_rule")
|
||||||
|
rule.write({
|
||||||
|
"active": not self.allow_cross_company_candidates
|
||||||
|
})
|
||||||
|
self.env.registry.clear_cache()
|
||||||
|
return res
|
||||||
|
|
@ -4,4 +4,5 @@ from odoo import models, fields, api, _
|
||||||
class ResPartner(models.Model):
|
class ResPartner(models.Model):
|
||||||
_inherit = 'res.partner'
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
|
||||||
|
|
@ -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'))]"/>
|
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
|
||||||
</record>
|
</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="write" model="ir.model.data">
|
||||||
<function name="search" model="ir.model.data">
|
<function name="search" model="ir.model.data">
|
||||||
<value eval="[('module', '=', 'hr_recruitment_skills'), ('name','=','hr_applicant_skill_interviewer_rule')] "/>
|
<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_code" placeholder = "E1" required="1" width="30%"/>
|
||||||
<field name="experience_from" required="1" placeholder="0" />
|
<field name="experience_from" required="1" placeholder="0" />
|
||||||
<field name="experience_to" required="1" placeholder="2" />
|
<field name="experience_to" required="1" placeholder="2" />
|
||||||
|
<field name="company_id"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</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">
|
<list string="Category" editable="bottom">
|
||||||
<field name="category_name" required="1"/>
|
<field name="category_name" required="1"/>
|
||||||
<field name="default_user"/>
|
<field name="default_user"/>
|
||||||
|
<field name="company_id" required="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</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>
|
<group>
|
||||||
|
|
||||||
<field name="company_type" widget="radio"
|
<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"/>
|
<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 controllers
|
||||||
|
from . import models
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "HRMS Employee Dashboard",
|
"name": "Dashboard",
|
||||||
"version": "18.0.1.0.0",
|
"version": "18.0.1.0.0",
|
||||||
"category": "Human Resources",
|
"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",
|
"author": "Pranay",
|
||||||
"license": "LGPL-3",
|
"license": "LGPL-3",
|
||||||
"depends": [
|
"depends": [
|
||||||
|
|
@ -11,6 +11,12 @@
|
||||||
"hr",
|
"hr",
|
||||||
"hr_attendance",
|
"hr_attendance",
|
||||||
"hr_holidays",
|
"hr_holidays",
|
||||||
|
"hr_expense",
|
||||||
|
"calendar",
|
||||||
|
"project_todo",
|
||||||
|
"hr_resignation",
|
||||||
|
"knowledge",
|
||||||
|
"website",
|
||||||
"maintenance",
|
"maintenance",
|
||||||
"employee_it_declaration",
|
"employee_it_declaration",
|
||||||
"business_travel_expense_management",
|
"business_travel_expense_management",
|
||||||
|
|
@ -18,6 +24,7 @@
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"views/hrms_emp_dashboard_views.xml",
|
"views/hrms_emp_dashboard_views.xml",
|
||||||
|
"views/res_config_settings_views.xml",
|
||||||
],
|
],
|
||||||
"assets": {
|
"assets": {
|
||||||
"web.assets_backend": [
|
"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 today = new Date();
|
||||||
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
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({
|
this.state = useState({
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
data: null,
|
data: null,
|
||||||
period: "this_month",
|
period: "this_month",
|
||||||
calendarView: "weekly",
|
calendarView: "monthly",
|
||||||
dateFrom: this.formatDate(monthStart),
|
dateFrom: this.formatDate(monthStart),
|
||||||
dateTo: this.formatDate(monthEnd),
|
dateTo: this.formatDate(monthEnd),
|
||||||
calendarDateFrom: this.formatDate(weekStart),
|
calendarDateFrom: this.formatDate(monthStart),
|
||||||
calendarDateTo: this.formatDate(weekEnd),
|
calendarDateTo: this.formatDate(monthEnd),
|
||||||
|
selectedDay: null,
|
||||||
|
activeTab: this.getStoredActiveTab(),
|
||||||
|
holidayTab: 'current',
|
||||||
|
leaveRequests: [],
|
||||||
|
leaveRequestsTotal: 0,
|
||||||
|
leaveRequestsLimit: 5,
|
||||||
});
|
});
|
||||||
this.rpc = rpc;
|
this.rpc = rpc;
|
||||||
this.action = useService("action");
|
this.action = useService("action");
|
||||||
|
|
@ -42,7 +44,6 @@ class HrmsEmployeeDashboard extends Component {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
onWillDestroy(() => this.destroyCharts());
|
onWillDestroy(() => this.destroyCharts());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
|
|
@ -52,6 +53,22 @@ class HrmsEmployeeDashboard extends Component {
|
||||||
return `${year}-${month}-${day}`;
|
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() {
|
async loadData() {
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.state.error = null;
|
this.state.error = null;
|
||||||
|
|
@ -63,17 +80,45 @@ class HrmsEmployeeDashboard extends Component {
|
||||||
calendar_date_to: this.state.calendarDateTo,
|
calendar_date_to: this.state.calendarDateTo,
|
||||||
calendar_view: this.state.calendarView,
|
calendar_view: this.state.calendarView,
|
||||||
});
|
});
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
this.state.error = response.error || "Unable to load employee dashboard.";
|
this.state.error = response.error || "Unable to load employee dashboard.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.state.data = response;
|
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 expenses = response.expenses || {};
|
||||||
const total =
|
const total = (expenses.series || []).reduce((sum, value) => sum + Number(value || 0), 0);
|
||||||
(expenses.series || []).reduce(
|
|
||||||
(sum, value) => sum + Number(value || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
this.state.expenseCount = total;
|
this.state.expenseCount = total;
|
||||||
setTimeout(() => this.initCharts(), 100);
|
setTimeout(() => this.initCharts(), 100);
|
||||||
} catch (error) {
|
} 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() {
|
destroyCharts() {
|
||||||
for (const chart of this.charts) {
|
for (const chart of this.charts) {
|
||||||
try {
|
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() {
|
// addExpense() {
|
||||||
// this.action.doAction({
|
// this.action.doAction({
|
||||||
// type: "ir.actions.act_window",
|
// 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() {
|
get statusText() {
|
||||||
return this.state.data?.attendance_state === "checked_in" ? "Check Out" : "Check In";
|
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>
|
<div t-if="state.loading" class="hrms-loading">Loading employee dashboard...</div>
|
||||||
|
|
||||||
<t t-if="state.data && !state.loading">
|
<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">
|
<section class="hrms-employee-card">
|
||||||
<div class="hrms-employee-main">
|
<div class="hrms-employee-main">
|
||||||
<img class="hrms-avatar" t-att-src="state.data.employee.image_url" alt="Employee"/>
|
<img class="hrms-avatar" t-att-src="state.data.employee.image_url" alt="Employee"/>
|
||||||
|
|
@ -27,21 +48,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="hrms-employee-actions">
|
<div class="hrms-employee-actions">
|
||||||
<div class="hrms-action-buttons">
|
<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
|
<button
|
||||||
t-att-class="'hrms-icon-button ' + (state.data.attendance_state === 'checked_in' ? 'checkout' : 'checkin')"
|
t-att-class="'hrms-icon-button ' + (state.data.attendance_state === 'checked_in' ? 'checkout' : 'checkin')"
|
||||||
t-on-click="toggleAttendance"
|
t-on-click="toggleAttendance"
|
||||||
t-att-title="statusText">
|
t-att-title="statusText">
|
||||||
|
<i t-att-class="state.data.attendance_state === 'checked_in'
|
||||||
<i t-att-class="state.data.attendance_state === 'checked_in'
|
? 'fa fa-sign-out'
|
||||||
? 'fa fa-sign-out'
|
: 'fa fa-sign-in'"/>
|
||||||
: 'fa fa-sign-in'"/>
|
|
||||||
|
|
||||||
<span t-esc="statusText"/>
|
<span t-esc="statusText"/>
|
||||||
</button>
|
</button>
|
||||||
<button class="hrms-icon-button" t-on-click="downloadPayslip" title="Download Payslip">
|
<button class="hrms-icon-button" t-on-click="downloadPayslip" title="Download Payslip">
|
||||||
<i class="fa fa-download"/>
|
<i class="fa fa-download"/>
|
||||||
<span>Payslip</span>
|
<span>Payslip</span>
|
||||||
|
|
@ -53,7 +68,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="hrms-filter-box">
|
<div class="hrms-filter-box">
|
||||||
<span>Period</span>
|
<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_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="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>
|
<option value="last_3_months" t-att-selected="state.period === 'last_3_months'">Last 3 Months</option>
|
||||||
|
|
@ -73,6 +88,34 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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"><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>
|
<div class="hrms-kpi present"><span>Worked Hours</span><strong t-esc="state.data.attendance_summary.worked_display"/></div>
|
||||||
|
|
@ -84,14 +127,18 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="hrms-grid">
|
<div class="hrms-employee-columns">
|
||||||
<section class="hrms-panel wide">
|
<div class="hrms-employee-main-column">
|
||||||
|
<section class="hrms-panel hrms-calendar-panel">
|
||||||
<div class="hrms-panel-header">
|
<div class="hrms-panel-header">
|
||||||
<h2>Attendance Calendar</h2>
|
<h2>Work Calendar</h2>
|
||||||
<div class="hrms-month-controls">
|
<div class="hrms-month-controls">
|
||||||
<button class="btn btn-light btn-sm" t-on-click="previousMonth"><i class="fa fa-chevron-left"/></button>
|
<button class="btn btn-light btn-sm" t-on-click="previousMonth"><i class="fa fa-chevron-left"/></button>
|
||||||
<strong t-esc="monthLabel"/>
|
<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-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">
|
<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="weekly" t-att-selected="state.calendarView === 'weekly'">Weekly</option>
|
||||||
<option value="monthly" t-att-selected="state.calendarView === 'monthly'">Monthly</option>
|
<option value="monthly" t-att-selected="state.calendarView === 'monthly'">Monthly</option>
|
||||||
|
|
@ -109,10 +156,14 @@
|
||||||
<span>Sun</span>
|
<span>Sun</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hrms-calendar" t-att-class="state.calendarView === 'yearly' ? 'yearly' : ''">
|
<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">
|
<button t-foreach="state.data.attendance_calendar" t-as="day" t-key="day.date"
|
||||||
<t t-if="day.status === 'empty'">
|
type="button"
|
||||||
<span/>
|
class="hrms-day"
|
||||||
</t>
|
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'">
|
<t t-elif="day.type === 'month'">
|
||||||
<div class="hrms-day-top">
|
<div class="hrms-day-top">
|
||||||
<span t-esc="day.weekday"/>
|
<span t-esc="day.weekday"/>
|
||||||
|
|
@ -123,222 +174,418 @@
|
||||||
<small><t t-esc="day.worked_display"/> worked</small>
|
<small><t t-esc="day.worked_display"/> worked</small>
|
||||||
<small><t t-esc="day.break_display"/> break</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 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>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
<div class="hrms-day-top">
|
<div class="hrms-day-top">
|
||||||
<strong>
|
<strong t-esc="day.day"/>
|
||||||
<t t-esc="day.day"/>
|
<span t-esc="day.weekday"/>
|
||||||
<span t-esc="day.weekday"/>
|
</div>
|
||||||
</strong>
|
<div t-if="day.worked_display"
|
||||||
<span t-if="day.label and day.show_metrics" class="hrms-day-badge" t-esc="day.label"/>
|
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>
|
||||||
<div t-if="!day.show_metrics and day.label" class="hrms-day-message">
|
<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-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>
|
<span t-elif="day.status === 'holiday'" class="hrms-day-message-icon"><i class="fa fa-calendar"/></span>
|
||||||
<strong t-esc="day.label || '-'"/>
|
<strong t-esc="day.label || '-'"/>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="day.show_metrics" t-att-class="'hrms-day-balance ' + (day.balance_hours >= 0 ? 'positive' : 'negative')">
|
<div t-if="day.signals && day.signals.length" class="hrms-day-signals">
|
||||||
<t t-esc="day.balance_display"/>
|
<t t-foreach="day.signals" t-as="signal" t-key="signal.type + signal.label">
|
||||||
</div>
|
<button t-if="signal.type === 'more'"
|
||||||
<div t-if="day.show_metrics" class="hrms-day-metrics">
|
type="button"
|
||||||
<div>
|
class="hrms-day-signal more hrms-more-btn"
|
||||||
<span>Wrk.</span>
|
t-on-click.stop="() => this.openDayDetails(day)"
|
||||||
<strong t-esc="day.worked_display"/>
|
t-att-title="'Click to view all'">
|
||||||
</div>
|
<i class="fa fa-ellipsis-h"/>
|
||||||
<div>
|
<t t-esc="signal.label"/>
|
||||||
<span>Break</span>
|
</button>
|
||||||
<strong t-esc="day.break_display"/>
|
<span t-else="" t-att-class="'hrms-day-signal ' + signal.type">
|
||||||
</div>
|
<i t-att-class="signal.icon"/>
|
||||||
<div>
|
<t t-esc="signal.label"/>
|
||||||
<span>Exp.</span>
|
</span>
|
||||||
<strong t-esc="day.expected_display"/>
|
</t>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
</t>
|
</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>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="action_hrms_emp_dashboard" model="ir.actions.client">
|
<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="tag">hrms_emp_dashboard</field>
|
||||||
<field name="target">current</field>
|
<field name="target">current</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_hrms_emp_dashboard_root"
|
<menuitem id="menu_hrms_emp_dashboard_root"
|
||||||
name="Employee Dashboard"
|
name="Dashboard"
|
||||||
action="action_hrms_emp_dashboard"
|
action="action_hrms_emp_dashboard"
|
||||||
web_icon="hrms_emp_dashboard,static/description/icon.png"
|
web_icon="hrms_emp_dashboard,static/description/icon.png"
|
||||||
sequence="-90"
|
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',
|
'author': 'Raman Marikanti',
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'depends': ['base', 'hr_recruitment', 'hr_payroll', 'hr_recruitment_extended'],
|
'depends': ['base', 'employee_bridge', 'hr_payroll'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/mail_template.xml',
|
'data/mail_template.xml',
|
||||||
'views/stages.xml',
|
|
||||||
'views/offer_letter_views.xml',
|
'views/offer_letter_views.xml',
|
||||||
'views/hr_applicant_offer_views.xml',
|
|
||||||
'views/offer_response_templates.xml',
|
'views/offer_response_templates.xml',
|
||||||
|
'views/recruitment_employee_bridge.xml',
|
||||||
# 'views/templates.xml',
|
# 'views/templates.xml',
|
||||||
'views/menu_views.xml',
|
'views/menu_views.xml',
|
||||||
'wizards/offer_release_request_wizard.xml',
|
|
||||||
'wizards/applicant_offer_mail_wizard.xml',
|
'wizards/applicant_offer_mail_wizard.xml',
|
||||||
'wizards/offer_letter_reject_wizard.xml',
|
'wizards/offer_letter_reject_wizard.xml',
|
||||||
'report/offer_letter_report.xml',
|
'report/offer_letter_report.xml',
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
<field name="name">Applicant Offer Email Template</field>
|
<field name="name">Applicant Offer Email Template</field>
|
||||||
<field name="model_id" ref="offer_letters.model_offer_letter"/>
|
<field name="model_id" ref="offer_letters.model_offer_letter"/>
|
||||||
<field name="email_from">{{ user.email_formatted }}</field>
|
<field name="email_from">{{ user.email_formatted }}</field>
|
||||||
<field name="email_to">{{ object.main_candidate_id.email_from or '' }}</field>
|
<field name="email_to">{{ object.recipient_email or '' }}</field>
|
||||||
<field name="subject">Offer Letter - {{ object.position or object.main_candidate_id.job_id.name or '' }}</field>
|
<field name="subject">Offer Letter - {{ object.position or '' }}</field>
|
||||||
<field name="description">
|
<field name="description">
|
||||||
Send applicant offer mail with offer letter attachment.
|
Send applicant offer mail with offer letter attachment.
|
||||||
</field>
|
</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<div style="margin: 0; padding: 0; font-size: 13px; line-height: 1.7;">
|
<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>
|
<p>
|
||||||
With reference to the interview and subsequent discussions you had with us, we are pleased to select
|
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
|
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 offer_letter
|
||||||
from . import hr_applicant
|
from . import recruitment_employee_bridge
|
||||||
from . import hr_candidate
|
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
default=lambda self: _('New'),
|
||||||
copy=False
|
copy=False
|
||||||
)
|
)
|
||||||
candidate_id = fields.Many2one( 'hr.applicant', string='Applicant', required=False,
|
bridge_id = fields.Many2one(
|
||||||
)
|
'recruitment.employee.bridge',
|
||||||
main_candidate_id = fields.Many2one('hr.candidate',string='Candidate', readonly=False, required=True)
|
string='Onboarding',
|
||||||
|
ondelete='restrict',
|
||||||
@api.onchange('candidate_id')
|
tracking=True,
|
||||||
def _onchange_candidate_id(self):
|
index=True,
|
||||||
if self.candidate_id:
|
)
|
||||||
self.main_candidate_id = self.candidate_id.candidate_id
|
main_candidate_name = fields.Char(string="Candidate Name", compute="_compute_bridge_contact", store=True)
|
||||||
|
recipient_email = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||||
main_candidate_name = fields.Char(compute="_compute_main_candidate_name", readonly=False)
|
recipient_street = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||||
|
recipient_street2 = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||||
@api.depends('candidate_id','main_candidate_id')
|
recipient_city = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||||
def _compute_main_candidate_name(self):
|
recipient_zip = fields.Char(compute="_compute_bridge_contact", store=True)
|
||||||
for rec in self:
|
recipient_country_id = fields.Many2one("res.country", compute="_compute_bridge_contact", store=True)
|
||||||
if rec.candidate_id:
|
|
||||||
rec.main_candidate_name = rec.candidate_id.partner_name
|
@api.depends(
|
||||||
elif rec.main_candidate_id:
|
'bridge_id',
|
||||||
rec.main_candidate_name = rec.main_candidate_id.partner_name
|
'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)
|
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)
|
request_date = fields.Datetime(string='Requested On', readonly=True, tracking=True)
|
||||||
|
|
||||||
|
|
@ -120,20 +142,20 @@ class OfferLetter(models.Model):
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
if vals.get('name', _('New')) == _('New'):
|
if vals.get('name', _('New')) == _('New'):
|
||||||
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
|
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
|
||||||
if vals.get('candidate_id') and 'salary' in vals:
|
offer_letter = super(OfferLetter, self).create(vals)
|
||||||
self.env['hr.applicant'].browse(vals['candidate_id']).write({
|
if vals.get('bridge_id') and 'salary' in vals and 'finalized_ctc' in offer_letter.bridge_id._fields:
|
||||||
'finalized_ctc': vals['salary'],
|
offer_letter.bridge_id.finalized_ctc = offer_letter.salary
|
||||||
})
|
return offer_letter
|
||||||
return super(OfferLetter, self).create(vals)
|
|
||||||
|
def write(self, vals):
|
||||||
def write(self, vals):
|
result = super(OfferLetter, self).write(vals)
|
||||||
result = super(OfferLetter, self).write(vals)
|
if 'salary' in vals:
|
||||||
if 'salary' in vals:
|
for record in self.filtered('bridge_id'):
|
||||||
for record in self.filtered('candidate_id'):
|
if 'finalized_ctc' in record.bridge_id._fields:
|
||||||
record.candidate_id.finalized_ctc = record.salary
|
record.bridge_id.finalized_ctc = record.salary
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def action_open_send_offer_wizard(self):
|
def action_open_send_offer_wizard(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
@ -162,7 +184,7 @@ class OfferLetter(models.Model):
|
||||||
def action_accept_offer(self):
|
def action_accept_offer(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
# employee = self.env['hr.employee'].create({
|
# employee = self.env['hr.employee'].create({
|
||||||
# 'name': self.candidate_id.partner_name,
|
# 'name': self.main_candidate_name,
|
||||||
# 'job_title': self.position,
|
# 'job_title': self.position,
|
||||||
# 'department_id': self.department_id.id,
|
# 'department_id': self.department_id.id,
|
||||||
# 'currency_id': self.currency_id.id,
|
# 'currency_id': self.currency_id.id,
|
||||||
|
|
@ -174,17 +196,18 @@ class OfferLetter(models.Model):
|
||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_reject_offer(self):
|
def action_reject_offer(self, reason=False):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
self.write({
|
self.write({
|
||||||
'state': 'rejected',
|
'state': 'rejected',
|
||||||
'response_date': fields.Datetime.now()
|
'response_date': fields.Datetime.now(),
|
||||||
|
'rejection_reason': reason
|
||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.onchange('candidate_id')
|
@api.onchange('bridge_id')
|
||||||
def _onchange_candidate_id(self):
|
def _onchange_bridge_id(self):
|
||||||
self.position = self.candidate_id.job_id.name
|
self.position = self.bridge_id.job_id.name
|
||||||
|
|
||||||
def get_paydetailed_lines(self):
|
def get_paydetailed_lines(self):
|
||||||
today = fields.Date.today()
|
today = fields.Date.today()
|
||||||
|
|
@ -278,3 +301,15 @@ class OfferLetter(models.Model):
|
||||||
|
|
||||||
def generate_pdf_report(self):
|
def generate_pdf_report(self):
|
||||||
return self.env.ref('offer_letters.hr_offer_letters_employee_print').report_action(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;">
|
<div style="margin-bottom: 25px;">
|
||||||
<strong>To,</strong>
|
<strong>To,</strong>
|
||||||
<br/>
|
<br/>
|
||||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
<strong t-esc="o.main_candidate_name"/>
|
||||||
<br/>
|
<br/>
|
||||||
<div t-if="o.candidate_id and o.candidate_id.private_street and o.candidate_id.private_city">
|
<div t-if="o.recipient_street and o.recipient_city">
|
||||||
<t t-esc="o.candidate_id.private_street"/>
|
<t t-esc="o.recipient_street"/>
|
||||||
<br/>
|
<br/>
|
||||||
<t t-esc="o.candidate_id.private_street2" t-if="o.candidate_id.private_street2"/>
|
<t t-esc="o.recipient_street2" t-if="o.recipient_street2"/>
|
||||||
<t t-if="o.candidate_id.private_street2">
|
<t t-if="o.recipient_street2">
|
||||||
<br/>
|
<br/>
|
||||||
</t>
|
</t>
|
||||||
<t t-esc="o.candidate_id.private_city"/>
|
<t t-esc="o.recipient_city"/>
|
||||||
<t t-esc="o.candidate_id.private_zip"/>
|
<t t-esc="o.recipient_zip"/>
|
||||||
<br/>
|
<br/>
|
||||||
<t t-esc="o.candidate_id.private_country_id.name"/>
|
<t t-esc="o.recipient_country_id.name"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DEAR LINE -->
|
<!-- DEAR LINE -->
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 20px;">
|
||||||
<strong>Dear
|
<strong>Dear
|
||||||
<t t-esc="o.main_candidate_id.partner_name"/>,
|
<t t-esc="o.main_candidate_name"/>,
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -368,7 +368,7 @@
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
<strong t-esc="o.main_candidate_name"/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong t-esc="o.position"/>
|
<strong t-esc="o.position"/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
@ -486,7 +486,7 @@
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong>Employee Name :
|
<strong>Employee Name :
|
||||||
<t t-esc="o.main_candidate_id.partner_name"/>
|
<t t-esc="o.main_candidate_name"/>
|
||||||
</strong>
|
</strong>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
@ -595,7 +595,7 @@
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
<strong t-esc="o.main_candidate_name"/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong t-esc="o.position"/>
|
<strong t-esc="o.position"/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
@ -621,7 +621,7 @@
|
||||||
Company incorporated under Indian Companies Act 1956, having registered office in
|
Company incorporated under Indian Companies Act 1956, having registered office in
|
||||||
Hyderabad,
|
Hyderabad,
|
||||||
India ("Company")
|
India ("Company")
|
||||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
<strong t-esc="o.main_candidate_name"/>
|
||||||
(Recipient)
|
(Recipient)
|
||||||
</p>
|
</p>
|
||||||
<p>Whereas "Company" wishes to explore the possibility of entering into an employment
|
<p>Whereas "Company" wishes to explore the possibility of entering into an employment
|
||||||
|
|
@ -875,7 +875,7 @@
|
||||||
<strong t-esc="o.joining_date"/>
|
<strong t-esc="o.joining_date"/>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding-top: 20px;">
|
<td style="padding-top: 20px;">
|
||||||
<strong t-esc="o.main_candidate_id.partner_name"/>
|
<strong t-esc="o.main_candidate_name"/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong t-esc="o.position"/>
|
<strong t-esc="o.position"/>
|
||||||
<br/>
|
<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_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_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_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"
|
<button name="action_open_send_offer_wizard" type="object" string="Send Offer" class="oe_highlight"
|
||||||
invisible="state != 'requested'" groups="hr.group_hr_manager"/>
|
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_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="get_paydetailed_lines" type="object" string="Get Data" class="oe_danger"/>
|
||||||
<button name="generate_pdf_report" type="object" string="Generate PDF" class="oe_highlight"/>
|
<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"/>
|
<field name="state" widget="statusbar" statusbar_visible="requested,sent,accepted,rejected,expired"/>
|
||||||
|
|
@ -32,9 +32,8 @@
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="name" readonly="state != 'requested'"/>
|
<field name="name" readonly="state != 'requested'"/>
|
||||||
<field name="main_candidate_name" invisible="1"/>
|
<field name="bridge_id"/>
|
||||||
<field name="candidate_id" invisible="not candidate_id" readonly="1" force_save="1"/>
|
<field name="main_candidate_name" readonly="1"/>
|
||||||
<field name="main_candidate_id" invisible="candidate_id" force_save="1"/>
|
|
||||||
<field name="requested_by_id" readonly="1"/>
|
<field name="requested_by_id" readonly="1"/>
|
||||||
<field name="request_date" readonly="1"/>
|
<field name="request_date" readonly="1"/>
|
||||||
<field name="manager_id"/>
|
<field name="manager_id"/>
|
||||||
|
|
@ -50,8 +49,9 @@
|
||||||
<field name="probation_period"/>
|
<field name="probation_period"/>
|
||||||
<field name="pay_struct_id"/>
|
<field name="pay_struct_id"/>
|
||||||
<field name="sent_date" readonly="1"/>
|
<field name="sent_date" readonly="1"/>
|
||||||
<field name="response_date" readonly="1"/>
|
<field name="response_date" readonly="1"/>
|
||||||
</group>
|
<field name="recipient_email" readonly="1"/>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Terms Conditions">
|
<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 applicant_offer_mail_wizard
|
||||||
from . import offer_release_request_wizard
|
|
||||||
from . import offer_letter_reject_wizard
|
from . import offer_letter_reject_wizard
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
||||||
_name = 'applicant.offer.mail.wizard'
|
_name = 'applicant.offer.mail.wizard'
|
||||||
_description = '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)
|
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)
|
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)
|
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):
|
def default_get(self, fields_list):
|
||||||
defaults = super().default_get(fields_list)
|
defaults = super().default_get(fields_list)
|
||||||
offer_letter = self._get_offer_letter_from_context()
|
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)
|
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({
|
defaults.update({
|
||||||
'applicant_id': applicant.id,
|
'bridge_id': bridge.id,
|
||||||
'offer_letter_id': offer_letter.id,
|
'offer_letter_id': offer_letter.id,
|
||||||
'position': offer_letter.position,
|
'position': offer_letter.position,
|
||||||
'salary': offer_letter.salary,
|
'salary': offer_letter.salary,
|
||||||
|
|
@ -64,11 +66,13 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
||||||
defaults.update(self._prepare_mail_defaults(template, offer_letter))
|
defaults.update(self._prepare_mail_defaults(template, offer_letter))
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
def _get_applicant(self):
|
def _get_bridge(self):
|
||||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
bridge = self.env['recruitment.employee.bridge'].browse(
|
||||||
if not applicant.exists():
|
self.env.context.get('default_bridge_id') or self.env.context.get('active_id')
|
||||||
raise UserError(_("The applicant does not exist or is not accessible."))
|
)
|
||||||
return applicant
|
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):
|
def _get_offer_letter_from_context(self):
|
||||||
if self.env.context.get('active_model') != 'offer.letter':
|
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."))
|
raise UserError(_("The offer letter does not exist or is not accessible."))
|
||||||
return offer_letter
|
return offer_letter
|
||||||
|
|
||||||
def _get_default_pay_structure(self, applicant):
|
def _get_default_pay_structure(self, bridge):
|
||||||
company = applicant.company_id or self.env.company
|
company = bridge.company_id or self.env.company
|
||||||
return self.env['hr.payroll.structure'].search([
|
return self.env['hr.payroll.structure'].search([
|
||||||
'|',
|
'|',
|
||||||
('company_id', '=', company.id),
|
('company_id', '=', company.id),
|
||||||
('company_id', '=', False),
|
('company_id', '=', False),
|
||||||
], limit=1)
|
], limit=1)
|
||||||
|
|
||||||
def _get_default_manager(self, applicant):
|
def _get_default_manager(self, bridge):
|
||||||
return applicant.user_id.employee_id or self.env.user.employee_id
|
return self.env.user.employee_id
|
||||||
|
|
||||||
def _create_offer_letter(self, applicant):
|
def _create_offer_letter(self, bridge):
|
||||||
pay_structure = self._get_default_pay_structure(applicant)
|
pay_structure = self._get_default_pay_structure(bridge)
|
||||||
if not pay_structure:
|
if not pay_structure:
|
||||||
raise UserError(_("Please configure at least one salary structure before sending an offer."))
|
raise UserError(_("Please configure at least one salary structure before sending an offer."))
|
||||||
|
|
||||||
offer_letter = self.env['offer.letter'].create({
|
offer_letter = self.env['offer.letter'].create({
|
||||||
'candidate_id': applicant.id,
|
'bridge_id': bridge.id,
|
||||||
'position': applicant.job_id.name or applicant.hr_job_recruitment.job_id.name or applicant.partner_name or applicant.display_name,
|
'position': bridge.job_id.name or bridge.employee_name or bridge.display_name,
|
||||||
'salary': applicant.finalized_ctc or applicant.salary_expected or applicant.current_ctc or 0.0,
|
'salary': bridge.finalized_ctc if 'finalized_ctc' in bridge._fields else 0.0,
|
||||||
'joining_date': fields.Date.today(),
|
'joining_date': fields.Date.today(),
|
||||||
'pay_struct_id': pay_structure.id,
|
'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()
|
offer_letter.get_paydetailed_lines()
|
||||||
return offer_letter
|
return offer_letter
|
||||||
|
|
@ -108,7 +112,7 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
||||||
def _update_offer_letter(self):
|
def _update_offer_letter(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
vals = {
|
vals = {
|
||||||
'candidate_id': self.applicant_id.id,
|
'bridge_id': self.bridge_id.id,
|
||||||
'position': self.position,
|
'position': self.position,
|
||||||
'salary': self.salary,
|
'salary': self.salary,
|
||||||
'joining_date': self.joining_date,
|
'joining_date': self.joining_date,
|
||||||
|
|
@ -125,7 +129,7 @@ class ApplicantOfferMailWizard(models.TransientModel):
|
||||||
report = self.env.ref('offer_letters.hr_offer_letters_employee_print')
|
report = self.env.ref('offer_letters.hr_offer_letters_employee_print')
|
||||||
from odoo import _ as translate
|
from odoo import _ as translate
|
||||||
pdf_content, _ = report.sudo()._render_qweb_pdf(report, offer_letter.id)
|
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({
|
return self.env['ir.attachment'].create({
|
||||||
'name': attachment_name,
|
'name': attachment_name,
|
||||||
'datas': base64.b64encode(pdf_content),
|
'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
|
attachment_ids = (other_attachments | attachment).ids if attachment else other_attachments.ids
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'email_from': generated_values.get('email_from') or offer_letter.candidate_id.user_id.email or self.env.user.email,
|
'email_from': generated_values.get('email_from') or self.env.user.email,
|
||||||
'email_to': generated_values.get('email_to') or offer_letter.candidate_id.email_from or '',
|
'email_to': generated_values.get('email_to') or offer_letter.recipient_email or '',
|
||||||
'email_cc': generated_values.get('email_cc', ''),
|
'email_cc': generated_values.get('email_cc', ''),
|
||||||
'email_subject': generated_values.get('subject', ''),
|
'email_subject': generated_values.get('subject', ''),
|
||||||
'email_body': generated_values.get('body_html', ''),
|
'email_body': generated_values.get('body_html', ''),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Send Offer">
|
<form string="Send Offer">
|
||||||
<group>
|
<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="template_id" options="{'no_create': True}" readonly="1"/>
|
||||||
<field name="generated_attachment_id" invisible="1"/>
|
<field name="generated_attachment_id" invisible="1"/>
|
||||||
</group>
|
</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):
|
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>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="hr_applicant_view_form_offer_letters_inherit" model="ir.ui.view">
|
<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="name">hr.applicant.view.form.offer.letters.inherit</field>
|
||||||
<field name="model">hr.applicant</field>
|
<field name="model">hr.applicant</field>
|
||||||
|
|
@ -35,8 +36,8 @@
|
||||||
<field name="current_offer_letter_id" readonly="1"/>
|
<field name="current_offer_letter_id" readonly="1"/>
|
||||||
<field name="offer_release_status" widget="badge" readonly="1"/>
|
<field name="offer_release_status" widget="badge" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
<field name="offer_letter_ids" nolabel="1">
|
<field name="offer_letter_ids" nolabel="1" options="{'no_create': True, 'no_open': True}">
|
||||||
<list>
|
<list create="0" delete="0" no_open="True">
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="requested_by_id"/>
|
<field name="requested_by_id"/>
|
||||||
<field name="request_date"/>
|
<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>
|
||||||
Loading…
Reference in New Issue