diff --git a/Dockerfile b/Dockerfile index 23689f03c..1ea2d46f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,7 +107,8 @@ RUN if [ -f requirements.txt ]; then \ pypdf \ phonenumbers \ python-docx \ - pyzk + pyzk \ + firebase-admin # ------------------------------------------------------------------ # Create Required Directories diff --git a/addons_extensions/base_custom/__init__.py b/addons_extensions/base_custom/__init__.py new file mode 100644 index 000000000..43559cd9a --- /dev/null +++ b/addons_extensions/base_custom/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/addons_extensions/base_custom/__manifest__.py b/addons_extensions/base_custom/__manifest__.py new file mode 100644 index 000000000..4a5527eb0 --- /dev/null +++ b/addons_extensions/base_custom/__manifest__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Base Changes', + 'category': 'Base', + 'version': '1.1', + 'author': 'Seshi Kanth', + 'summary': 'Changes of the form', + 'description': 'This module contains form changes.', + 'depends': [ + 'base', + 'web', + ], + + 'data': [ + # 'views/user_menu.xml', + ], + + 'assets': { + 'web.assets_backend': [ + 'base_custom/static/src/css/backend.css', + 'base_custom/static/src/js/user_menu_patch.js', + 'base_custom/static/src/js/hide_user_menu.js', + 'base_custom/static/src/xml/user_menu.xml', + ], + }, + + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/addons_extensions/base_custom/models/__init__.py b/addons_extensions/base_custom/models/__init__.py new file mode 100644 index 000000000..8164bedcc --- /dev/null +++ b/addons_extensions/base_custom/models/__init__.py @@ -0,0 +1 @@ +from . import ir_http \ No newline at end of file diff --git a/addons_extensions/base_custom/models/ir_http.py b/addons_extensions/base_custom/models/ir_http.py new file mode 100644 index 000000000..8d4b1cdb3 --- /dev/null +++ b/addons_extensions/base_custom/models/ir_http.py @@ -0,0 +1,17 @@ +from odoo import models + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + def session_info(self): + session_info = super().session_info() + + employee = self.env.user.employee_id + + session_info.update({ + "employee_name": employee.name or self.env.user.name, + "employee_designation": employee.job_id.name if employee.job_id else "", + }) + + return session_info \ No newline at end of file diff --git a/addons_extensions/base_custom/static/src/css/backend.css b/addons_extensions/base_custom/static/src/css/backend.css new file mode 100644 index 000000000..5db0b894a --- /dev/null +++ b/addons_extensions/base_custom/static/src/css/backend.css @@ -0,0 +1,23 @@ +.o_switch_company_menu .oe_topbar_name{ + display: none !important; +} + +.oe_topbar_name{ + display:flex; + flex-direction:column; + line-height:1.2; +} + +.oe_topbar_name .fw-bold{ + font-size:13px; + font-weight:600; +} + +.oe_topbar_name .designation{ + font-size:11px; + color:#7b7b7b; +} + +.text-muted { + color: #000000 !important; +} \ No newline at end of file diff --git a/addons_extensions/base_custom/static/src/js/hide_user_menu.js b/addons_extensions/base_custom/static/src/js/hide_user_menu.js new file mode 100644 index 000000000..3316e803f --- /dev/null +++ b/addons_extensions/base_custom/static/src/js/hide_user_menu.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +const userMenuRegistry = registry.category("user_menuitems"); + +const itemsToHide = [ + "documentation", + "support", + "shortcuts", + "odoo_account", +]; + +for (const item of itemsToHide) { + if (userMenuRegistry.contains(item)) { + userMenuRegistry.remove(item); + } +} \ No newline at end of file diff --git a/addons_extensions/base_custom/static/src/js/user_menu_patch.js b/addons_extensions/base_custom/static/src/js/user_menu_patch.js new file mode 100644 index 000000000..4c8225155 --- /dev/null +++ b/addons_extensions/base_custom/static/src/js/user_menu_patch.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { UserMenu } from "@web/webclient/user_menu/user_menu"; +import { session } from "@web/session"; + +patch(UserMenu.prototype, { + + setup() { + super.setup(); + + this.employeeName = session.employee_name; + this.employeeDesignation = session.employee_designation; + }, + + get employeeInfo() { + return { + employeeName: this.employeeName, + employeeDesignation: this.employeeDesignation, + }; + }, + +}); \ No newline at end of file diff --git a/addons_extensions/base_custom/static/src/xml/user_menu.xml b/addons_extensions/base_custom/static/src/xml/user_menu.xml new file mode 100644 index 000000000..f271304a6 --- /dev/null +++ b/addons_extensions/base_custom/static/src/xml/user_menu.xml @@ -0,0 +1,28 @@ + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
\ No newline at end of file diff --git a/addons_extensions/employee_bridge/__init__.py b/addons_extensions/employee_bridge/__init__.py new file mode 100644 index 000000000..b6de22769 --- /dev/null +++ b/addons_extensions/employee_bridge/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import controllers +from . import wizards diff --git a/addons_extensions/employee_bridge/__manifest__.py b/addons_extensions/employee_bridge/__manifest__.py new file mode 100644 index 000000000..2bf145920 --- /dev/null +++ b/addons_extensions/employee_bridge/__manifest__.py @@ -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, +} diff --git a/addons_extensions/employee_bridge/controllers/__init__.py b/addons_extensions/employee_bridge/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/addons_extensions/employee_bridge/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/addons_extensions/employee_bridge/controllers/main.py b/addons_extensions/employee_bridge/controllers/main.py new file mode 100644 index 000000000..8d2dafa5b --- /dev/null +++ b/addons_extensions/employee_bridge/controllers/main.py @@ -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/", 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)]) + } \ No newline at end of file diff --git a/addons_extensions/employee_bridge/data/ir_sequence_data.xml b/addons_extensions/employee_bridge/data/ir_sequence_data.xml new file mode 100644 index 000000000..148b091b7 --- /dev/null +++ b/addons_extensions/employee_bridge/data/ir_sequence_data.xml @@ -0,0 +1,10 @@ + + + + Onboarding + recruitment.employee.bridge + EB/ + 5 + + + diff --git a/addons_extensions/employee_bridge/data/mail_template.xml b/addons_extensions/employee_bridge/data/mail_template.xml new file mode 100644 index 000000000..50ec5f84e --- /dev/null +++ b/addons_extensions/employee_bridge/data/mail_template.xml @@ -0,0 +1,41 @@ + + + + JOD Form Email + + {{ user.email_formatted or object.company_id.email or '' }} + {{ object.work_email or object.private_email or object.employee_id.work_email or '' }} + Joining Onboarding Form - {{ object.employee_name or object.employee_id.name or '' }} + +
+

