From 45126f5f95674f67783c75799a0f3f2223181f3e Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Wed, 23 Sep 2026 18:47:21 -0400 Subject: [PATCH] [20.0][OU-ADD] fleet: migration scripts 20.0 gives a service a span instead of a point: date is dropped, date_from and date_to added. date_from carries 19.0's date help text unchanged -- "Date when the cost has been executed" -- and the same default of today. That default is what makes the loss invisible. The column is created full, not empty, so nothing looks wrong: all six of the seed's service logs came out on the day the upgrade ran, having held six different dates. A vehicle's whole service history moves to today. After the rename: 6 distinct dates, none of them today. Assisted-by: Claude Opus 5 --- docsource/modules190-200.rst | 2 +- .../fleet/20.0.0.1/noupdate_changes.xml | 6 + .../scripts/fleet/20.0.0.1/post-migration.py | 9 ++ .../scripts/fleet/20.0.0.1/pre-migration.py | 52 +++++++++ .../fleet/20.0.0.1/upgrade_analysis.txt | 105 ++++++++++++++++++ .../fleet/tests/test_fleet_migration.py | 46 ++++++++ 6 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 openupgrade_scripts/scripts/fleet/20.0.0.1/noupdate_changes.xml create mode 100644 openupgrade_scripts/scripts/fleet/20.0.0.1/post-migration.py create mode 100644 openupgrade_scripts/scripts/fleet/20.0.0.1/pre-migration.py create mode 100644 openupgrade_scripts/scripts/fleet/20.0.0.1/upgrade_analysis.txt create mode 100644 openupgrade_scripts/scripts/fleet/tests/test_fleet_migration.py diff --git a/docsource/modules190-200.rst b/docsource/modules190-200.rst index 8da46d62bbc..4c806041e2b 100644 --- a/docsource/modules190-200.rst +++ b/docsource/modules190-200.rst @@ -156,7 +156,7 @@ Module coverage 19.0 -> 20.0 +---------------------------------------------------+----------------------+-------------------------------------------------+ | event_sms | | | +---------------------------------------------------+----------------------+-------------------------------------------------+ -| fleet | | | +| fleet |Done | | +---------------------------------------------------+----------------------+-------------------------------------------------+ | |new| fleet_maintenance | | | +---------------------------------------------------+----------------------+-------------------------------------------------+ diff --git a/openupgrade_scripts/scripts/fleet/20.0.0.1/noupdate_changes.xml b/openupgrade_scripts/scripts/fleet/20.0.0.1/noupdate_changes.xml new file mode 100644 index 00000000000..8d26494e5f1 --- /dev/null +++ b/openupgrade_scripts/scripts/fleet/20.0.0.1/noupdate_changes.xml @@ -0,0 +1,6 @@ + + + + directions_car + + diff --git a/openupgrade_scripts/scripts/fleet/20.0.0.1/post-migration.py b/openupgrade_scripts/scripts/fleet/20.0.0.1/post-migration.py new file mode 100644 index 00000000000..b8674ef954c --- /dev/null +++ b/openupgrade_scripts/scripts/fleet/20.0.0.1/post-migration.py @@ -0,0 +1,9 @@ +# Copyright 2026 Don Kendall +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.load_data(env, "fleet", "20.0.0.1/noupdate_changes.xml") diff --git a/openupgrade_scripts/scripts/fleet/20.0.0.1/pre-migration.py b/openupgrade_scripts/scripts/fleet/20.0.0.1/pre-migration.py new file mode 100644 index 00000000000..8c5c78355b0 --- /dev/null +++ b/openupgrade_scripts/scripts/fleet/20.0.0.1/pre-migration.py @@ -0,0 +1,52 @@ +# Copyright 2026 Don Kendall +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def _rename_the_service_date(env): + """A service log's `date` becomes `date_from`, and nothing says so. + + 20.0 gives a service record a span instead of a point: `date` is dropped, + `date_from` and `date_to` are added, and a CHECK enforces the order:: + + CHECK((date_to IS NULL) OR (date_from IS NOT NULL AND date_to >= date_from)) + + The analysis reports a DEL and two NEWs, which reads as three unrelated + changes. The field definitions say otherwise -- 20.0's `date_from` carries + 19.0's `date` help text **unchanged**, "Date when the cost has been + executed", and the same `default=fields.Date.context_today`: + + 19.0 date = fields.Date(help='Date when the cost has been executed', ...) + 20.0 date_from = fields.Date(string='Start of Service', + help='Date when the cost has been executed', ...) + + and 20.0's own `_get_odometer`-adjacent code passes `date_from` wherever it + needs the service's date. + + Because `date_from` carries that default, nothing about the upgrade looks + wrong without this: the column is created, every existing row is filled with + **the day the upgrade ran**, and every service in the vehicle's history + silently moves to today. The seed's six service logs hold six different + dates; all six come out identical. + + Renaming in pre-migration carries the values and leaves `date_to` null, + which the CHECK allows. + """ + if openupgrade.column_exists(env.cr, "fleet_vehicle_log_services", "date"): + openupgrade.rename_fields( + env, + [ + ( + "fleet.vehicle.log.services", + "fleet_vehicle_log_services", + "date", + "date_from", + ) + ], + ) + + +@openupgrade.migrate() +def migrate(env, version): + _rename_the_service_date(env) diff --git a/openupgrade_scripts/scripts/fleet/20.0.0.1/upgrade_analysis.txt b/openupgrade_scripts/scripts/fleet/20.0.0.1/upgrade_analysis.txt new file mode 100644 index 00000000000..60a0ea660e7 --- /dev/null +++ b/openupgrade_scripts/scripts/fleet/20.0.0.1/upgrade_analysis.txt @@ -0,0 +1,105 @@ +---Models in module 'fleet'--- +---Fields in module 'fleet'--- +fleet / fleet.service.type / company_id (many2one) : NEW relation: res.company, hasdefault: default +fleet / fleet.vehicle / contract_state (selection) : selection_keys added: [done] (most likely nothing to do) +fleet / fleet.vehicle / horsepower_tax (float) : DEL +fleet / fleet.vehicle / plan_to_change_bike (boolean) : DEL +fleet / fleet.vehicle / plan_to_change_car (boolean) : DEL +fleet / fleet.vehicle / plan_to_change_vehicle (boolean): NEW +fleet / fleet.vehicle / transmission (selection) : selection_keys added: [semi_automatic] (most likely nothing to do) +fleet / fleet.vehicle.log.contract / state (selection) : selection_keys added: [done] (most likely nothing to do) +fleet / fleet.vehicle.log.services / date (date) : DEL +fleet / fleet.vehicle.log.services / date_from (date) : NEW hasdefault: default +fleet / fleet.vehicle.log.services / date_to (date) : NEW +fleet / fleet.vehicle.model / color (char) : DEL +fleet / fleet.vehicle.model / company_id (many2one) : NEW relation: res.company +fleet / fleet.vehicle.model / horsepower_tax (float) : DEL +fleet / fleet.vehicle.model / trailer_hook (boolean) : DEL +fleet / fleet.vehicle.model / transmission (selection) : selection_keys added: [semi_automatic] (most likely nothing to do) +fleet / fleet.vehicle.model / vehicle_type (selection) : selection_keys added: [other] (most likely nothing to do) +---XML records in module 'fleet'--- +fleet.service.type: fleet.type_contract_leasing (noupdate switched) +fleet.service.type: fleet.type_contract_omnium (noupdate switched) +fleet.vehicle.state: fleet.fleet_vehicle_state_downgraded (noupdate switched) +fleet.vehicle.state: fleet.fleet_vehicle_state_new_request (noupdate switched) +fleet.vehicle.state: fleet.fleet_vehicle_state_registered (noupdate switched) +fleet.vehicle.state: fleet.fleet_vehicle_state_to_order (noupdate switched) +NEW ir.access: fleet.access_fleet_report_manager +NEW ir.access: fleet.access_fleet_vehicle_assignation_log_fleet_group_user +NEW ir.access: fleet.access_fleet_vehicle_odometer_report_manager +NEW ir.access: fleet.access_fleet_vehicle_send_mail +NEW ir.access: fleet.access_mail_activity_type_fleet_manager +NEW ir.access: fleet.fleet_rule_service_visibility_manager +NEW ir.access: fleet.fleet_rule_vehicle_visibility_user +NEW ir.access: fleet.fleet_service_type_access_right +NEW ir.access: fleet.fleet_service_type_access_right_user +NEW ir.access: fleet.fleet_vehicle_log_contract_access_right_user +NEW ir.access: fleet.fleet_vehicle_log_services_access_right_user +NEW ir.access: fleet.fleet_vehicle_model_access_right +NEW ir.access: fleet.fleet_vehicle_model_access_right_user +NEW ir.access: fleet.fleet_vehicle_model_brand_access_right +NEW ir.access: fleet.fleet_vehicle_model_brand_access_right_user +NEW ir.access: fleet.fleet_vehicle_model_brand_category_right_user +NEW ir.access: fleet.fleet_vehicle_model_category_access_right +NEW ir.access: fleet.fleet_vehicle_odometer_access_right +NEW ir.access: fleet.fleet_vehicle_state_access_right +NEW ir.access: fleet.fleet_vehicle_state_access_right_user +NEW ir.access: fleet.fleet_vehicle_tag_access_right +NEW ir.access: fleet.fleet_vehicle_tag_access_right_user +NEW ir.access: fleet.ir_rule_fleet_log_services +NEW ir.access: fleet.ir_rule_fleet_odometer +NEW ir.access: fleet.ir_rule_fleet_report +NEW ir.access: fleet.ir_rule_fleet_service_type +NEW ir.access: fleet.ir_rule_fleet_vehicle +NEW ir.access: fleet.ir_rule_fleet_vehicle_log_contract +NEW ir.access: fleet.ir_rule_fleet_vehicle_model +NEW ir.actions.act_window: fleet.fleet_vehicle_contract_types_action +ir.actions.act_window: fleet.fleet_vehicle_odometer_reporting_action (deleted domain) +ir.actions.act_window: fleet.mail_activity_type_action_config_fleet (deleted domain) +ir.cron: fleet.ir_cron_contract_costs_generator (noupdate switched) +DEL ir.model.access: fleet.access_fleet_report_manager +DEL ir.model.access: fleet.access_fleet_vehicle_assignation_log_fleet_group_user +DEL ir.model.access: fleet.access_fleet_vehicle_odometer_report_manager +DEL ir.model.access: fleet.access_fleet_vehicle_send_mail +DEL ir.model.access: fleet.access_mail_activity_type_fleet_manager +DEL ir.model.access: fleet.fleet_service_type_access_right +DEL ir.model.access: fleet.fleet_service_type_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_access_right +DEL ir.model.access: fleet.fleet_vehicle_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_log_contract_access_right +DEL ir.model.access: fleet.fleet_vehicle_log_contract_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_log_services_access_right +DEL ir.model.access: fleet.fleet_vehicle_log_services_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_model_access_right +DEL ir.model.access: fleet.fleet_vehicle_model_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_model_brand_access_right +DEL ir.model.access: fleet.fleet_vehicle_model_brand_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_model_brand_category_right_user +DEL ir.model.access: fleet.fleet_vehicle_model_category_access_right +DEL ir.model.access: fleet.fleet_vehicle_odometer_access_right +DEL ir.model.access: fleet.fleet_vehicle_state_access_right +DEL ir.model.access: fleet.fleet_vehicle_state_access_right_user +DEL ir.model.access: fleet.fleet_vehicle_tag_access_right +DEL ir.model.access: fleet.fleet_vehicle_tag_access_right_user +NEW ir.model.constraint: fleet.constraint_fleet_vehicle_log_services_check_service_dates +ir.model.constraint: fleet.constraint_fleet_vehicle_model_category_name_uniq (changed definition: is now 'False' ('UNIQUE (name)')) +ir.model.constraint: fleet.constraint_fleet_vehicle_state_fleet_state_name_unique (changed definition: is now 'False' ('unique(name)')) +ir.model.constraint: fleet.constraint_fleet_vehicle_tag_name_uniq (changed definition: is now 'False' ('unique (name)')) +NEW ir.model.constraint: fleet.constraint_fleet_vehicle_unique_license_plate +DEL ir.rule: fleet.fleet_rule_contract_visibility_manager (noupdate) +DEL ir.rule: fleet.fleet_rule_odometer_visibility_manager (noupdate) +DEL ir.rule: fleet.fleet_rule_service_visibility_manager (noupdate) +DEL ir.rule: fleet.fleet_rule_vehicle_visibility_manager (noupdate) +DEL ir.rule: fleet.ir_rule_fleet_log_services (noupdate) +DEL ir.rule: fleet.ir_rule_fleet_odometer (noupdate) +DEL ir.rule: fleet.ir_rule_fleet_report (noupdate) +DEL ir.rule: fleet.ir_rule_fleet_vehicle (noupdate) +DEL ir.rule: fleet.ir_rule_fleet_vehicle_log_contract (noupdate) +NEW ir.ui.menu: fleet.fleet_contracts_configuration +NEW ir.ui.menu: fleet.fleet_vehicle_contract_types_menu +NEW ir.ui.view: fleet.fleet_service_type_view_form_shared +NEW ir.ui.view: fleet.fleet_vehicle_odometer_report_view_tree +NEW ir.ui.view: fleet.view_partner_form +DEL ir.ui.view: fleet.fleet_vehicle_odometer_view_graph +mail.activity.type: fleet.mail_act_fleet_contract_to_renew (noupdate switched) +mail.message.subtype: fleet.mt_fleet_driver_updated (noupdate switched) diff --git a/openupgrade_scripts/scripts/fleet/tests/test_fleet_migration.py b/openupgrade_scripts/scripts/fleet/tests/test_fleet_migration.py new file mode 100644 index 00000000000..169cd48afa7 --- /dev/null +++ b/openupgrade_scripts/scripts/fleet/tests/test_fleet_migration.py @@ -0,0 +1,46 @@ +from odoo.tests import TransactionCase + +from odoo.addons.openupgrade_framework import openupgrade_test + + +@openupgrade_test +class TestFleetMigration(TransactionCase): + """Assertions on a service log's date surviving its rename to date_from. + + There is no 19.0 fixture here and there does not need to be: the seed + already holds the discriminating rows, six service logs on six different + dates. What makes the failure invisible without a test is that `date_from` + carries a default of today, so the wrong answer is a full column rather than + an empty one. + """ + + def test_a_service_log_kept_the_date_it_was_executed_on(self): + self.env.cr.execute( + "SELECT count(*), count(DISTINCT date_from) FROM fleet_vehicle_log_services" + ) + total, distinct = self.env.cr.fetchone() + self.assertTrue(total, "the seed has no service logs to check") + self.assertGreater( + distinct, + 1, + "every service log carries the same start date, so the history was " + "overwritten with the day the upgrade ran", + ) + + def test_every_service_log_still_has_a_start_date(self): + """A rename must not leave a row without the date it used to carry.""" + self.env.cr.execute( + "SELECT count(*) FROM fleet_vehicle_log_services WHERE date_from IS NULL" + ) + self.assertEqual( + self.env.cr.fetchone()[0], 0, "a service log came out with no start date" + ) + + def test_the_old_column_is_gone_rather_than_left_beside_the_new_one(self): + self.env.cr.execute( + """ + SELECT count(*) FROM information_schema.columns + WHERE table_name = 'fleet_vehicle_log_services' AND column_name = 'date' + """ + ) + self.assertEqual(self.env.cr.fetchone()[0], 0, "date was copied, not renamed")