diff --git a/docsource/modules160-170.rst b/docsource/modules160-170.rst index 96b39eee33ea..e5638ca03f4c 100644 --- a/docsource/modules160-170.rst +++ b/docsource/modules160-170.rst @@ -838,7 +838,7 @@ Module coverage 16.0 -> 17.0 +---------------------------------------------------+----------------------+-------------------------------------------------+ | product_matrix | Nothing to do | | +---------------------------------------------------+----------------------+-------------------------------------------------+ -| project | | | +| project | Done | | +---------------------------------------------------+----------------------+-------------------------------------------------+ | |new| project_account | | | +---------------------------------------------------+----------------------+-------------------------------------------------+ diff --git a/openupgrade_scripts/scripts/project/17.0.1.3/post-migration.py b/openupgrade_scripts/scripts/project/17.0.1.3/post-migration.py new file mode 100644 index 000000000000..c24f48da6631 --- /dev/null +++ b/openupgrade_scripts/scripts/project/17.0.1.3/post-migration.py @@ -0,0 +1,196 @@ +# Copyright 2024 Viindoo Technology Joint Stock Company (Viindoo) +# Copyright 2025 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from dateutil.rrule import FR, MO, MONTHLY, SA, SU, TH, TU, WE, YEARLY, rrule +from openupgradelib import openupgrade + +_deleted_xml_records = [ + "project.ir_cron_recurring_tasks", + "project.mt_project_task_blocked", + "project.mt_project_task_dependency_change", + "project.mt_project_task_ready", + "project.mt_task_blocked", + "project.mt_task_dependency_change", + "project.mt_task_progress", + "project.mt_task_ready", +] + + +def _fill_project_task_display_in_project(env): + """Set it to False when there's a parent but not display project.""" + openupgrade.logged_query( + env.cr, + """ + UPDATE project_task + SET display_in_project = False + WHERE parent_id IS NOT NULL AND display_project_id IS NULL; + """, + ) + + +def _convert_project_task_repeat_type_after(env): + """Convert the disappeared "repeat N times" strategy to "repeat until".""" + DAYS_MAPPING = { + "mon": MO, + "tue": TU, + "wed": WE, + "thu": TH, + "fri": FR, + "sat": SA, + "sun": SU, + } + WEEKS_MAPPING = {"first": 1, "second": 2, "third": 3, "last": 4} + MONTH_MAPPING = { + "january": 1, + "february": 2, + "march": 3, + "april": 4, + "may": 5, + "june": 6, + "july": 7, + "august": 8, + "september": 9, + "october": 10, + "november": 11, + "december": 12, + } + # First, expire all the already consumed recurrences + openupgrade.logged_query( + env.cr, + """ + UPDATE project_task_recurrence + SET repeat_type = 'until', + repeat_until = (CURRENT_DATE - interval '1 days')::date + WHERE repeat_type = 'after' + AND recurrence_left <= 0 + """, + ) + # For day interval, we can do it massively by SQL, as there's no variability + openupgrade.logged_query( + env.cr, + """ + WITH sub AS ( + SELECT recurrence_id, MAX(date_deadline) AS date_deadline + FROM project_task + WHERE recurrence_id IS NOT NULL + GROUP by recurrence_id + ) + UPDATE project_task_recurrence ptr + SET repeat_type = 'until', + repeat_until = ( + sub.date_deadline + ( + interval '1 days' * ptr.repeat_interval * ptr.recurrence_left + ) + )::date + FROM sub + WHERE ptr.id = sub.recurrence_id + AND ptr.repeat_type = 'after' + AND ptr.repeat_unit = 'day' + AND ptr.recurrence_left > 0 + """, + ) + # WEEK + # - Obtain the number of days per week of each ocurrence + # - Get the remaining weeks from the number of days per week + # - Always put the end date on the last day of the week (Sunday) + env.cr.execute( + """ + SELECT + ptr.id, ptr.recurrence_left, ptr.repeat_interval, + ptr.mon, ptr.tue, ptr.wed, ptr.thu, ptr.fri, ptr.sat, ptr.sun, + MAX(pt.date_deadline) AS date_deadline + FROM project_task pt JOIN project_task_recurrence ptr + ON ptr.id = pt.recurrence_id + WHERE ptr.repeat_type = 'after' + AND ptr.repeat_unit = 'week' + AND date_deadline IS NOT NULL + GROUP BY pt.id, ptr.id + """ + ) + for row in env.cr.dictfetchall(): + ptr = env["project.task.recurrence"].browse(row["id"]) + days_per_week = sum(1 if row[x] else 0 for x in DAYS_MAPPING) + weeks = int(row["recurrence_left"] % days_per_week) + 1 + repeat_until = row["date_deadline"] + timedelta( + weeks=weeks * row["repeat_interval"] + ) + repeat_until += timedelta(days=6 - repeat_until.weekday()) # Put Sunday + ptr.write({"repeat_type": "until", "repeat_until": repeat_until}) + # MONTH AND YEAR + # - Build rrule inspired on v16 code, but in an optimized way + # - Get end date through rrule execution + env.cr.execute( + """ + SELECT + ptr.id, ptr.recurrence_left, ptr.repeat_interval, ptr.repeat_unit, + ptr.repeat_on_month, ptr.repeat_on_year, ptr.repeat_day, ptr.repeat_month, + ptr.repeat_week, ptr.repeat_weekday, MAX(pt.date_deadline) AS date_deadline + FROM project_task pt JOIN project_task_recurrence ptr + ON ptr.id = pt.recurrence_id + WHERE ptr.repeat_type = 'after' + AND ptr.repeat_unit IN ('month', 'year') + AND date_deadline IS NOT NULL + GROUP BY pt.id, ptr.id + """ + ) + for row in env.cr.dictfetchall(): + ptr = env["project.task.recurrence"].browse(row["id"]) + rrule_kwargs = { + "interval": row["recurrence_left"] * row["repeat_interval"], + "dtstart": row["date_deadline"], + "freq": MONTHLY if row["repeat_unit"] == "month" else YEARLY, + } + if (row["repeat_on_month"] == "day" and row["repeat_unit"] == "month") or ( + row["repeat_on_year"] == "day" and row["repeat_unit"] == "year" + ): + rrule_kwargs["byweekday"] = [ + DAYS_MAPPING[row["repeat_weekday"]](WEEKS_MAPPING[row["repeat_week"]]) + ] + if row["repeat_unit"] == "year" and row["repeat_on_year"] == "date": + rrule_kwargs["bymonth"] = MONTH_MAPPING[row["repeat_month"]] + repeat_until = list(rrule(**rrule_kwargs))[1] + if row["repeat_unit"] == "month" and row["repeat_on_month"] == "date": + repeat_until = repeat_until.replace(day=int(row["repeat_day"])) + if row["repeat_unit"] == "year" and row["repeat_on_year"] == "date": + repeat_until = repeat_until.replace( + day=int(row["repeat_day"]), month=MONTH_MAPPING[row["repeat_month"]] + ) + ptr.write({"repeat_type": "until", "repeat_until": repeat_until}) + + +def _fill_project_update_task_count(env): + """Use an heuristics to get the fields task_count and closed_task_count for + historical project updates: + + - Task count: the number of tasks which creation date is below project update + creation date. + - Closed task count: the number of tasks which end date is below project update + creation date. + """ + for update in env["project.update"].search([]): + base_domain = [("display_project_id", "=", update.project_id.id)] + update.task_count = env["project.task"].search_count( + base_domain + [("create_date", "<=", update.create_date)] + ) + update.closed_task_count = env["project.task"].search_count( + base_domain + [("date_end", "<=", update.create_date)] + ) + + +@openupgrade.migrate() +def migrate(env, version): + _fill_project_task_display_in_project(env) + _convert_project_task_repeat_type_after(env) + openupgrade.load_data(env, "project", "17.0.1.3/noupdate_changes.xml") + openupgrade.delete_record_translations( + env.cr, + "project", + ("project_message_user_assigned", "rating_project_request_email_template"), + ) + openupgrade.delete_records_safely_by_xml_id( + env, + _deleted_xml_records, + ) diff --git a/openupgrade_scripts/scripts/project/17.0.1.3/pre-migration.py b/openupgrade_scripts/scripts/project/17.0.1.3/pre-migration.py new file mode 100644 index 000000000000..e4d84cf2b132 --- /dev/null +++ b/openupgrade_scripts/scripts/project/17.0.1.3/pre-migration.py @@ -0,0 +1,63 @@ +# Copyright 2024 Viindoo Technology Joint Stock Company (Viindoo) +# Copyright 2025 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openupgradelib import openupgrade + + +def _rename_fields(env): + openupgrade.rename_fields( + env, + [ + ("project.task", "project_task", "planned_hours", "allocated_hours"), + ( + "project.task.type", + "project_task_type", + "auto_validation_kanban_state", + "auto_validation_state", + ), + ], + ) + + +def _convert_project_task_state(env): + """Handle kanban_state, is_closed, etc to state conversion.""" + openupgrade.add_fields( + env, [("state", "project.task", "project_task", "selection", False, "project")] + ) + openupgrade.logged_query( + env.cr, + """ + UPDATE project_task + SET state = CASE + WHEN kanban_state = 'done' THEN CASE + WHEN is_closed THEN '1_done' + ELSE '03_approved' + END + WHEN kanban_state = 'blocked' THEN CASE + WHEN is_closed THEN '1_canceled' + ELSE '02_changes_requested' + END + ELSE '01_in_progress' + END + WHERE kanban_state IN ('done', 'blocked', 'normal'); + """, + ) + # Secnd pass for setting '04_waiting_normal' on tasks with "Blocked By" entries + # that qualify + openupgrade.logged_query( + env.cr, + """ + UPDATE project_task pt + SET state = '04_waiting_normal' + FROM task_dependencies_rel rel + JOIN project_task dep ON dep.id = rel.depends_on_id + WHERE rel.task_id = pt.id AND dep.state NOT IN ('1_done', '1_canceled') + """, + ) + + +@openupgrade.migrate() +def migrate(env, version): + _rename_fields(env) + _convert_project_task_state(env) diff --git a/openupgrade_scripts/scripts/project/17.0.1.3/upgrade_analysis_work.txt b/openupgrade_scripts/scripts/project/17.0.1.3/upgrade_analysis_work.txt new file mode 100644 index 000000000000..5099c391dbcf --- /dev/null +++ b/openupgrade_scripts/scripts/project/17.0.1.3/upgrade_analysis_work.txt @@ -0,0 +1,234 @@ +---Models in module 'project'--- +new model project.project.stage.delete.wizard [transient] +# NOTHING TO DO + +---Fields in module 'project'--- +project / project.milestone / message_main_attachment_id (many2one): DEL relation: ir.attachment +project / project.milestone / rating_ids (one2many) : NEW relation: rating.rating +project / project.project / activity_user_id (many2one) : not related anymore +project / project.project / activity_user_id (many2one) : now a function +# NOTHING TO DO + +project / project.project / alias_enabled (boolean) : DEL +project / project.project / allow_recurring_tasks (boolean): DEL +project / project.project / allow_subtasks (boolean) : DEL +# NOTHING TO DO: + +project / project.project / company_id (many2one) : now a function +# NOTHING TO DO: handled by ORM + +project / project.project / currency_id (many2one) : not related anymore +project / project.project / currency_id (many2one) : now a function +# NOTHING TO DO: compute non-stored + +project / project.project / last_update_status (selection): selection_keys is now '['at_risk', 'done', 'off_track', 'on_hold', 'on_track', 'to_define']' ('['at_risk', 'off_track', 'on_hold', 'on_track', 'to_define']') +# NOTHING TO DO: new key: 'done' + +project / project.project / message_main_attachment_id (many2one): DEL relation: ir.attachment +# NOTHING TO DO + +project / project.project / partner_email (char) : DEL +project / project.project / partner_phone (char) : DEL +# NOTHING TO DO + +project / project.project / resource_calendar_id (many2one): not related anymore +project / project.project / resource_calendar_id (many2one): now a function +# NOTHING TO DO: compute non-stored + +project / project.project.stage / company_id (many2one) : NEW relation: res.company +project / project.tags / _order : _order is now 'name' ('id') +project / project.task / _order : _order is now 'priority desc, sequence, date_deadline asc, id desc' ('priority desc, sequence, id desc') +# NOTHING TO DO + +project / project.task / activity_user_id (many2one) : not related anymore +project / project.task / activity_user_id (many2one) : now a function +# NOTHING TO DO + +project / project.task / allocated_hours (float) : NEW +project / project.task / planned_hours (float) : DEL +# DONE: rename planned_hours -> allocated_hours + +project / project.task / ancestor_id (many2one) : DEL relation: project.task +project / project.task / date_deadline (date) : type is now 'datetime' ('date') +# NOTHING TO DO + +project / project.task / display_in_project (boolean) : NEW +project / project.task / display_project_id (many2one) : DEL relation: project.project +# DONE: post-migration: set display_in_project to False when there's a parent but no display project + +project / project.task / email_from (char) : DEL +project / project.task / fri (boolean) : DEL +project / project.task / is_analytic_account_id_changed (boolean): DEL +project / project.task / is_blocked (boolean) : DEL +project / project.task / message_main_attachment_id (many2one): DEL relation: ir.attachment +project / project.task / mon (boolean) : DEL +project / project.task / partner_email (char) : DEL +project / project.task / partner_phone (char) : DEL +# NOTHING TO DO + +project / project.task / recurrence_update (selection) : DEL selection_keys: ['all', 'subsequent', 'this'] +project / project.task / repeat_day (selection) : DEL selection_keys: ['1', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '2', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '3', '30', '31', '4', '5', '6', '7', '8', '9'] +project / project.task / repeat_month (selection) : DEL selection_keys: ['april', 'august', 'december', 'february', 'january', 'july', 'june', 'march', 'may', 'november', 'october', 'september'] +project / project.task / repeat_number (integer) : DEL +project / project.task / repeat_on_month (selection) : DEL selection_keys: ['date', 'day'] +project / project.task / repeat_on_year (selection) : DEL selection_keys: ['date', 'day'] +project / project.task / repeat_type (selection) : selection_keys is now '['forever', 'until']' ('['after', 'forever', 'until']') +project / project.task / repeat_week (selection) : DEL selection_keys: ['first', 'last', 'second', 'third'] +project / project.task / repeat_weekday (selection) : DEL selection_keys: ['fri', 'mon', 'sat', 'sun', 'thu', 'tue', 'wed'] +project / project.task / sat (boolean) : DEL +# NOTHING TO DO: deleted fields, the recurrence task option has been simplified in odoo 17 +# There is no longer the ability to select days of the week, there is no longer the ability to select days of the month + +project / project.task / is_closed (boolean) : DEL +project / project.task / kanban_state (selection) : DEL required, selection_keys: ['blocked', 'done', 'normal'] +project / project.task / state (selection) : NEW required, selection_keys: ['01_in_progress', '02_changes_requested', '03_approved', '04_waiting_normal', '1_canceled', '1_done'], isfunction: function, stored +# DONE: pre-migration: add the field and convert data from kanban_state, is_closed to state. Done in pre for avoiding the ORM computation. + +project / project.task / sun (boolean) : DEL +project / project.task / thu (boolean) : DEL +project / project.task / tue (boolean) : DEL +project / project.task / wed (boolean) : DEL +# NOTHING TO DO + +project / project.task.recurrence / fri (boolean) : DEL +project / project.task.recurrence / mon (boolean) : DEL +project / project.task.recurrence / next_recurrence_date (date) : DEL +project / project.task.recurrence / recurrence_left (integer) : DEL +project / project.task.recurrence / repeat_day (selection) : DEL selection_keys: ['1', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '2', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '3', '30', '31', '4', '5', '6', '7', '8', '9'] +project / project.task.recurrence / repeat_month (selection) : DEL selection_keys: ['april', 'august', 'december', 'february', 'january', 'july', 'june', 'march', 'may', 'november', 'october', 'september'] +project / project.task.recurrence / repeat_on_month (selection) : DEL selection_keys: ['date', 'day'] +project / project.task.recurrence / repeat_on_year (selection) : DEL selection_keys: ['date', 'day'] +project / project.task.recurrence / repeat_week (selection) : DEL selection_keys: ['first', 'last', 'second', 'third'] +project / project.task.recurrence / repeat_weekday (selection) : DEL selection_keys: ['fri', 'mon', 'sat', 'sun', 'thu', 'tue', 'wed'] +project / project.task.recurrence / sat (boolean) : DEL +project / project.task.recurrence / sun (boolean) : DEL +project / project.task.recurrence / thu (boolean) : DEL +project / project.task.recurrence / tue (boolean) : DEL +project / project.task.recurrence / wed (boolean) : DEL +# NOTHING TO DO: The recurrence task option has been simplified in odoo 17: +# There is no longer the ability to select days of the week, there is no longer the ability to select days of the month. +# If we want to restore the old behavior, an OCA module should be done + +project / project.task.recurrence / repeat_type (selection) : selection_keys is now '['forever', 'until']' ('['after', 'forever', 'until']') +project / project.task.recurrence / repeat_number (integer) : DEL +# DONE: post-migration: replace setting 'after' by 'until' in combination with "repeat_until" extracting the information from date_deadline, repeat_number, repeat_interval, repeat_unit and the rest of the involved fields + +project / project.task.type / auto_validation_kanban_state (boolean): DEL +project / project.task.type / auto_validation_state (boolean): NEW hasdefault: default +# DONE pre-migration: rename field + +project / project.task.type / legend_blocked (char) : DEL required +project / project.task.type / legend_done (char) : DEL required +project / project.task.type / legend_normal (char) : DEL required +project / project.task.type / user_id (many2one) : now a function +# NOTHING TO DO + +project / project.update / _order : _order is now 'id desc' ('date desc') +project / project.update / activity_user_id (many2one) : not related anymore +project / project.update / activity_user_id (many2one) : now a function +# NOTHING TO DO + +project / project.update / closed_task_count (integer) : NEW +project / project.update / task_count (integer) : NEW +# DONE: post-migration: Infer under a heuristics these fields for historical data + +project / project.update / message_main_attachment_id (many2one): DEL relation: ir.attachment +project / project.update / rating_ids (one2many) : NEW relation: rating.rating +project / project.update / status (selection) : selection_keys is now '['at_risk', 'done', 'off_track', 'on_hold', 'on_track']' ('['at_risk', 'off_track', 'on_hold', 'on_track']') +project / res.company / analytic_plan_id (many2one) : DEL relation: account.analytic.plan +project / res.partner / project_ids (one2many) : NEW relation: project.project +# NOTHING TO DO + +---XML records in module 'project'--- +NEW ir.actions.act_window: project.action_view_my_task +NEW ir.actions.act_window: project.mail_activity_plan_action_config_project_task_plan +NEW ir.actions.act_window: project.mail_activity_plan_action_config_task_plan +NEW ir.actions.act_window.view: project.mail_activity_plan_action_project_task_view_form +NEW ir.actions.act_window.view: project.mail_activity_plan_action_project_task_view_tree +NEW ir.actions.act_window.view: project.open_view_my_task_list_calendar +NEW ir.actions.act_window.view: project.open_view_my_task_list_kanban +NEW ir.actions.act_window.view: project.open_view_my_task_list_tree +NEW ir.actions.act_window.view: project.open_view_project_all_group_stage_kanban_view +NEW ir.actions.act_window.view: project.open_view_project_all_group_stage_tree_view +NEW ir.actions.server: project.action_server_convert_to_subtask +NEW ir.actions.server: project.action_server_view_my_task +NEW ir.actions.server: project.unlink_project_stage_action +# NOTHING TO DO + +DEL ir.cron: project.ir_cron_recurring_tasks (noupdate) +# DONE: delete in post-migration + +NEW ir.model.access: project.access_mail_activity_plan_project_manager +NEW ir.model.access: project.access_mail_activity_plan_template_project_manager +NEW ir.model.access: project.access_project_project_stage_delete_wizard +NEW ir.model.constraint: project.constraint_project_task_private_task_has_no_parent +NEW ir.model.constraint: project.constraint_project_task_recurring_task_has_no_parent +NEW ir.rule: project.mail_plan_rule_group_project_manager_task (noupdate) +NEW ir.rule: project.mail_plan_templates_rule_group_project_manager_task (noupdate) +NEW ir.rule: project.project_project_stage_rule (noupdate) +# NOTHING TO DO: new feature + +NEW ir.ui.menu: project.mail_activity_plan_menu_config_project +NEW ir.ui.menu: project.menu_project_management_all_tasks +NEW ir.ui.menu: project.menu_project_management_my_tasks +DEL ir.ui.menu: project.menu_tasks_config +# NOTHING TO DO: new feature + +NEW ir.ui.view: project.mail_activity_plan_view_form_project_and_task +NEW ir.ui.view: project.open_view_all_tasks_list_view +NEW ir.ui.view: project.open_view_my_tasks_list_view +NEW ir.ui.view: project.portal_my_task_allocated_hours_template +NEW ir.ui.view: project.project_kanban_view_group_stage +NEW ir.ui.view: project.project_list_view_group_stage +NEW ir.ui.view: project.project_project_view_activity +NEW ir.ui.view: project.project_share_wizard_confirm_form +NEW ir.ui.view: project.project_task_convert_to_subtask_view_form +NEW ir.ui.view: project.project_task_view_tree_base +NEW ir.ui.view: project.project_task_view_tree_main_base +NEW ir.ui.view: project.quick_create_project_form +NEW ir.ui.view: project.quick_create_task_form_inherit_view_default_project +NEW ir.ui.view: project.task_link_preview_front_end +NEW ir.ui.view: project.task_link_preview_portal_layout +NEW ir.ui.view: project.view_project_config +NEW ir.ui.view: project.view_project_config_group_stage +NEW ir.ui.view: project.view_project_config_kanban_group_stage +NEW ir.ui.view: project.view_project_project_stage_delete_wizard +NEW ir.ui.view: project.view_project_project_stage_unarchive_wizard +NEW ir.ui.view: project.view_task_kanban_inherit_all_task +NEW ir.ui.view: project.view_task_kanban_inherit_view_default_project +NEW ir.ui.view: project.view_task_search_form_base +NEW ir.ui.view: project.view_task_search_form_project_base +NEW ir.ui.view: project.view_task_search_form_project_fsm_base +# NOTHING TO DO + +DEL ir.ui.view: project.portal_my_task_planned_hours_template +DEL ir.ui.view: project.project_task_view_tree_activity +DEL ir.ui.view: project.report_project_task_user_view_tree +DEL ir.ui.view: project.view_task_search_form_extended +# NOTHING TO DO + +NEW mail.message.subtype: project.mt_project_task_approved (noupdate) +NEW mail.message.subtype: project.mt_project_task_canceled (noupdate) +NEW mail.message.subtype: project.mt_project_task_changes_requested (noupdate) +NEW mail.message.subtype: project.mt_project_task_done (noupdate) +NEW mail.message.subtype: project.mt_project_task_in_progress (noupdate) +NEW mail.message.subtype: project.mt_project_task_waiting (noupdate) +NEW mail.message.subtype: project.mt_task_approved (noupdate) +NEW mail.message.subtype: project.mt_task_canceled (noupdate) +NEW mail.message.subtype: project.mt_task_changes_requested (noupdate) +NEW mail.message.subtype: project.mt_task_done (noupdate) +NEW mail.message.subtype: project.mt_task_in_progress (noupdate) +NEW mail.message.subtype: project.mt_task_waiting (noupdate) +# NOTHING TO DO + +DEL mail.message.subtype: project.mt_project_task_blocked (noupdate) +DEL mail.message.subtype: project.mt_project_task_dependency_change (noupdate) +DEL mail.message.subtype: project.mt_project_task_ready (noupdate) +DEL mail.message.subtype: project.mt_task_blocked (noupdate) +DEL mail.message.subtype: project.mt_task_dependency_change (noupdate) +DEL mail.message.subtype: project.mt_task_progress (noupdate) +DEL mail.message.subtype: project.mt_task_ready (noupdate) +# DONE: safely delete in post-migration + +DEL res.groups: project.group_subtask_project +# NOTHING TO DO