diff --git a/addons_extensions/hr_recruitment_extended/models/candidate_experience.py b/addons_extensions/hr_recruitment_extended/models/candidate_experience.py index 2b6bfdbba..892c1aae5 100644 --- a/addons_extensions/hr_recruitment_extended/models/candidate_experience.py +++ b/addons_extensions/hr_recruitment_extended/models/candidate_experience.py @@ -24,4 +24,4 @@ class CandidateExperience(models.Model): @api.depends('experience_code', 'experience_from', 'experience_to') def _compute_display_name(self): for template in self: - template.display_name = False if not template.experience_code else f"{template.experience_code} ({template.experience_from} - {template.experience_to} Years)" \ No newline at end of file + template.display_name = False if not template.experience_code else (f"{template.experience_code} ({template.experience_from} - {template.experience_to} Years)" if template.experience_to > 0 else f"{template.experience_code}") \ No newline at end of file diff --git a/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py b/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py index 97b114a76..9f8f92579 100644 --- a/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py +++ b/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py @@ -7,6 +7,7 @@ import datetime class HRJobRecruitment(models.Model): _name = 'hr.job.recruitment' + _description = 'Recruitment' _inherit = ['mail.thread', 'mail.activity.mixin'] _inherits = {'hr.job': 'job_id'} _rec_name = 'recruitment_sequence' @@ -233,6 +234,12 @@ class HRJobRecruitment(models.Model): recruitment_status = fields.Selection([('open','Open'),('closed','Closed'),('hold','Hold'),('modified','Modified'),('cancelled','Cancelled')], default='open', tracking=True) + @api.onchange('recruitment_status') + def _onchange_recruitment_status(self): + for rec in self: + if rec.recruitment_status != 'open': + rec.website_published == False + @api.onchange('job_id','job_category') def onchange_job_id(self): for rec in self: diff --git a/addons_extensions/hr_recruitment_extended/models/requisitions.py b/addons_extensions/hr_recruitment_extended/models/requisitions.py index f60bcaf47..d2a4b57ca 100644 --- a/addons_extensions/hr_recruitment_extended/models/requisitions.py +++ b/addons_extensions/hr_recruitment_extended/models/requisitions.py @@ -6,19 +6,52 @@ from odoo.exceptions import ValidationError class RecruitmentRequisition(models.Model): _inherit = 'recruitment.requisition' - hr_job_recruitment = fields.Many2one('hr.job.recruitment') + hr_job_recruitment = fields.Many2one('hr.job.recruitment', tracking=True) position_title = fields.Char(string="Position Title", required=False,related='job_id.name') + job_category = fields.Many2one('job.category', tracking=True) + job_priority = fields.Selection([ + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High') + ], string='Priority', tracking=True) + experience = fields.Many2one('candidate.experience', string="Experience", tracking=True) + recruitment_status = fields.Selection([('open','Open'),('closed','Closed'),('hold','Hold'),('modified','Modified'),('cancelled','Cancelled')],related='hr_job_recruitment.recruitment_status',readonly=False, tracking=True) - def button_create_jd(self): - self.hr_job_recruitment = self.env['hr.job.recruitment'].create({ - 'job_id': self.job_id.id, - 'department_id': self.department_id.id, - 'no_of_recruitment':self.number_of_positions, - 'description':self.job_description, - 'skill_ids': [(6, 0, self.primary_skill_ids.ids)], - 'secondary_skill_ids': [(6, 0, self.secondary_skill_ids.ids)], - 'requested_by': self.requested_by.partner_id.id, - 'user_id': self.assign_to.id - }) + @api.onchange('recruitment_status') + def _onchange_recruitment_status(self): + for rec in self: + if rec.recruitment_status != 'open': + rec.hr_job_recruitment.website_published = False + + @api.onchange('assign_to') + def onchange_assign_to(self): + for rec in self: + if rec.hr_job_recruitment and rec.state == 'jd_created': + rec.hr_job_recruitment.user_id = rec.assign_to.id + + def action_final_approve(self): + res = super().action_final_approve() + for rec in self: + if not rec.hr_job_recruitment: + rec.hr_job_recruitment = self.env['hr.job.recruitment'].create({ + 'job_id': rec.job_id.id if rec.job_id else False, + 'job_category': rec.job_category.id if rec.job_category else False, + 'no_of_recruitment': rec.number_of_positions, + 'description': rec.job_description, + 'skill_ids': [(6, 0, rec.primary_skill_ids.ids)], + 'secondary_skill_ids': [(6, 0, rec.secondary_skill_ids.ids)], + 'requested_by': rec.requested_by.partner_id.id if rec.recruitment_type == 'internal' else rec.client_id.id, + 'recruitment_type': rec.recruitment_type, + 'contract_type_id': rec.contract_type_id.id if rec.contract_type_id else False, + 'address_id': rec.client_id.id if rec.client_id.company_type == 'company' else (rec.client_id.parent_id.id if rec.client_id.parent_id else False), + 'user_id': rec.assign_to.id, + 'address_id': rec.requested_by.company_id.partner_id.id if rec.requested_by.company_id else '', + 'job_priority': rec.job_priority, + 'experience': rec.experience.id if rec.experience else False, + 'budget':rec.budget, + 'target_from': rec.target_startdate if rec.target_startdate else fields.Date.today(), + 'target_to': rec.target_deadline if rec.target_deadline else False, + }) + + return res - self.state ='done' diff --git a/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv b/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv index 532c1851c..8172f10b9 100644 --- a/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv +++ b/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv @@ -31,5 +31,4 @@ access_application_stage_status,application.stage.status,model_application_stage access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1 access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1 access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0 -access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0 -,,,,,,, \ No newline at end of file +access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0 \ No newline at end of file diff --git a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml index 4b82ecaa5..0793f64c8 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml @@ -12,7 +12,7 @@ - +
- Trackers + Job Publish Channels
@@ -101,7 +101,7 @@ @@ -236,6 +236,7 @@ + @@ -261,9 +262,9 @@ domain="[('recruitment_status','=', 'closed')]"/> - - @@ -429,12 +430,28 @@ - - Job Positions Recruitment + + UnPublished Recruitments hr.job.recruitment kanban,list,form,search - {"search_default_Current":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0} + {"search_default_open_status":1,"search_default_my_assignments":1,"search_default_unpublished_records":1,'no_of_eligible_submissions': 0} + +

+ Ready to recruit more efficiently? +

+

+ Let's create a job position Recruitment Requests. +

+
+
+ + + Published Recruitments + hr.job.recruitment + kanban,list,form,search + + {"search_default_open_status":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0}

Ready to recruit more efficiently? @@ -454,13 +471,26 @@ active="0" sequence="2"/> + + + 1 - + + + + + + + + + diff --git a/addons_extensions/menu_control_center/__manifest__.py b/addons_extensions/menu_control_center/__manifest__.py index 48e8d8b62..5288eb79b 100644 --- a/addons_extensions/menu_control_center/__manifest__.py +++ b/addons_extensions/menu_control_center/__manifest__.py @@ -15,13 +15,13 @@ 'security/ir.model.access.csv', 'data/data.xml', 'views/masters.xml', - 'views/groups.xml', + # 'views/groups.xml', 'views/login.xml', 'views/menu_access_control_views.xml', ], # 'assets': { # 'web.assets_backend': [ - # 'menu_control_center/static/src/js/menu_service.js', + # 'menu_control_center/static/src/js/login.js', # ], # }, 'installable': True, diff --git a/addons_extensions/menu_control_center/controllers/main.py b/addons_extensions/menu_control_center/controllers/main.py index 7e333b9bd..bfd75825d 100644 --- a/addons_extensions/menu_control_center/controllers/main.py +++ b/addons_extensions/menu_control_center/controllers/main.py @@ -1,28 +1,53 @@ from odoo import http, _ from odoo.http import request from odoo.addons.web.controllers.home import Home -import werkzeug class CustomMasterLogin(Home): @http.route() def web_login(self, *args, **kw): - # Call the original Odoo login + request.env['ir.ui.menu'].sudo().clear_caches() master_selected = kw.get('master_select') response = super(CustomMasterLogin, self).web_login(*args, **kw) - # We only modify the QWeb response (GET request) if response.is_qweb: - # load your masters - masters = request.env['master.control'].sudo().search([]) - response.qcontext['masters'] = masters + response.qcontext['masters'] = request.env['master.control'].sudo().search([]) request.env['ir.ui.menu'].sudo().clear_caches() request.env['ir.ui.menu'].sudo()._visible_menu_ids() - # After successful login if request.session.uid and master_selected: - request.session['active_master'] = master_selected + user = request.env.user + master = request.env['master.control'].sudo().search( + [('code', '=', master_selected)], limit=1 + ) + + if master.exists() and master.access_group_ids: + if not (user.groups_id & master.access_group_ids): + request.session.logout(keep_db=True) + + # Create a response with JavaScript alert + html = f""" + + + + + + """ + return http.Response(html, content_type='text/html') + # request.session.uid = None + # request.params['login_success'] = False + # response.qcontext['error'] = _( + # "You don't have access to login to '%s'. " + # "Please contact the administrator." + # ) % master.display_name + # return response + + request.session['active_master'] = master.code return response + diff --git a/addons_extensions/menu_control_center/models/__init__.py b/addons_extensions/menu_control_center/models/__init__.py index ba2150479..7a85e40bd 100644 --- a/addons_extensions/menu_control_center/models/__init__.py +++ b/addons_extensions/menu_control_center/models/__init__.py @@ -1,5 +1,5 @@ from . import masters from . import ir_http -from . import groups +# from . import groups from . import models from . import menu \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/masters.py b/addons_extensions/menu_control_center/models/masters.py index 2d77a54ac..840e61007 100644 --- a/addons_extensions/menu_control_center/models/masters.py +++ b/addons_extensions/menu_control_center/models/masters.py @@ -7,8 +7,7 @@ class MasterControl(models.Model): sequence = fields.Integer() name = fields.Char(string='Master Name', required=True) code = fields.Char(string='Code', required=True) - default_show = fields.Boolean(default=True) - access_group_ids = fields.One2many('group.access.line','master_control_id',string='Roles') + access_group_ids = fields.Many2many('res.groups',string='Roles') @api.depends('name', 'code') def _compute_display_name(self): @@ -18,87 +17,87 @@ class MasterControl(models.Model): else: record.display_name = False - def action_generate_groups(self): - """Generate category → groups list""" - for rec in self: + # def action_generate_groups(self): + # """Generate category → groups list""" + # for rec in self: + # + # # Clear old groups + # rec.access_group_ids.unlink() + # + # groups = self.env['res.groups'].sudo().search([],order='category_id') + # show_group = True if rec.default_show else False + # print(show_group) + # for grp in groups: + # self.env['group.access.line'].create({ + # 'master_control_id': rec.id, + # 'group_id': grp.id, + # 'show_group': show_group, + # }) + # + # return { + # 'type': 'ir.actions.client', + # 'tag': 'display_notification', + # 'params': { + # 'title': _('Success'), + # 'message': _('Groups generated successfully!'), + # 'type': 'success', + # } + # } + # + # # ----------------------------------------- + # # UPDATE GROUPS (Detect new groups) + # # ----------------------------------------- + # def action_update_groups(self): + # import pdb + # pdb.set_trace() + # for rec in self: + # created_count = 0 + # + # existing_ids = set(rec.access_group_ids.mapped('group_id.id')) + # + # categories = self.env['ir.module.category'].search([]) + # + # for category in categories: + # groups = self.env['res.groups'].search([ + # ('category_id', '=', category.id) + # ]) + # + # for grp in groups: + # # create only missing group + # if grp.id not in existing_ids: + # rec.access_group_ids.create({ + # 'master_control_id': rec.id, + # 'category_id': category.id, + # 'group_id': grp.id, + # 'show_group': True if rec.default_show else False, + # }) + # created_count += 1 + # existing_ids.add(grp.id) + # + # if created_count: + # message = f"Added {created_count} new groups." + # msg_type = "success" + # else: + # message = "No new groups found." + # msg_type = "info" + # + # return { + # 'type': 'ir.actions.client', + # 'tag': 'display_notification', + # 'params': { + # 'title': _('Group Update'), + # 'message': _(message), + # 'type': msg_type, + # } + # } - # Clear old groups - rec.access_group_ids.unlink() - - groups = self.env['res.groups'].sudo().search([],order='category_id') - show_group = True if rec.default_show else False - print(show_group) - for grp in groups: - self.env['group.access.line'].create({ - 'master_control_id': rec.id, - 'group_id': grp.id, - 'show_group': show_group, - }) - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Success'), - 'message': _('Groups generated successfully!'), - 'type': 'success', - } - } - - # ----------------------------------------- - # UPDATE GROUPS (Detect new groups) - # ----------------------------------------- - def action_update_groups(self): - import pdb - pdb.set_trace() - for rec in self: - created_count = 0 - - existing_ids = set(rec.access_group_ids.mapped('group_id.id')) - - categories = self.env['ir.module.category'].search([]) - - for category in categories: - groups = self.env['res.groups'].search([ - ('category_id', '=', category.id) - ]) - - for grp in groups: - # create only missing group - if grp.id not in existing_ids: - rec.access_group_ids.create({ - 'master_control_id': rec.id, - 'category_id': category.id, - 'group_id': grp.id, - 'show_group': True if rec.default_show else False, - }) - created_count += 1 - existing_ids.add(grp.id) - - if created_count: - message = f"Added {created_count} new groups." - msg_type = "success" - else: - message = "No new groups found." - msg_type = "info" - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Group Update'), - 'message': _(message), - 'type': msg_type, - } - } - - -class GroupsAccessLine(models.Model): - _name = 'group.access.line' - _description = 'Group Access Line' - _rec_name = 'group_id' - - category_id = fields.Many2one('ir.module.category', related='group_id.category_id') - group_id = fields.Many2one('res.groups', string="Role") - show_group = fields.Boolean(string="Show", default=True) - master_control_id = fields.Many2one('master.control') \ No newline at end of file +# +# class GroupsAccessLine(models.Model): +# _name = 'group.access.line' +# _description = 'Group Access Line' +# _rec_name = 'group_id' +# +# category_id = fields.Many2one('ir.module.category', related='group_id.category_id') +# group_id = fields.Many2one('res.groups', string="Role") +# show_group = fields.Boolean(string="Show", default=True) +# master_control_id = fields.Many2one('master.control') \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/models.py b/addons_extensions/menu_control_center/models/models.py index a33ec72a0..a21211600 100644 --- a/addons_extensions/menu_control_center/models/models.py +++ b/addons_extensions/menu_control_center/models/models.py @@ -53,119 +53,143 @@ class MenuAccessControl(models.Model): all_subs |= self._get_all_submenus(sm) return all_subs + def _get_all_menus_sql(self): + """ + Fetch all active menus with hierarchy using SQL + Returns list of dicts: + [ + {id, parent_id} + ] + """ + self.env.cr.execute(""" + WITH RECURSIVE menu_tree AS ( + SELECT id, parent_id + FROM ir_ui_menu + WHERE parent_id IS NULL + AND active = true + + UNION ALL + + SELECT m.id, m.parent_id + FROM ir_ui_menu m + JOIN menu_tree mt ON mt.id = m.parent_id + WHERE m.active = true + ) + SELECT id, parent_id + FROM menu_tree + ORDER BY parent_id NULLS FIRST, id + """) + return self.env.cr.dictfetchall() + def action_generate_menus(self): """ - Generate main menus and all submenus (recursive), + Generate main menus and all submenus (SQL-based), and set access_line_id for every submenu. """ - for rec in self: + MenuLine = self.env['menu.access.line'] - # clear old menus + for rec in self: + # Clear old menus rec.access_menu_line_ids.unlink() rec.access_sub_menu_line_ids.unlink() - active_menus = self.env['ir.ui.menu'].search([ - ('parent_id', '=', False), - ('active', '=', True) - ]) + menus = rec._get_all_menus_sql() - for menu in active_menus: + # Map: parent_id -> children + children_map = {} + for m in menus: + children_map.setdefault(m['parent_id'], []).append(m) - # 1️⃣ Create main menu line - main_line = self.env['menu.access.line'].create({ + # ---------- Main menus ---------- + for main in children_map.get(None, []): + + main_line = MenuLine.create({ 'access_control_id': rec.id, - 'menu_id': menu.id, + 'menu_id': main['id'], 'is_main_menu': True, }) - # 2️⃣ Fetch all recursive submenus - submenus = self._get_all_submenus(menu) - - # 3️⃣ Create submenu lines with correct parent - for sm in submenus: - self.env['menu.access.line'].create({ + # ---------- Submenus ---------- + stack = children_map.get(main['id'], []) + while stack: + sm = stack.pop() + MenuLine.create({ 'access_control_id': rec.id, - 'menu_id': sm.id, + 'menu_id': sm['id'], 'is_main_menu': True, - 'access_line_id': main_line.id, # important + 'access_line_id': main_line.id, }) + stack.extend(children_map.get(sm['id'], [])) def action_update_menus(self): - line = self.env['menu.access.line'] - menu = self.env['ir.ui.menu'] + MenuLine = self.env['menu.access.line'] for rec in self: created_count = 0 - # All existing menu IDs across BOTH One2manys + # Existing menu IDs existing_menu_ids = set( rec.access_menu_line_ids.mapped('menu_id.id') + rec.access_sub_menu_line_ids.mapped('menu_id.id') ) - # ---------- Step 1: Ensure all top-level menus exist ---------- - top_menus = menu.search([ - ('parent_id', '=', False), - ('active', '=', True), - ]) + menus = rec._get_all_menus_sql() - for menu in top_menus: + # Map parent -> children + children_map = {} + for m in menus: + children_map.setdefault(m['parent_id'], []).append(m) - # Create missing MAIN MENU - main_line = line.search([ + # ---------- Main menus ---------- + for main in children_map.get(None, []): + + main_line = MenuLine.search([ ('access_control_id', '=', rec.id), - ('menu_id', '=', menu.id), - ('access_line_id', '=', False) + ('menu_id', '=', main['id']), + ('access_line_id', '=', False), ], limit=1) if not main_line: - main_line = line.create({ + main_line = MenuLine.create({ 'access_control_id': rec.id, - 'menu_id': menu.id, + 'menu_id': main['id'], 'is_main_menu': True, }) created_count += 1 - existing_menu_ids.add(menu.id) + existing_menu_ids.add(main['id']) - # ---------- Step 2: Ensure all SUBMENUS exist ---------- - submenus = rec._get_all_submenus(menu) + # ---------- Submenus ---------- + stack = children_map.get(main['id'], []) + while stack: + sm = stack.pop() - for sm in submenus: - - # If submenu is missing → create it - if sm.id not in existing_menu_ids: - line.create({ + if sm['id'] not in existing_menu_ids: + MenuLine.create({ 'access_control_id': rec.id, - 'menu_id': sm.id, + 'menu_id': sm['id'], 'is_main_menu': True, 'access_line_id': main_line.id, }) created_count += 1 - existing_menu_ids.add(sm.id) + existing_menu_ids.add(sm['id']) + + stack.extend(children_map.get(sm['id'], [])) # ---------- Notification ---------- - if created_count: - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Success'), - 'message': _('Added %s new menu(s) (including submenus)') % created_count, - 'type': 'success', - 'sticky': False, - } - } - else: - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Info'), - 'message': _('No new menus found to add.'), - 'type': 'info', - 'sticky': False, - } + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success') if created_count else _('Info'), + 'message': ( + _('Added %s new menu(s) (including submenus)') % created_count + if created_count else + _('No new menus found to add.') + ), + 'type': 'success' if created_count else 'info', + 'sticky': False, } + } class MenuAccessLine(models.Model): diff --git a/addons_extensions/menu_control_center/security/ir.model.access.csv b/addons_extensions/menu_control_center/security/ir.model.access.csv index f6e0f1e02..59b08a1cb 100644 --- a/addons_extensions/menu_control_center/security/ir.model.access.csv +++ b/addons_extensions/menu_control_center/security/ir.model.access.csv @@ -4,4 +4,3 @@ access_menu_access_line,access.menu.access.line,model_menu_access_line,hr.group_ access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1 access_master_control_public,master.control.public,model_master_control,base.group_public,1,0,0,0 access_master_control_hr,master.control.hr,model_master_control,hr.group_hr_manager,1,1,1,1 -group_access_line_access,group_access_line_access,model_group_access_line,,1,1,1,1 diff --git a/addons_extensions/menu_control_center/static/src/js/login.js b/addons_extensions/menu_control_center/static/src/js/login.js new file mode 100644 index 000000000..b45835e1a --- /dev/null +++ b/addons_extensions/menu_control_center/static/src/js/login.js @@ -0,0 +1,27 @@ +odoo.define('menu_control_center.login', function (require) { + "use strict"; + + var WebClient = require('web.WebClient'); + + WebClient.include({ + show_application: function() { + var self = this; + var res = this._super.apply(this, arguments); + + // Check for flash messages in session + var flash_msg = sessionStorage.getItem('flash_message'); + var flash_type = sessionStorage.getItem('flash_type'); + + if (flash_msg) { + // Show notification + this.do_notify(flash_msg, '', true); + + // Clear stored messages + sessionStorage.removeItem('flash_message'); + sessionStorage.removeItem('flash_type'); + } + + return res; + }, + }); +}); \ No newline at end of file diff --git a/addons_extensions/menu_control_center/views/masters.xml b/addons_extensions/menu_control_center/views/masters.xml index b15db1e26..9416757e6 100644 --- a/addons_extensions/menu_control_center/views/masters.xml +++ b/addons_extensions/menu_control_center/views/masters.xml @@ -18,34 +18,28 @@ master.control

-
-
+ + + + + - + - - - - - - - + diff --git a/addons_extensions/menu_control_center/views/menu_access_control_views.xml b/addons_extensions/menu_control_center/views/menu_access_control_views.xml index 3460bb2ac..78fe2338d 100644 --- a/addons_extensions/menu_control_center/views/menu_access_control_views.xml +++ b/addons_extensions/menu_control_center/views/menu_access_control_views.xml @@ -96,7 +96,7 @@ - + @@ -106,8 +106,8 @@ - - + + diff --git a/addons_extensions/requisitions/__manifest__.py b/addons_extensions/requisitions/__manifest__.py index 6644233f8..84b0f33f1 100644 --- a/addons_extensions/requisitions/__manifest__.py +++ b/addons_extensions/requisitions/__manifest__.py @@ -9,7 +9,7 @@ 'website': "https://www.ftprotech.com", 'category': 'Recruitment', 'version': '0.1', - 'depends': ['hr_recruitment', 'mail','base','hr'], + 'depends': ['hr_recruitment', 'mail','base','hr','account'], 'data': [ 'security/ir.model.access.csv', 'data/ir_sequence.xml', diff --git a/addons_extensions/requisitions/data/mail_templates.xml b/addons_extensions/requisitions/data/mail_templates.xml index eb99be74d..f9bc19059 100644 --- a/addons_extensions/requisitions/data/mail_templates.xml +++ b/addons_extensions/requisitions/data/mail_templates.xml @@ -12,26 +12,46 @@

Dear John Doe, -

+
+
A new requisition has been submitted for your review:

    -
  • Requisition Name: New
  • -
  • Department: Sales
  • -
  • Position Title: Sales Manager
  • -
  • Number of Positions: 3
  • -
  • Requested By: Emily Clark
  • -
  • Requested On: 2024-12-31
  • +
  • + Requisition Name: + New +
  • +
  • + Position Title: + Sales Manager +
  • +
  • + Number of Positions: + 3 +
  • +
  • + Requested By: + Emily Clark +
  • +
  • + Requested On: + 2024-12-31 +
-
+
- Notes: Requirement -
+ Notes: + Requirement +
-
- Please click here to view the requisition details. -

+
+ Please click here + to view the requisition details. +
+
Regards, -
+
Emily Clark

@@ -40,8 +60,6 @@ - - Recruitment Requisition Cancellation @@ -50,23 +68,216 @@ Requisition Cancelled: {{ object.name }}
-

Dear HR Manager,

-

The requisition New has been cancelled for the following reason:

-

+

Dear HR Manager, +

+

The requisition + + New + + has been cancelled for the following reason: +

+

+ +

You can view the requisition details by clicking the link below:
- + View Requisition

Regards,

-

Requested By

+

+ Requested By +

-
-
+ + + + Recruitment Requisition: Sent to Finance + + {{ object.hr_manager_id.email_formatted or user.email_formatted }} + {{ object.finance_manager_id.email }} + Requisition Awaiting Finance Approval: {{ object.name }} + +
+

Dear + + , +

+ +

The following recruitment requisition has been reviewed by HR and is now awaiting your + approval: +

+ +
    +
  • + Requisition: + +
  • +
  • + Position: + +
  • +
  • + Positions Required: + +
  • +
  • + Requested By: + +
  • +
+ +

+ + View Requisition + +

+ +

Regards,
HR Team +

+
+
+
+ + Recruitment Requisition: Finance Approved + + {{ object.finance_manager_id.email_formatted or user.email_formatted }} + {{ object.hr_manager_id.email }} + Finance Approved: {{ object.name }} + +
+

Dear + + , +

+ +

The finance team has approved the following recruitment requisition:

+ +
    +
  • + Requisition: + +
  • +
  • + Position: + +
  • +
  • + Finance Comments: + +
  • +
+ +

Please proceed with final approval.

+ +

+ + Review Requisition + +

+ +

Regards,
Finance Team +

+
+
+
+ + + + Recruitment Requisition: Finance Rejected + + {{ object.finance_manager_id.email_formatted or user.email_formatted }} + {{ object.hr_manager_id.email }} + Finance Rejected: {{ object.name }} + +
+

Dear + + , +

+ +

The finance team has Rejected the following recruitment requisition:

+ +
    +
  • + Requisition: + +
  • +
  • + Position: + +
  • +
  • + Finance Comments: + +
  • +
+ +

+ + Review Requisition + +

+ +

Regards,
Finance Team +

+
+
+
+ + + Recruitment Requisition: Approved & JD Creation + + {{ user.email_formatted }} + {{ object.assign_to.email }} + JD Creation Required: {{ object.name }} + +
+

Dear + + , +

+ +

The following recruitment requisition has received final approval.

+ +
    +
  • + Requisition: + +
  • +
  • + Position: + +
  • +
+ +

Please proceed with Job Description creation.

+ +

+ + Open Requisition + +

+ +

Regards,
HR Team +

+
+
+
+ diff --git a/addons_extensions/requisitions/models/hr_requisition.py b/addons_extensions/requisitions/models/hr_requisition.py index 05c420efa..0bfe23ddf 100644 --- a/addons_extensions/requisitions/models/hr_requisition.py +++ b/addons_extensions/requisitions/models/hr_requisition.py @@ -1,6 +1,5 @@ from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError - +from odoo.exceptions import ValidationError, UserError class RecruitmentRequisition(models.Model): @@ -8,75 +7,133 @@ class RecruitmentRequisition(models.Model): _description = 'Recruitment Requisition' _inherit = ['mail.thread', 'mail.activity.mixin'] - name = fields.Char(string="Requisition Name", required=True, default="New") - department_id = fields.Many2one('hr.department', string="Department", required=True) - requested_by = fields.Many2one('res.users', string="Requested By", default=lambda self: self.env.user) - requested_on = fields.Date(string='Requested On') + jd_file = fields.Binary(string="JD") + jd_file_name = fields.Char(string="JD") + name = fields.Char(string="Requisition Name", required=True, default="New", tracking=True) + requested_by = fields.Many2one('res.users', string="Requested By", default=lambda self: self.env.user, tracking=True) + client_id = fields.Many2one('res.partner', string="Client", default=lambda self: self.env.user.company_id.partner_id, tracking=True) + recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], default='internal') + requested_on = fields.Date(string='Requested On', tracking=True) hr_manager_id = fields.Many2one('res.users', string="HR Manager", compute='_compute_hr_manager') - hr_job = fields.Many2one('') - position_title = fields.Char(string="Position Title", required=True) - number_of_positions = fields.Integer(string="Number of Positions", required=True) + finance_manager_id = fields.Many2one('res.users',string="Finance Manager", compute="_compute_finance_manager") + is_hr = fields.Boolean(compute="_compute_check_access") + is_finance_manager = fields.Boolean(compute="_compute_check_access") + + @api.onchange('client_id') + def onchange_client_id(self): + for rec in self: + if rec.client_id != rec.requested_by.company_id.partner_id and rec.client_id != rec.requested_by.partner_id: + rec.recruitment_type = 'external' + else: + rec.recruitment_type = 'internal' + + @api.depends('hr_manager_id','finance_manager_id') + def _compute_check_access(self): + for rec in self: + rec.is_hr = False + rec.is_finance_manager = False + if (rec.hr_manager_id and rec.hr_manager_id == self.env.user) or self.env.user.id == 2: + rec.is_hr = True + if (rec.finance_manager_id and rec.finance_manager_id == self.env.user) or self.env.user.id == 2: + rec.is_finance_manager = True + + + hr_job = fields.Many2one('hr.job', tracking=True) + number_of_positions = fields.Integer(string="Number of Positions", required=True, tracking=True) job_description = fields.Html(string="Job Summary") - job_id = fields.Many2one('hr.job',"JD ID") + job_id = fields.Many2one('hr.job',"Job Position", tracking=True) state = fields.Selection([ ('draft', 'Draft'), - ('waiting_approval', 'Waiting Approval'), - ('approved', 'Approved'), - ('done', 'Done'), + ('hr', 'HR Manager'), + ('finance', 'Finance'), + ('final', 'Final Approval'), + ('jd_created', 'JD Created'), + ('rejected', 'Rejected'), ('cancel', 'Cancelled') - ], default='draft', track_visibility='onchange') - notes = fields.Text(string="Notes") + ], default='draft', tracking=True) + notes = fields.Text(string="Notes", tracking=True) primary_skill_ids = fields.Many2many('hr.skill', "recruitment_requisition_primary_hr_skill_rel", - 'hr_job_id', 'hr_skill_id', "Primary Skills", required=True) + 'hr_job_id', 'hr_skill_id', "Primary Skills", required=True, tracking=True) secondary_skill_ids = fields.Many2many('hr.skill', "recruitment_requisition_secondary_hr_skill_rel", - 'hr_job_id', 'hr_skill_id', "Secondary Skills") + 'hr_job_id', 'hr_skill_id', "Secondary Skills", tracking=True) assign_to = fields.Many2one('res.users', domain=lambda self: [ - ('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)]) + ('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)], tracking=True) + finance_notes = fields.Text(string="Finance Comments", tracking=True) + budget = fields.Char(string="Budget",tracking=True) + requested_deadline = fields.Date(string="Required Before",tracking=True) + target_deadline = fields.Date(string="Target Deadline",tracking=True) + target_startdate = fields.Date(string="Start Date",tracking=True) + contract_type_id = fields.Many2one('hr.contract.type',string="Employment Type") + + def action_finance_reject(self): + for rec in self: + if not rec.finance_notes: + raise UserError(_("Please enter rejection comments before rejecting.")) + + rec.state = 'hr' + template = self.env.ref( + 'requisitions.mail_template_recruitment_requisition_finance_reject', + raise_if_not_found=False + ) + if template: + template.send_mail(rec.id, force_send=True) @api.depends('requested_by') def _compute_hr_manager(self): hr_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_hr_id') self.hr_manager_id = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False - def action_submit(self): + @api.depends('requested_by') + def _compute_finance_manager(self): + finance_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_finance_manager_id') + self.finance_manager_id = self.env['res.users'].sudo().browse(int(finance_id)) if finance_id else False + + def action_submit_hr(self): self.name = self.env['ir.sequence'].next_by_code('hr.requisitions') self.requested_on = fields.Date.today() - self.state = 'waiting_approval' + self.state = 'hr' + if self.requested_deadline: + self.target_deadline = self.requested_deadline template = self.env.ref('requisitions.mail_template_recruitment_requisition_notification') # Replace `module_name` with your module name if template: template.send_mail(self.id, force_send=True) - def action_approve(self): + def action_send_finance(self): + self.state = 'finance' + template = self.env.ref('requisitions.mail_template_recruitment_requisition_finance', raise_if_not_found=False) + if template: + template.send_mail(self.id, force_send=True) + + def action_send_hr(self): + self.state = 'final' + template = self.env.ref('requisitions.mail_template_recruitment_requisition_final', raise_if_not_found=False) + if template: + template.send_mail(self.id, force_send=True) + + def action_final_approve(self): for rec in self: if not rec.assign_to: - raise ValidationError(_("Please Assign a recruitment user")) - rec.state = 'approved' - rec.button_create_jd() + raise ValidationError(_("Please assign a recruitment user.")) + + rec.state = 'jd_created' + template = self.env.ref( + 'requisitions.mail_template_recruitment_requisition_jd', + raise_if_not_found=False + ) + if template: + template.send_mail(rec.id, force_send=True) def action_cancel(self): return { 'type': 'ir.actions.act_window', 'res_model': 'recruitment.requisition.cancel.wizard', + 'name': _('Cancellation Reason'), 'view_mode': 'form', 'target': 'new', 'context': {'active_id': self.id}, } - def button_create_jd(self): - self.job_id = self.env['hr.job'].create({ - 'name': self.position_title, - 'department_id': self.department_id.id, - 'no_of_recruitment':self.number_of_positions, - 'description':self.job_description, - 'skill_ids': [(6, 0, self.primary_skill_ids.ids)], - 'secondary_skill_ids': [(6, 0, self.secondary_skill_ids.ids)], - 'requested_by': self.requested_by.partner_id.id, - 'user_id': self.assign_to.id - }) - - self.state ='done' - class HRJob(models.Model): diff --git a/addons_extensions/requisitions/models/res_config_settings.py b/addons_extensions/requisitions/models/res_config_settings.py index 05df35469..63475b6a3 100644 --- a/addons_extensions/requisitions/models/res_config_settings.py +++ b/addons_extensions/requisitions/models/res_config_settings.py @@ -9,6 +9,9 @@ class ResConfigSettings(models.TransientModel): requisition_hr_id = fields.Many2one('res.users',config_parameter='requisitions.requisition_hr_id', string='Requisition HR', domain=lambda self: [ - ('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_manager').id)]) + ('groups_id', 'in', self.env.ref('hr.group_hr_manager').id)]) - # requisition_md_id = fields.Many2one('res.users', string='Requisition MD') \ No newline at end of file + requisition_finance_manager = fields.Many2one('res.users',config_parameter='requisitions.requisition_finance_manager_id', string='Requisition Finance Manager', + domain=lambda self: [ + ('groups_id', 'in', + self.env.ref('account.group_account_manager').id)]) \ No newline at end of file diff --git a/addons_extensions/requisitions/views/hr_requisition.xml b/addons_extensions/requisitions/views/hr_requisition.xml index 3c1009ccc..4e52fa88f 100644 --- a/addons_extensions/requisitions/views/hr_requisition.xml +++ b/addons_extensions/requisitions/views/hr_requisition.xml @@ -1,28 +1,18 @@ - - - - view.hr.job.form - hr.job - - - - - - - requisition.form recruitment.requisition
-
@@ -34,25 +24,45 @@
- - - + + + + + + + + - - + + - - - + + + + + + - - - - + + + + + + + + + + + + + + +
@@ -65,7 +75,6 @@ - @@ -77,6 +86,13 @@ list,form
- + + +
diff --git a/addons_extensions/requisitions/views/res_config_settings.xml b/addons_extensions/requisitions/views/res_config_settings.xml index ce1260a4a..f828a4f79 100644 --- a/addons_extensions/requisitions/views/res_config_settings.xml +++ b/addons_extensions/requisitions/views/res_config_settings.xml @@ -8,10 +8,16 @@ + + + +
diff --git a/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml b/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml index b315e1938..e40b46dd5 100644 --- a/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml +++ b/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml @@ -7,7 +7,7 @@
- +