Dear ,

+

Welcome to .

+

+ Please submit your employee joining details using the link below. +

+

+ + Open Employee Joining Form + +

+ +

Attachments to Request

+ +

Personal Documents:

+
+ +

Education Documents:

+
+ +

Previous Employer Documents:

+
+ +

Other Documents:

+
+
+

Regards,
HR Team

+
+
+
+
diff --git a/addons_extensions/employee_bridge/data/onboarding_attachment_data.xml b/addons_extensions/employee_bridge/data/onboarding_attachment_data.xml new file mode 100644 index 000000000..403dd963a --- /dev/null +++ b/addons_extensions/employee_bridge/data/onboarding_attachment_data.xml @@ -0,0 +1,44 @@ + + + + Aadhaar Card + personal + 1 + + + + PAN Card + personal + 1 + + + + Passport Size Photo + personal + 1 + + + + Education Certificates + education + 1 + + + + Experience / Relieving Letter + previous_employer + 1 + + + + Last Three Months Payslips + previous_employer + 1 + + + + Cancelled Cheque / Bank Proof + others + 1 + + diff --git a/addons_extensions/employee_bridge/data/template.xml b/addons_extensions/employee_bridge/data/template.xml new file mode 100644 index 000000000..08f4e8544 --- /dev/null +++ b/addons_extensions/employee_bridge/data/template.xml @@ -0,0 +1,1690 @@ + + + + + + + diff --git a/addons_extensions/employee_bridge/models/__init__.py b/addons_extensions/employee_bridge/models/__init__.py new file mode 100644 index 000000000..8825e9eac --- /dev/null +++ b/addons_extensions/employee_bridge/models/__init__.py @@ -0,0 +1,3 @@ +from . import recruitment_employee_bridge +from . import hr_employee +from . import onboarding_documents diff --git a/addons_extensions/employee_bridge/models/hr_employee.py b/addons_extensions/employee_bridge/models/hr_employee.py new file mode 100644 index 000000000..e14a61b64 --- /dev/null +++ b/addons_extensions/employee_bridge/models/hr_employee.py @@ -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, + } diff --git a/addons_extensions/employee_bridge/models/onboarding_documents.py b/addons_extensions/employee_bridge/models/onboarding_documents.py new file mode 100644 index 000000000..2c9c72ce3 --- /dev/null +++ b/addons_extensions/employee_bridge/models/onboarding_documents.py @@ -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") diff --git a/addons_extensions/employee_bridge/models/recruitment_employee_bridge.py b/addons_extensions/employee_bridge/models/recruitment_employee_bridge.py new file mode 100644 index 000000000..e6ed3edb8 --- /dev/null +++ b/addons_extensions/employee_bridge/models/recruitment_employee_bridge.py @@ -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", _( + "

Hello %(name)s,

" + "

Please submit your joining onboarding details using this link:

" + "

Open Joining Form

" + ) % { + "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 diff --git a/addons_extensions/employee_bridge/security/ir.model.access.csv b/addons_extensions/employee_bridge/security/ir.model.access.csv new file mode 100644 index 000000000..010db3660 --- /dev/null +++ b/addons_extensions/employee_bridge/security/ir.model.access.csv @@ -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 diff --git a/addons_extensions/employee_bridge/static/src/js/post_onboarding_form.js b/addons_extensions/employee_bridge/static/src/js/post_onboarding_form.js new file mode 100644 index 000000000..7111428ca --- /dev/null +++ b/addons_extensions/employee_bridge/static/src/js/post_onboarding_form.js @@ -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 = `
+ +
`; + 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 = `
+ +
`; + 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(` + + + + + +
+ ${previewContent} +
+ + +
+
+ + + `); + + 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(` + + `); + } else if (fileType === 'application/pdf') { + this.$(`#${filePreviewWrapperId}`).html(` + + `); + } + + 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 = $(` +
+
+ + +
+ ${file.name} +
+ `); + + 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 = ` + `; + + // 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 = ` + + + + + + + + + + + + `; + + 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]) => ` + + `).join(''); + + const stateHtml = ` + + `; + + const presentStateHtml = ` + + `; + + 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(); + } + }, +}); diff --git a/addons_extensions/employee_bridge/views/hr_employee_views.xml b/addons_extensions/employee_bridge/views/hr_employee_views.xml new file mode 100644 index 000000000..f9611c436 --- /dev/null +++ b/addons_extensions/employee_bridge/views/hr_employee_views.xml @@ -0,0 +1,28 @@ + + + + hr.employee.view.form.bridge + hr.employee + + + + + + + + + + + + + + + + + + + + + +