diff --git a/maintenance_equipment_usability/README.rst b/maintenance_equipment_usability/README.rst new file mode 100644 index 0000000..f779c3f --- /dev/null +++ b/maintenance_equipment_usability/README.rst @@ -0,0 +1,101 @@ +=============================== +Maintenance Equipment Usability +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:55bf50b3bd1733e7339e1565bfc15fcf9a21f470fddf3addd825cd296b884f6f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-qrtl%2Frbkk--custom-lightgray.png?logo=github + :target: https://github.com/qrtl/rbkk-custom/tree/18.0/maintenance_equipment_usability + :alt: qrtl/rbkk-custom + +|badge1| |badge2| |badge3| + +This module adds an equipment status and a pass/fail maintenance result, +and computes equipment usability from the status, the latest completed +maintenance request result, and a configurable usability grace period. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +The equipment Status (Preparing, Operating, Idle, Disposal / Scrapped) +gates usability: only equipment in the Operating status can be usable. +In any other status the equipment is unusable regardless of its +maintenance result. + +When a maintenance request is completed (it reaches a done stage), set +its Maintenance Result to Passed or Failed. For equipment in the +Operating status, usability is then computed from the latest completed +request by completion date: + +- Failed result: the equipment is unusable. +- Passed result, within the validity period: the equipment is usable. +- Passed result, past the validity period: the equipment is unusable. +- No result yet: the equipment usability is unknown. + +The validity period runs from the latest completed maintenance result +date and defaults to 12 months. Configure it in Settings > Maintenance > +Usability Validity Period (for example 13 or 14 months). Usability is +evaluated from the current date, so equipment becomes unusable +automatically once the validity period elapses. + +The validity always extends to the end of the target month. For example, +with a result passed on 15 June and a 13-month validity period, the +equipment stays usable until 31 July of the following year and becomes +unusable on 1 August. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Quartile + +Contributors +------------ + +- `Quartile `__: + + - Shinnosuke Morita + +Maintainers +----------- + +.. |maintainer-smorita7749| image:: https://github.com/smorita7749.png?size=40px + :target: https://github.com/smorita7749 + :alt: smorita7749 + +Current maintainer: + +|maintainer-smorita7749| + +This module is part of the `qrtl/rbkk-custom `_ project on GitHub. + +You are welcome to contribute. diff --git a/maintenance_equipment_usability/__init__.py b/maintenance_equipment_usability/__init__.py new file mode 100644 index 0000000..116bc48 --- /dev/null +++ b/maintenance_equipment_usability/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/maintenance_equipment_usability/__manifest__.py b/maintenance_equipment_usability/__manifest__.py new file mode 100644 index 0000000..f3d2c1f --- /dev/null +++ b/maintenance_equipment_usability/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +{ + "name": "Maintenance Equipment Usability", + "version": "18.0.1.0.0", + "category": "Maintenance", + "summary": "Compute equipment usability from maintenance request results", + "author": "Quartile", + "website": "https://www.quartile.co", + "maintainers": ["smorita7749"], + "license": "LGPL-3", + "depends": ["maintenance"], + "data": [ + "views/res_config_settings_views.xml", + "views/maintenance_request_views.xml", + "views/maintenance_equipment_views.xml", + ], + "installable": True, +} diff --git a/maintenance_equipment_usability/i18n/ja.po b/maintenance_equipment_usability/i18n/ja.po new file mode 100644 index 0000000..3268f55 --- /dev/null +++ b/maintenance_equipment_usability/i18n/ja.po @@ -0,0 +1,193 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * maintenance_equipment_usability +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 01:09+0000\n" +"PO-Revision-Date: 2026-07-10 01:09+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: maintenance_equipment_usability +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.res_config_settings_view_form +msgid "months" +msgstr "" + +#. module: maintenance_equipment_usability +#: model:ir.model,name:maintenance_equipment_usability.model_res_company +msgid "Companies" +msgstr "会社" + +#. module: maintenance_equipment_usability +#: model:ir.model,name:maintenance_equipment_usability.model_res_config_settings +msgid "Config Settings" +msgstr "コンフィグ設定" + +#. module: maintenance_equipment_usability +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_request_view_form +msgid "Create Follow-up Request" +msgstr "フォローアップ依頼を作成" + +#. module: maintenance_equipment_usability +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.res_config_settings_view_form +msgid "Equipment Usability" +msgstr "設備使用可否" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_request__maintenance_result__failed +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_request_view_search +msgid "Failed" +msgstr "不合格" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,help:maintenance_equipment_usability.field_maintenance_request__source_request_id +msgid "Failed request this follow-up request was created from." +msgstr "このフォローアップ依頼の作成元となった不合格の整備依頼。" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_request__followup_request_ids +msgid "Follow-up Requests" +msgstr "フォローアップ依頼" + +#. module: maintenance_equipment_usability +#. odoo-python +#: code:addons/maintenance_equipment_usability/models/maintenance_request.py:0 +msgid "Follow-up of %s" +msgstr "%s のフォローアップ" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__status__idle +msgid "Idle" +msgstr "遊休" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_equipment__latest_maintenance_result_date +msgid "Latest Result Date" +msgstr "最新結果日" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_equipment__latest_maintenance_result_request_id +msgid "Latest Result Request" +msgstr "最新結果の整備依頼" + +#. module: maintenance_equipment_usability +#: model:ir.model,name:maintenance_equipment_usability.model_maintenance_equipment +msgid "Maintenance Equipment" +msgstr "整備設備" + +#. module: maintenance_equipment_usability +#: model:ir.model,name:maintenance_equipment_usability.model_maintenance_request +msgid "Maintenance Request" +msgstr "整備依頼" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_request__maintenance_result +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_request_view_search +msgid "Maintenance Result" +msgstr "整備結果" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_request__maintenance_result__none +msgid "No Result" +msgstr "結果なし" + +#. module: maintenance_equipment_usability +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.res_config_settings_view_form +msgid "" +"Number of months the equipment stays usable after its latest passed " +"maintenance result. Validity extends to the end of the target month (e.g. " +"passed on 15 June with 13 months stays usable until 31 July of the following " +"year)." +msgstr "" +"最後の合格結果から、設備が使用可能であり続ける月数。有効期限は対象月の月末まで" +"延長されます(例:6月15日に合格・13ヶ月なら翌年7月31日まで使用可能)。" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__status__operating +msgid "Operating" +msgstr "稼働中" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_request__maintenance_result__passed +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_request_view_search +msgid "Passed" +msgstr "合格" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__status__preparing +msgid "Preparing" +msgstr "準備中" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__status__retired +msgid "Retired" +msgstr "除却" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__status__scrapped +msgid "Scrapped" +msgstr "廃棄" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_equipment__smart_search +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_request__smart_search +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_res_company__smart_search +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_res_config_settings__smart_search +msgid "Smart Search" +msgstr "スマート検索" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_request__source_request_id +msgid "Source Request" +msgstr "作成元の整備依頼" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_equipment__status +msgid "Status" +msgstr "ステータス" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__usability_state__unknown +msgid "Unknown" +msgstr "不明" + +#. module: maintenance_equipment_usability +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_view_search +msgid "Unknown Usability" +msgstr "使用可否不明" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__usability_state__unusable +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_view_search +msgid "Unusable" +msgstr "使用不可" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_maintenance_equipment__usability_state +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_view_form +msgid "Usability" +msgstr "使用可否" + +#. module: maintenance_equipment_usability +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.res_config_settings_view_form +msgid "Usability Validity Period" +msgstr "使用可否の有効期間" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_res_company__usability_grace_period_months +#: model:ir.model.fields,field_description:maintenance_equipment_usability.field_res_config_settings__usability_grace_period_months +msgid "Usability Validity Period (Months)" +msgstr "使用可否の有効期間(月)" + +#. module: maintenance_equipment_usability +#: model:ir.model.fields.selection,name:maintenance_equipment_usability.selection__maintenance_equipment__usability_state__usable +#: model_terms:ir.ui.view,arch_db:maintenance_equipment_usability.hr_equipment_view_search +msgid "Usable" +msgstr "使用可" diff --git a/maintenance_equipment_usability/models/__init__.py b/maintenance_equipment_usability/models/__init__.py new file mode 100644 index 0000000..822b3bc --- /dev/null +++ b/maintenance_equipment_usability/models/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from . import maintenance_request +from . import maintenance_equipment +from . import res_company +from . import res_config_settings diff --git a/maintenance_equipment_usability/models/maintenance_equipment.py b/maintenance_equipment_usability/models/maintenance_equipment.py new file mode 100644 index 0000000..88f9b2a --- /dev/null +++ b/maintenance_equipment_usability/models/maintenance_equipment.py @@ -0,0 +1,154 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class MaintenanceEquipment(models.Model): + _inherit = "maintenance.equipment" + + status = fields.Selection( + selection=[ + ("preparing", "Preparing"), + ("operating", "Operating"), + ("idle", "Idle"), + ("retired", "Retired"), + ("scrapped", "Scrapped"), + ], + default="preparing", + ) + usability_state = fields.Selection( + selection=[ + ("unknown", "Unknown"), + ("usable", "Usable"), + ("unusable", "Unusable"), + ], + compute="_compute_usability", + search="_search_usability_state", + string="Usability", + ) + latest_maintenance_result_request_id = fields.Many2one( + comodel_name="maintenance.request", + compute="_compute_latest_maintenance_result", + store=True, + readonly=True, + string="Latest Result Request", + ) + latest_maintenance_result_date = fields.Date( + compute="_compute_latest_maintenance_result", + store=True, + readonly=True, + string="Latest Result Date", + ) + + @api.depends( + "maintenance_ids.close_date", + "maintenance_ids.request_date", + "maintenance_ids.stage_id.done", + "maintenance_ids.maintenance_result", + "maintenance_ids.archive", + ) + def _compute_latest_maintenance_result(self): + requests = ( + self.env["maintenance.request"] + .sudo() + .search( + [ + ("equipment_id", "in", self.ids), + ("stage_id.done", "=", True), + ("maintenance_result", "in", ("passed", "failed")), + ("archive", "=", False), + ], + order="equipment_id, close_date desc, request_date desc, id desc", + ) + ) + latest_by_equipment = {} + for request in requests: + latest_by_equipment.setdefault(request.equipment_id.id, request) + for equipment in self: + request = latest_by_equipment.get(equipment.id) + equipment.latest_maintenance_result_request_id = request + equipment.latest_maintenance_result_date = ( + (request.close_date or request.request_date) if request else False + ) + + @api.depends( + "company_id", + "status", + "latest_maintenance_result_request_id.maintenance_result", + "latest_maintenance_result_date", + ) + def _compute_usability(self): + today = fields.Date.context_today(self) + for equipment in self: + if equipment.status != "operating": + equipment.usability_state = "unusable" + continue + request = equipment.latest_maintenance_result_request_id + result_date = equipment.latest_maintenance_result_date + if not request or not result_date: + equipment.usability_state = "unknown" + continue + if request.maintenance_result == "failed": + equipment.usability_state = "unusable" + continue + company = equipment.company_id or self.env.company + grace_months = company.usability_grace_period_months + if today > result_date + relativedelta(months=grace_months, day=31): + # Validity extends to the end of the target month. + equipment.usability_state = "unusable" + else: + equipment.usability_state = "usable" + + def _search_usability_state(self, operator, value): + all_states = ("unknown", "usable", "unusable") + if operator == "=": + states = [value] if value in all_states else [] + elif operator == "!=": + states = [s for s in all_states if s != value] + elif operator == "in": + states = [s for s in value if s in all_states] + elif operator == "not in": + states = [s for s in all_states if s not in value] + else: + states = [] + if not states: + return [("id", "=", False)] + result = "latest_maintenance_result_request_id.maintenance_result" + # Validity extends to the end of the target month, so the earliest + # still-valid result date is the first day of its month. + threshold = fields.Date.context_today(self).replace(day=1) - relativedelta( + months=self.env.company.usability_grace_period_months + ) + domains = { + # unknown: operating, but no completed result yet + "unknown": [ + "&", + ("status", "=", "operating"), + ("latest_maintenance_result_request_id", "=", False), + ], + # usable: operating, latest result passed, within the grace period + "usable": [ + "&", + ("status", "=", "operating"), + "&", + (result, "=", "passed"), + ("latest_maintenance_result_date", ">=", threshold), + ], + # unusable: not operating, or result failed, or passed but past grace + "unusable": [ + "|", + ("status", "!=", "operating"), + "|", + (result, "=", "failed"), + "&", + (result, "=", "passed"), + ("latest_maintenance_result_date", "<", threshold), + ], + } + domain = domains[states[0]] + for state in states[1:]: + domain = ["|"] + domain + domains[state] + return domain diff --git a/maintenance_equipment_usability/models/maintenance_request.py b/maintenance_equipment_usability/models/maintenance_request.py new file mode 100644 index 0000000..526e739 --- /dev/null +++ b/maintenance_equipment_usability/models/maintenance_request.py @@ -0,0 +1,50 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import _, fields, models + + +class MaintenanceRequest(models.Model): + _inherit = "maintenance.request" + + maintenance_result = fields.Selection( + selection=[ + ("none", "No Result"), + ("passed", "Passed"), + ("failed", "Failed"), + ], + default="none", + copy=False, + tracking=True, + ) + source_request_id = fields.Many2one( + comodel_name="maintenance.request", + string="Source Request", + copy=False, + readonly=True, + help="Failed request this follow-up request was created from.", + ) + followup_request_ids = fields.One2many( + comodel_name="maintenance.request", + inverse_name="source_request_id", + string="Follow-up Requests", + ) + + def action_create_followup_request(self): + self.ensure_one() + followup = self.env["maintenance.request"].create( + { + "name": _("Follow-up of %s", self.name), + "equipment_id": self.equipment_id.id, + "maintenance_team_id": self.maintenance_team_id.id, + "maintenance_type": "corrective", + "source_request_id": self.id, + } + ) + return { + "type": "ir.actions.act_window", + "res_model": "maintenance.request", + "res_id": followup.id, + "view_mode": "form", + "target": "current", + } diff --git a/maintenance_equipment_usability/models/res_company.py b/maintenance_equipment_usability/models/res_company.py new file mode 100644 index 0000000..701ca17 --- /dev/null +++ b/maintenance_equipment_usability/models/res_company.py @@ -0,0 +1,13 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + usability_grace_period_months = fields.Integer( + string="Usability Validity Period (Months)", + default=12, + ) diff --git a/maintenance_equipment_usability/models/res_config_settings.py b/maintenance_equipment_usability/models/res_config_settings.py new file mode 100644 index 0000000..c255fe8 --- /dev/null +++ b/maintenance_equipment_usability/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + usability_grace_period_months = fields.Integer( + related="company_id.usability_grace_period_months", + readonly=False, + string="Usability Validity Period (Months)", + ) diff --git a/maintenance_equipment_usability/pyproject.toml b/maintenance_equipment_usability/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/maintenance_equipment_usability/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/maintenance_equipment_usability/readme/CONTRIBUTORS.md b/maintenance_equipment_usability/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..260160c --- /dev/null +++ b/maintenance_equipment_usability/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Quartile](https://www.quartile.co): + - Shinnosuke Morita diff --git a/maintenance_equipment_usability/readme/DESCRIPTION.md b/maintenance_equipment_usability/readme/DESCRIPTION.md new file mode 100644 index 0000000..d61125e --- /dev/null +++ b/maintenance_equipment_usability/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module adds an equipment status and a pass/fail maintenance result, and +computes equipment usability from the status, the latest completed maintenance +request result, and a configurable usability grace period. diff --git a/maintenance_equipment_usability/readme/USAGE.md b/maintenance_equipment_usability/readme/USAGE.md new file mode 100644 index 0000000..7297028 --- /dev/null +++ b/maintenance_equipment_usability/readme/USAGE.md @@ -0,0 +1,22 @@ +The equipment Status (Preparing, Operating, Idle, Disposal / Scrapped) gates +usability: only equipment in the Operating status can be usable. In any other +status the equipment is unusable regardless of its maintenance result. + +When a maintenance request is completed (it reaches a done stage), set its +Maintenance Result to Passed or Failed. For equipment in the Operating status, +usability is then computed from the latest completed request by completion date: + +- Failed result: the equipment is unusable. +- Passed result, within the validity period: the equipment is usable. +- Passed result, past the validity period: the equipment is unusable. +- No result yet: the equipment usability is unknown. + +The validity period runs from the latest completed maintenance result date and +defaults to 12 months. Configure it in Settings > Maintenance > Usability +Validity Period (for example 13 or 14 months). Usability is evaluated from the +current date, so equipment becomes unusable automatically once the validity +period elapses. + +The validity always extends to the end of the target month. For example, with a +result passed on 15 June and a 13-month validity period, the equipment stays +usable until 31 July of the following year and becomes unusable on 1 August. diff --git a/maintenance_equipment_usability/static/description/index.html b/maintenance_equipment_usability/static/description/index.html new file mode 100644 index 0000000..c20feca --- /dev/null +++ b/maintenance_equipment_usability/static/description/index.html @@ -0,0 +1,450 @@ + + + + + + +Maintenance Equipment Usability + + + +
+

