From 221a67844c9664e3bcfc19a94db9c4e4c8eae98d Mon Sep 17 00:00:00 2001 From: Carlos Espinosa de los Monteros Sanchez Date: Fri, 24 Jul 2026 11:40:12 -0600 Subject: [PATCH 1/2] [FIX] maintenance_plan: post_init_hook for Odoo 19 removed equipment fields Odoo 19 removed maintenance.equipment.next_action_date and .mtbf; the post_init_hook relied on both and raised AttributeError on install, rolling back the whole transaction on any DB with equipments that have expected_mtbf set. Derive the plan interval from expected_mtbf (in days, interval_step=day), drop the next_action_date matching / UserError branch and the mtbf-based duration, and batch the maintenance.plan creation. --- maintenance_plan/README.rst | 29 ++++----- maintenance_plan/__manifest__.py | 2 +- maintenance_plan/hooks.py | 63 ++++++++----------- maintenance_plan/readme/CONTRIBUTORS.md | 1 + .../static/description/index.html | 1 + 5 files changed, 43 insertions(+), 53 deletions(-) diff --git a/maintenance_plan/README.rst b/maintenance_plan/README.rst index b83d5a21b..c51980b3f 100644 --- a/maintenance_plan/README.rst +++ b/maintenance_plan/README.rst @@ -47,10 +47,10 @@ Despite Odoo's built-in recurrence functionality included in the maintenance requests themselves (since 17.0), this module could be needed for some reasons: -- Odoo implementation is javascript only -- Some companies already work with the existing "Maintenance Plan" - workflow -- Some modules are depending on this one +- Odoo implementation is javascript only +- Some companies already work with the existing "Maintenance Plan" + workflow +- Some modules are depending on this one For reference, this module was initially set deprecated when migrated to 17.0: @@ -131,16 +131,17 @@ Authors Contributors ------------ -- Akim Juillerat -- Matteo Mazzoni -- David Alonso -- AdriĆ  Gil Sorribes -- Jordi Ballester Alomar -- Lois Rilo -- Enric Tobella -- Alexei Rivera -- Yann Papouin -- Yannick Payot +- Akim Juillerat +- Matteo Mazzoni +- David Alonso +- AdriĆ  Gil Sorribes +- Jordi Ballester Alomar +- Lois Rilo +- Enric Tobella +- Alexei Rivera +- Yann Papouin +- Yannick Payot +- Carlos Espinosa Maintainers ----------- diff --git a/maintenance_plan/__manifest__.py b/maintenance_plan/__manifest__.py index 925451ac9..259c477ef 100644 --- a/maintenance_plan/__manifest__.py +++ b/maintenance_plan/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Maintenance Plan", "summary": "Extends preventive maintenance planning", - "version": "19.0.1.0.0", + "version": "19.0.1.0.1", "author": "Camptocamp SA, ForgeFlow, Odoo Community Association (OCA)", "license": "AGPL-3", "category": "Maintenance", diff --git a/maintenance_plan/hooks.py b/maintenance_plan/hooks.py index cdc9b3664..ef89e7fc3 100644 --- a/maintenance_plan/hooks.py +++ b/maintenance_plan/hooks.py @@ -1,47 +1,34 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - import logging -from odoo.exceptions import UserError +_logger = logging.getLogger(__name__) def post_init_hook(env): - logging.getLogger("odoo.addons.maintenance_plan").info( - "Migrating existing preventive maintenance" - ) + """Create a maintenance plan for equipments that had an expected MTBF. + Odoo 19 removed ``maintenance.equipment.next_action_date`` and + ``maintenance.equipment.mtbf``; the original migration relied on both and + raised ``AttributeError`` on install. We now derive the plan interval from + ``expected_mtbf`` (expressed in days) and no longer try to tag a specific + preventive request by its (now inexistent) next action date. + """ + _logger.info("Migrating existing preventive maintenance") equipments = env["maintenance.equipment"].search([("expected_mtbf", "!=", False)]) - - if equipments: - maintenance_kind = env["maintenance.kind"].create( - {"name": "Install", "active": True} - ) - - for equipment in equipments: - request = equipment.maintenance_ids.filtered( - lambda r, equipment=equipment: r.maintenance_type == "preventive" - and not r.stage_id.done - and r.request_date == equipment.next_action_date - ) - if len(request) > 1: - raise UserError( - env._( - "You have multiple preventive maintenance requests on " - "equipment %(name)s next action date (%(date)s). " - "Please leave only one preventive request on the " - "date of equipment's next action to install the module.", - name=equipment.name, - date=equipment.next_action_date, - ) - ) - elif len(request) == 1: - request.write({"maintenance_kind_id": maintenance_kind.id}) - env["maintenance.plan"].create( - { - "equipment_id": equipment.id, - "maintenance_kind_id": maintenance_kind.id, - "duration": equipment.mtbf, - "interval": equipment.expected_mtbf, - } - ) + if not equipments: + return + maintenance_kind = env["maintenance.kind"].create( + {"name": "Install", "active": True} + ) + env["maintenance.plan"].create( + [ + { + "equipment_id": equipment.id, + "maintenance_kind_id": maintenance_kind.id, + "interval": equipment.expected_mtbf, + "interval_step": "day", + } + for equipment in equipments + ] + ) diff --git a/maintenance_plan/readme/CONTRIBUTORS.md b/maintenance_plan/readme/CONTRIBUTORS.md index 0f23ccebe..a732aa1f1 100644 --- a/maintenance_plan/readme/CONTRIBUTORS.md +++ b/maintenance_plan/readme/CONTRIBUTORS.md @@ -8,3 +8,4 @@ - Alexei Rivera \<\> - Yann Papouin \<\> - Yannick Payot \<\> +- Carlos Espinosa \<\> diff --git a/maintenance_plan/static/description/index.html b/maintenance_plan/static/description/index.html index a27d24874..ec92b37be 100644 --- a/maintenance_plan/static/description/index.html +++ b/maintenance_plan/static/description/index.html @@ -481,6 +481,7 @@

