diff --git a/docsource/modules180-190.rst b/docsource/modules180-190.rst index 927763303295..c50dff02acb3 100644 --- a/docsource/modules180-190.rst +++ b/docsource/modules180-190.rst @@ -200,7 +200,7 @@ Module coverage 18.0 -> 19.0 +---------------------------------------------------+----------------------+-------------------------------------------------+ | hr_presence | | | +---------------------------------------------------+----------------------+-------------------------------------------------+ -| hr_recruitment | | | +| hr_recruitment |Done | | +---------------------------------------------------+----------------------+-------------------------------------------------+ | hr_recruitment_skills | | | +---------------------------------------------------+----------------------+-------------------------------------------------+ diff --git a/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/post-migration.py b/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/post-migration.py new file mode 100644 index 000000000000..58c5a35cf48a --- /dev/null +++ b/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/post-migration.py @@ -0,0 +1,120 @@ +# Copyright 2026 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + +_deleted_xmlids = [ + "hr_recruitment.hr_candidate_comp_rule", + "hr_recruitment.hr_candidate_interviewer_rule", + "hr_recruitment.hr_candidate_user_rule", +] + + +def hr_candidate2hr_applicant(env): + """ + hr.candidate and hr.applicant have been merged. Copy data for nonstored related + v18 fields from hr_candidate to hr_applicant where they are stored in v19. + """ + field_names = [ + "availability", + "color", + "email_from", + "email_normalized", + "employee_id", + "linkedin_profile", + "partner_id", + "partner_name", + "partner_phone", + "partner_phone_sanitized", + "type_id", + "message_bounce", + ] + updates = ", ".join( + f"{field_name} = hr_candidate.{field_name}" for field_name in field_names + ) + updates += ", phone_sanitized=hr_candidate.partner_phone_sanitized" + + env.cr.execute( + f""" + UPDATE hr_applicant + SET + {updates} + FROM {openupgrade.get_legacy_name("hr_candidate")} hr_candidate + WHERE + hr_applicant.candidate_id=hr_candidate.id + """ + ) + + +def hr_candidate_properties2hr_applicant_properties(env): + """ + Merge applicant and candidate properties + Merge property definitions from companies into jobs' applicant property definitions + if there are v18 candidates setting them. + Ignore applicants not linked to a job + """ + env.cr.execute( + f""" + UPDATE hr_applicant + SET applicant_properties= + COALESCE(applicant_properties, '{{}}') || + hr_candidate.candidate_properties + FROM {openupgrade.get_legacy_name("hr_candidate")} hr_candidate + WHERE + hr_applicant.candidate_id=hr_candidate.id + AND + hr_candidate.candidate_properties IS NOT NULL + """ + ) + if env.cr.rowcount: + env.cr.execute( + f""" + UPDATE hr_job + SET applicant_properties_definition= + COALESCE(applicant_properties_definition, '[]') || + res_company.candidate_properties_definition + FROM res_company + WHERE + ( + hr_job.company_id=res_company.id + OR + hr_job.company_id IS NULL + ) + AND + res_company.candidate_properties_definition IS NOT NULL + AND + exists ( + SELECT + hr_applicant.id + FROM hr_applicant + JOIN + {openupgrade.get_legacy_name("hr_candidate")} hr_candidate + ON hr_applicant.candidate_id=hr_candidate.id + WHERE + hr_candidate.candidate_properties IS NOT NULL + AND + hr_applicant.job_id=hr_job.id + AND + hr_candidate.company_id=res_company.id + ) + """ + ) + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.load_data(env, "hr_recruitment", "19.0.1.1/noupdate_changes.xml") + openupgrade.delete_record_translations( + env.cr, + "hr_recruitment", + [ + "email_template_data_applicant_congratulations", + "email_template_data_applicant_interest", + "email_template_data_applicant_not_interested", + "email_template_data_applicant_refuse", + ], + ["body_html"], + ) + openupgrade.delete_records_safely_by_xml_id(env, _deleted_xmlids) + hr_candidate2hr_applicant(env) + hr_candidate_properties2hr_applicant_properties(env) diff --git a/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/pre-migration.py b/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/pre-migration.py new file mode 100644 index 000000000000..19e53396c768 --- /dev/null +++ b/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/pre-migration.py @@ -0,0 +1,23 @@ +# Copyright 2026 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def hr_candidate2hr_applicant(env): + """ + Prepare merging hr.candidate into hr.applicant: + Merge models + Rename hr_candidate to legacy table + Lift constraints on hr_candidate.id + """ + openupgrade.merge_models(env.cr, "hr.candidate", "hr.applicant", "candidate_id") + openupgrade.rename_tables(env.cr, [("hr_candidate", None)]) + openupgrade.lift_constraints( + env.cr, openupgrade.get_legacy_name("hr_candidate"), "id", cascade=True + ) + + +@openupgrade.migrate() +def migrate(env, version): + hr_candidate2hr_applicant(env) diff --git a/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/upgrade_analysis_work.txt b/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/upgrade_analysis_work.txt new file mode 100644 index 000000000000..4809fc7ea08c --- /dev/null +++ b/openupgrade_scripts/scripts/hr_recruitment/19.0.1.1/upgrade_analysis_work.txt @@ -0,0 +1,260 @@ +---Models in module 'hr_recruitment'--- +obsolete model candidate.send.mail [transient] + +# NOTHING TO DO + +obsolete model hr.candidate (merged to hr.applicant) + +# DONE: merge models in pre-migration, adapt data in post + +new model hr.talent.pool +new model job.add.applicants [transient] +new model talent.pool.add.applicants [transient] + +# NOTHING TO DO + +---Fields in module 'hr_recruitment'--- +hr_recruitment / calendar.event / candidate_id (many2one) : DEL relation: hr.candidate +hr_recruitment / hr.applicant / _order : _order is now 'priority desc, sequence, id desc' ('priority desc, id desc') + +# NOTHING TO DO + +hr_recruitment / hr.applicant / availability (date) : is now stored +hr_recruitment / hr.applicant / availability (date) : not related anymore + +# DONE: copied from hr.candidate + +hr_recruitment / hr.applicant / candidate_id (many2one) : DEL relation: hr.candidate, required + +# DONE: not null constraint lifted + +hr_recruitment / hr.applicant / color (integer) : is now stored +hr_recruitment / hr.applicant / color (integer) : not related anymore +hr_recruitment / hr.applicant / email_from (char) : is now stored +hr_recruitment / hr.applicant / email_from (char) : not related anymore +hr_recruitment / hr.applicant / email_from (char) : now a function +hr_recruitment / hr.applicant / email_normalized (char) : is now stored +hr_recruitment / hr.applicant / email_normalized (char) : not related anymore +hr_recruitment / hr.applicant / email_normalized (char) : now a function +hr_recruitment / hr.applicant / employee_id (many2one) : is now stored +hr_recruitment / hr.applicant / employee_id (many2one) : not related anymore + +# DONE: copied from hr.candidate + +hr_recruitment / hr.applicant / kanban_state (selection) : selection_keys added: [waiting] (most likely nothing to do) + +# NOTHING TO DO + +hr_recruitment / hr.applicant / linkedin_profile (char) : is now stored +hr_recruitment / hr.applicant / linkedin_profile (char) : not related anymore + +# DONE: copied from hr.candidate + +hr_recruitment / hr.applicant / message_bounce (integer) : NEW hasdefault: default + +# NOTHING TO DO + +hr_recruitment / hr.applicant / partner_id (many2one) : is now stored +hr_recruitment / hr.applicant / partner_id (many2one) : not related anymore +hr_recruitment / hr.applicant / partner_name (char) : is now stored +hr_recruitment / hr.applicant / partner_name (char) : not a function anymore +hr_recruitment / hr.applicant / partner_phone (char) : is now stored +hr_recruitment / hr.applicant / partner_phone (char) : not related anymore +hr_recruitment / hr.applicant / partner_phone (char) : now a function +hr_recruitment / hr.applicant / partner_phone_sanitized (char): is now stored +hr_recruitment / hr.applicant / partner_phone_sanitized (char): not related anymore +hr_recruitment / hr.applicant / partner_phone_sanitized (char): now a function + +# DONE: copied from hr.candidate + +hr_recruitment / hr.applicant / phone_mobile_search (char) : NEW stored: False + +# NOTHING TO DO + +hr_recruitment / hr.applicant / phone_sanitized (char) : NEW isfunction: function, stored + +# DONE: set from hr.candidate#partner_phone_sanitized + +hr_recruitment / hr.applicant / pool_applicant_id (many2one) : NEW relation: hr.applicant +hr_recruitment / hr.applicant / sequence (integer) : NEW hasdefault: default +hr_recruitment / hr.applicant / talent_pool_ids (many2many) : NEW relation: hr.talent.pool + +# NOTHING TO DO + +hr_recruitment / hr.applicant / type_id (many2one) : is now stored +hr_recruitment / hr.applicant / type_id (many2one) : not related anymore + +# DONE: copied from hr.candidate + +hr_recruitment / hr.candidate / active (boolean) : DEL + +# NOTHING TO DO + +hr_recruitment / hr.candidate / activity_ids (one2many) : DEL relation: mail.activity + +# DONE: updated by merge_model + +hr_recruitment / hr.candidate / applicant_ids (one2many) : DEL relation: hr.applicant + +# NOTHING TO DO + +hr_recruitment / hr.candidate / attachment_ids (one2many) : DEL relation: ir.attachment + +# DONE: updated by merge_model + +hr_recruitment / hr.candidate / availability (date) : DEL + +# DONE: copied to hr.applicant + +hr_recruitment / hr.candidate / candidate_properties (properties): DEL + +# DONE: merged into hr.applicant#applicant_properties + +hr_recruitment / hr.candidate / categ_ids (many2many) : DEL relation: hr.applicant.category + +# NOTHING TO DO + +hr_recruitment / hr.candidate / color (integer) : DEL + +# DONE: copied to hr.applicant + +hr_recruitment / hr.candidate / company_id (many2one) : DEL relation: res.company +hr_recruitment / hr.candidate / email_cc (char) : DEL + +# NOTHING TO DO + +hr_recruitment / hr.candidate / email_from (char) : DEL +hr_recruitment / hr.candidate / email_normalized (char) : DEL +hr_recruitment / hr.candidate / employee_id (many2one) : DEL relation: hr.employee +hr_recruitment / hr.candidate / linkedin_profile (char) : DEL + +# DONE: copied to hr.applicant + +hr_recruitment / hr.candidate / meeting_ids (one2many) : DEL relation: calendar.event + +# NOTHING TO DO + +hr_recruitment / hr.candidate / message_bounce (integer) : DEL + +# DONE: copied to hr.applicant + +hr_recruitment / hr.candidate / message_follower_ids (one2many): DEL relation: mail.followers +hr_recruitment / hr.candidate / message_ids (one2many) : DEL relation: mail.message + +# DONE: updated by merge_model + +hr_recruitment / hr.candidate / message_main_attachment_id (many2one): DEL relation: ir.attachment + +# NOTHING TO DO: hr.applicant has its own main attachment + +hr_recruitment / hr.candidate / partner_id (many2one) : DEL relation: res.partner +hr_recruitment / hr.candidate / partner_name (char) : DEL +hr_recruitment / hr.candidate / partner_phone (char) : DEL +hr_recruitment / hr.candidate / partner_phone_sanitized (char): DEL +hr_recruitment / hr.candidate / phone_mobile_search (char) : DEL stored: False +hr_recruitment / hr.candidate / phone_sanitized (char) : DEL + +# DONE: copied to hr.applicant + +hr_recruitment / hr.candidate / priority (selection) : DEL selection_keys: ['0', '1', '2', '3'] +hr_recruitment / hr.candidate / rating_ids (one2many) : DEL relation: rating.rating + +# NOTHING TO DO + +hr_recruitment / hr.candidate / type_id (many2one) : DEL relation: hr.recruitment.degree + +# DONE: copied to hr.applicant + +hr_recruitment / hr.candidate / user_id (many2one) : DEL relation: res.users +hr_recruitment / hr.candidate / website_message_ids (one2many): DEL relation: mail.message + +# NOTHING TO DO + +hr_recruitment / hr.employee / applicant_ids (one2many) : NEW relation: hr.applicant + +# NOTHING TO DO: hr.applicant#employee_id is filled from hr.candidate + +hr_recruitment / hr.employee / candidate_id (one2many) : DEL relation: hr.candidate +hr_recruitment / hr.job / activity_ids (one2many) : NEW relation: mail.activity +hr_recruitment / hr.job / allowed_user_ids (many2many) : module is now 'hr' ('hr_recruitment') +hr_recruitment / hr.job / date_from (date) : DEL +hr_recruitment / hr.job / date_to (date) : DEL +hr_recruitment / hr.job / expected_degree (many2one) : NEW relation: hr.recruitment.degree +hr_recruitment / hr.job / job_source_ids (one2many) : NEW relation: hr.recruitment.source +hr_recruitment / hr.job / user_id (many2one) : module is now 'hr' ('hr_recruitment') +hr_recruitment / hr.recruitment.degree / score (float) : NEW required, hasdefault: default +hr_recruitment / hr.recruitment.source / campaign_id (many2one) : NEW relation: utm.campaign +hr_recruitment / hr.recruitment.stage / legend_waiting (char) : NEW required, hasdefault: default, translate +hr_recruitment / hr.recruitment.stage / rotting_threshold_days (integer): NEW hasdefault: default +hr_recruitment / hr.talent.pool / active (boolean) : NEW hasdefault: default +hr_recruitment / hr.talent.pool / categ_ids (many2many) : NEW relation: hr.applicant.category +hr_recruitment / hr.talent.pool / color (integer) : NEW hasdefault: default +hr_recruitment / hr.talent.pool / company_id (many2one) : NEW relation: res.company, hasdefault: default +hr_recruitment / hr.talent.pool / description (html) : NEW +hr_recruitment / hr.talent.pool / message_follower_ids (one2many): NEW relation: mail.followers +hr_recruitment / hr.talent.pool / message_ids (one2many) : NEW relation: mail.message +hr_recruitment / hr.talent.pool / name (char) : NEW required, translate +hr_recruitment / hr.talent.pool / pool_manager (many2one) : NEW relation: res.users, hasdefault: default +hr_recruitment / hr.talent.pool / rating_ids (one2many) : NEW relation: rating.rating +hr_recruitment / hr.talent.pool / talent_ids (many2many) : NEW relation: hr.applicant +hr_recruitment / hr.talent.pool / website_message_ids (one2many): NEW relation: mail.message + +# NOTHING TO DO + +hr_recruitment / res.company / candidate_properties_definition (properties_definition): DEL + +# DONE: merged into hr.job#applicant_properties_definition + +hr_recruitment / res.partner / applicant_ids (one2many) : NEW relation: hr.applicant + +# NOTHING TO DO + +---XML records in module 'hr_recruitment'--- +NEW ir.actions.act_window: hr_recruitment.action_hr_talent_pool +NEW ir.actions.act_window: hr_recruitment.action_hr_talent_pool_applications +NEW ir.actions.act_window: hr_recruitment.mail_followers_edit_action_from_hr_recruitment +DEL ir.actions.act_window: hr_recruitment.action_hr_candidate +DEL ir.actions.server: hr_recruitment.action_candidate_send_mail +DEL ir.actions.server: hr_recruitment.ir_actions_server_refuse_applicant +NEW ir.model.access: hr_recruitment.access_hr_talent_pool +NEW ir.model.access: hr_recruitment.access_hr_talent_pool_interviewer +NEW ir.model.access: hr_recruitment.access_job_add_applicants +NEW ir.model.access: hr_recruitment.access_job_add_applicants_interviewer +NEW ir.model.access: hr_recruitment.access_talent_pool_add_applicants +NEW ir.model.access: hr_recruitment.access_talent_pool_add_applicants_interviewer +DEL ir.model.access: hr_recruitment.access_candidate_send_mail +DEL ir.model.access: hr_recruitment.access_candidate_send_mail_interviewer +DEL ir.model.access: hr_recruitment.access_hr_candidate_interviewer +DEL ir.model.access: hr_recruitment.access_hr_candidate_user +NEW ir.model.constraint: hr_recruitment.constraint_hr_applicant_job_id_stage_id_idx +NEW ir.model.constraint: hr_recruitment.constraint_hr_recruitment_degree_score_range +NEW ir.rule: hr_recruitment.hr_talent_pool_user_rule (noupdate) + +# NOTHING TO DO + +DEL ir.rule: hr_recruitment.hr_candidate_comp_rule (noupdate) +DEL ir.rule: hr_recruitment.hr_candidate_interviewer_rule (noupdate) +DEL ir.rule: hr_recruitment.hr_candidate_user_rule (noupdate) + +# DONE: deleted in post-migration + +NEW ir.ui.menu: hr_recruitment.menu_hr_talent_pools +DEL ir.ui.menu: hr_recruitment.menu_hr_candidate +NEW ir.ui.view: hr_recruitment.applicant_hired_template (noupdate) +NEW ir.ui.view: hr_recruitment.hr_kanban_view_applicant_talent_pool +NEW ir.ui.view: hr_recruitment.hr_talent_pool_view_form +NEW ir.ui.view: hr_recruitment.hr_talent_pool_view_kanban +NEW ir.ui.view: hr_recruitment.hr_talent_pool_view_list +NEW ir.ui.view: hr_recruitment.job_add_applicants_view_form +NEW ir.ui.view: hr_recruitment.talent_pool_add_applicants_view_form +DEL ir.ui.view: hr_recruitment.candidate_hired_template (noupdate) +DEL ir.ui.view: hr_recruitment.candidate_send_mail_view_form +DEL ir.ui.view: hr_recruitment.hr_candidate_view_calendar +DEL ir.ui.view: hr_recruitment.hr_candidate_view_form +DEL ir.ui.view: hr_recruitment.hr_candidate_view_kanban +DEL ir.ui.view: hr_recruitment.hr_candidate_view_search +DEL ir.ui.view: hr_recruitment.hr_candidate_view_tree +NEW mail.message.subtype: hr_recruitment.mt_talent_new (noupdate) +NEW res.groups.privilege: hr_recruitment.res_groups_privilege_recruitment (noupdate) + +# NOTHING TO DO diff --git a/openupgrade_scripts/scripts/hr_recruitment/tests/data_hr_recruitment_migration.py b/openupgrade_scripts/scripts/hr_recruitment/tests/data_hr_recruitment_migration.py new file mode 100644 index 000000000000..544f8af16910 --- /dev/null +++ b/openupgrade_scripts/scripts/hr_recruitment/tests/data_hr_recruitment_migration.py @@ -0,0 +1,61 @@ +env = locals().get("env") + +company = env.company +company.candidate_properties_definition = [ + {"name": "ou18field", "string": "Ou18field", "type": "char"}, + {"name": "ou18field_unused", "string": "Unused Ou18field", "type": "char"}, +] +job_with_company = env["hr.job"].create( + { + "name": "ou18-job-with-company", + "company_id": company.id, + "applicant_properties_definition": [ + { + "name": "ou18field_from_job", + "string": "Ou18field from job", + "type": "char", + }, + ], + } +) +job_without_company = env["hr.job"].create( + { + "name": "ou18-job-without-company", + "company_id": False, + } +) +applicant_job_with_company = env["hr.applicant"].create( + { + "candidate_id": env["hr.candidate"] + .create( + { + "partner_name": "ou18-applicant-job-with-company", + "candidate_properties": { + "ou18field": "from ou18 for ou18-applicant-job-with-company", + }, + } + ) + .id, + "job_id": job_with_company.id, + "applicant_properties": { + "ou18field_from_job": "from ou18 job", + }, + } +) +applicant_job_without_company = env["hr.applicant"].create( + { + "candidate_id": env["hr.candidate"] + .create( + { + "partner_name": "ou18-applicant-job-without-company", + "candidate_properties": { + "ou18field": "from ou18 for ou18-applicant-job-without-company", + }, + } + ) + .id, + "job_id": job_without_company.id, + } +) + +env.cr.commit() diff --git a/openupgrade_scripts/scripts/hr_recruitment/tests/test_hr_recruitment_migration.py b/openupgrade_scripts/scripts/hr_recruitment/tests/test_hr_recruitment_migration.py new file mode 100644 index 000000000000..1626a9ce4050 --- /dev/null +++ b/openupgrade_scripts/scripts/hr_recruitment/tests/test_hr_recruitment_migration.py @@ -0,0 +1,64 @@ +from odoo.tests import TransactionCase + +from odoo.addons.openupgrade_framework import openupgrade_test + + +@openupgrade_test +class TestHrRecruitmentMigration(TransactionCase): + def test_properties(self): + """ + Test that candidate properties have been moved to jobs + """ + applicant_job_with_company = self.env["hr.applicant"].search( + [ + ("partner_name", "=", "ou18-applicant-job-with-company"), + ] + ) + applicant_job_without_company = self.env["hr.applicant"].search( + [ + ("partner_name", "=", "ou18-applicant-job-without-company"), + ] + ) + + self.assertItemsEqual( + applicant_job_with_company.job_id.applicant_properties_definition, + [ + {"name": "ou18field", "type": "char", "string": "Ou18field"}, + { + "name": "ou18field_unused", + "type": "char", + "string": "Unused Ou18field", + }, + { + "name": "ou18field_from_job", + "string": "Ou18field from job", + "type": "char", + }, + ], + ) + self.assertEqual( + dict(applicant_job_with_company.applicant_properties), + { + "ou18field": "from ou18 for ou18-applicant-job-with-company", + "ou18field_from_job": "from ou18 job", + "ou18field_unused": False, + }, + ) + self.assertItemsEqual( + applicant_job_without_company.job_id.applicant_properties_definition, + [ + {"name": "ou18field", "type": "char", "string": "Ou18field"}, + { + "name": "ou18field_unused", + "type": "char", + "string": "Unused Ou18field", + }, + ], + ) + self.assertEqual( + dict(applicant_job_without_company.applicant_properties), + { + "ou18field": "from ou18 for ou18-applicant-job-without-company", + "ou18field_unused": False, + }, + )