Maintenance Equipment Usability

+ + +

Beta License: LGPL-3 qrtl/rbkk-custom

+

This module adds an equipment status and a pass/fail maintenance result, +and computes equipment usability from the status, the latest completed +maintenance request result, and a configurable usability grace period.

+

Table of contents

+ +
+

Usage

+

The equipment Status (Preparing, Operating, Idle, Disposal / Scrapped) +gates usability: only equipment in the Operating status can be usable. +In any other status the equipment is unusable regardless of its +maintenance result.

+

When a maintenance request is completed (it reaches a done stage), set +its Maintenance Result to Passed or Failed. For equipment in the +Operating status, usability is then computed from the latest completed +request by completion date:

+
    +
  • Failed result: the equipment is unusable.
  • +
  • Passed result, within the validity period: the equipment is usable.
  • +
  • Passed result, past the validity period: the equipment is unusable.
  • +
  • No result yet: the equipment usability is unknown.
  • +
+

The validity period runs from the latest completed maintenance result +date and defaults to 12 months. Configure it in Settings > Maintenance > +Usability Validity Period (for example 13 or 14 months). Usability is +evaluated from the current date, so equipment becomes unusable +automatically once the validity period elapses.

+

The validity always extends to the end of the target month. For example, +with a result passed on 15 June and a 13-month validity period, the +equipment stays usable until 31 July of the following year and becomes +unusable on 1 August.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