Contributors

  • Alexei Rivera <arivera@archeti.com>
  • Yann Papouin <ypa@decgroupe.com>
  • Yannick Payot <yannick.payot@acsone.eu>
  • +
  • Carlos Espinosa <carlos.espinosa@ffp.mx>
  • From 329fbf6e4eea1002a1144f691ef359f935ae07de Mon Sep 17 00:00:00 2001 From: Carlos Espinosa de los Monteros Sanchez Date: Fri, 24 Jul 2026 12:33:21 -0600 Subject: [PATCH 2/2] [ADD] maintenance_plan: test post_init_hook Cover the Odoo 19 post_init_hook: a maintenance.plan (kind 'Install') is created for each equipment with an expected MTBF, using the MTBF as the interval (in days), and equipment without an expected MTBF gets no plan from the hook. --- maintenance_plan/tests/__init__.py | 1 + maintenance_plan/tests/test_post_init_hook.py | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 maintenance_plan/tests/test_post_init_hook.py diff --git a/maintenance_plan/tests/__init__.py b/maintenance_plan/tests/__init__.py index def774ebc..808fd70d4 100644 --- a/maintenance_plan/tests/__init__.py +++ b/maintenance_plan/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_maintenance_plan from . import test_maintenance_plan_domain +from . import test_post_init_hook diff --git a/maintenance_plan/tests/test_post_init_hook.py b/maintenance_plan/tests/test_post_init_hook.py new file mode 100644 index 000000000..f0341585e --- /dev/null +++ b/maintenance_plan/tests/test_post_init_hook.py @@ -0,0 +1,42 @@ +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.tests.common import tagged + +from odoo.addons.base.tests.common import BaseCommon + +from ..hooks import post_init_hook + + +@tagged("post_install", "-at_install") +class TestPostInitHook(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.equipment_obj = cls.env["maintenance.equipment"] + cls.plan_obj = cls.env["maintenance.plan"] + + def test_post_init_hook_creates_plan_from_expected_mtbf(self): + """A plan is created for each equipment with an expected MTBF.""" + equipment = self.equipment_obj.create( + {"name": "Equipment with MTBF", "expected_mtbf": 30} + ) + self.assertFalse(equipment.maintenance_plan_ids) + + post_init_hook(self.env) + + plans = self.plan_obj.search([("equipment_id", "=", equipment.id)]) + self.assertEqual(len(plans), 1) + plan = plans + # Interval is derived from expected_mtbf, expressed in days. + self.assertEqual(plan.interval, 30) + self.assertEqual(plan.interval_step, "day") + self.assertEqual(plan.maintenance_kind_id.name, "Install") + + def test_post_init_hook_ignores_equipment_without_mtbf(self): + """Equipment without an expected MTBF gets no plan from the hook.""" + equipment = self.equipment_obj.create({"name": "Equipment without MTBF"}) + + post_init_hook(self.env) + + plans = self.plan_obj.search([("equipment_id", "=", equipment.id)]) + self.assertFalse(plans)