smorita7749

+

This module is part of the qrtl/rbkk-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/maintenance_equipment_usability/tests/__init__.py b/maintenance_equipment_usability/tests/__init__.py new file mode 100644 index 0000000..a42d84c --- /dev/null +++ b/maintenance_equipment_usability/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from . import test_maintenance_equipment_usability diff --git a/maintenance_equipment_usability/tests/test_maintenance_equipment_usability.py b/maintenance_equipment_usability/tests/test_maintenance_equipment_usability.py new file mode 100644 index 0000000..33d2fae --- /dev/null +++ b/maintenance_equipment_usability/tests/test_maintenance_equipment_usability.py @@ -0,0 +1,135 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from dateutil.relativedelta import relativedelta + +from odoo import fields + +from odoo.addons.base.tests.common import BaseCommon + + +class TestMaintenanceEquipmentUsability(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.equipment = cls.env["maintenance.equipment"].create( + {"name": "Usability Test Equipment", "status": "operating"} + ) + cls.open_stage = cls.env.ref("maintenance.stage_0") + cls.done_stage = cls.env.ref("maintenance.stage_3") + cls.today = fields.Date.context_today(cls.equipment) + + def _create_request(self, stage, result="none", request_date=None, close_date=None): + vals = { + "name": f"Request {stage.name}", + "equipment_id": self.equipment.id, + "stage_id": stage.id, + "request_date": request_date or self.today, + "maintenance_result": result, + } + if close_date: + vals["close_date"] = close_date + return self.env["maintenance.request"].create(vals) + + def test_usability_unknown_without_completed_result(self): + self.assertEqual(self.equipment.usability_state, "unknown") + # A result on a request that is not done yet is ignored. + self._create_request(self.open_stage, result="passed") + self.assertEqual(self.equipment.usability_state, "unknown") + # A done request without a result is ignored. + self._create_request(self.done_stage, result="none", close_date=self.today) + self.assertEqual(self.equipment.usability_state, "unknown") + self.assertFalse(self.equipment.latest_maintenance_result_request_id) + self.assertFalse(self.equipment.latest_maintenance_result_date) + + def test_passed_within_grace_is_usable(self): + close = self.today - relativedelta(months=1) + self._create_request(self.done_stage, result="passed", close_date=close) + self.assertEqual(self.equipment.usability_state, "usable") + + def test_passed_past_grace_is_unusable(self): + close = self.today - relativedelta(months=13) + self._create_request(self.done_stage, result="passed", close_date=close) + self.assertEqual(self.equipment.usability_state, "unusable") + + def test_failed_is_unusable(self): + self._create_request(self.done_stage, result="failed", close_date=self.today) + self.assertEqual(self.equipment.usability_state, "unusable") + + def test_non_operating_status_is_unusable(self): + self._create_request( + self.done_stage, + result="passed", + close_date=self.today - relativedelta(months=1), + ) + # Operating + passed within grace = usable. + self.assertEqual(self.equipment.usability_state, "usable") + # Any non-operating status forces unusable regardless of the result. + self.equipment.status = "idle" + self.assertEqual(self.equipment.usability_state, "unusable") + self.equipment.status = "preparing" + self.assertEqual(self.equipment.usability_state, "unusable") + + def test_usability_uses_latest_result_by_close_date(self): + self._create_request( + self.done_stage, + result="passed", + close_date=self.today - relativedelta(months=1), + ) + self.assertEqual(self.equipment.usability_state, "usable") + failed = self._create_request( + self.done_stage, result="failed", close_date=self.today + ) + self.assertEqual(self.equipment.usability_state, "unusable") + self.assertEqual(self.equipment.latest_maintenance_result_request_id, failed) + + def test_grace_period_setting_extends_usability(self): + close = self.today - relativedelta(months=13) + self._create_request(self.done_stage, result="passed", close_date=close) + # Default 12-month grace: the 13-month-old result has expired. + self.assertEqual(self.equipment.usability_state, "unusable") + # Extending the grace period to 14 months makes it usable again. + self.env.company.usability_grace_period_months = 14 + self.equipment.invalidate_recordset(["usability_state"]) + self.assertEqual(self.equipment.usability_state, "usable") + + def test_search_usability_state(self): + usable_equipment = self.equipment + self._create_request( + self.done_stage, + result="passed", + close_date=self.today - relativedelta(months=1), + ) + expired_equipment = self.env["maintenance.equipment"].create( + {"name": "Expired Equipment", "status": "operating"} + ) + self.env["maintenance.request"].create( + { + "name": "Expired Request", + "equipment_id": expired_equipment.id, + "stage_id": self.done_stage.id, + "request_date": self.today - relativedelta(months=13), + "close_date": self.today - relativedelta(months=13), + "maintenance_result": "passed", + } + ) + usable = self.env["maintenance.equipment"].search( + [("usability_state", "=", "usable")] + ) + unusable = self.env["maintenance.equipment"].search( + [("usability_state", "=", "unusable")] + ) + self.assertIn(usable_equipment, usable) + self.assertNotIn(expired_equipment, usable) + self.assertIn(expired_equipment, unusable) + + def test_create_followup_request_from_failed(self): + request = self._create_request( + self.done_stage, result="failed", close_date=self.today + ) + request.action_create_followup_request() + followup = request.followup_request_ids + self.assertEqual(len(followup), 1) + self.assertEqual(followup.source_request_id, request) + self.assertEqual(followup.equipment_id, self.equipment) + self.assertEqual(followup.maintenance_type, "corrective") diff --git a/maintenance_equipment_usability/views/maintenance_equipment_views.xml b/maintenance_equipment_usability/views/maintenance_equipment_views.xml new file mode 100644 index 0000000..c38241a --- /dev/null +++ b/maintenance_equipment_usability/views/maintenance_equipment_views.xml @@ -0,0 +1,106 @@ + + + + + maintenance.equipment.form + maintenance.equipment + + + + + + + + + + + + + + + + + maintenance.equipment.list + maintenance.equipment + + + + + + + + + + maintenance.equipment.kanban + maintenance.equipment + + + + + + +
+ +
+
+ +
+
+ +
+
+
+
+ + + maintenance.equipment.search + maintenance.equipment + + + + + + + + + + + + + +
diff --git a/maintenance_equipment_usability/views/maintenance_request_views.xml b/maintenance_equipment_usability/views/maintenance_request_views.xml new file mode 100644 index 0000000..e8dd9dc --- /dev/null +++ b/maintenance_equipment_usability/views/maintenance_request_views.xml @@ -0,0 +1,70 @@ + + + + + maintenance.request.form + maintenance.request + + + + + + + + + + + + + maintenance.request.list + maintenance.request + + + + + + + + + + maintenance.request.search + maintenance.request + + + + + + + + + + + + + diff --git a/maintenance_equipment_usability/views/res_config_settings_views.xml b/maintenance_equipment_usability/views/res_config_settings_views.xml new file mode 100644 index 0000000..10acbda --- /dev/null +++ b/maintenance_equipment_usability/views/res_config_settings_views.xml @@ -0,0 +1,23 @@ + + + + + res.config.settings.form.maintenance.usability + res.config.settings + + + + + + + months + + + + + +