[IMP] maintenance_project_task_domain, maintenance_server_data, maintenance_service_http_monitoring, maintenance_create_requests_from_project_task: pre-commit execution

This commit is contained in:
Stéphan Sainléger
2026-03-17 22:01:38 +01:00
parent 30a19649d2
commit a563a9f860
43 changed files with 786 additions and 1051 deletions

View File

@@ -3,6 +3,7 @@
## Contexte codebase
### Architecture des modules
```
maintenance_server_data (base, maintenance)
├── Définit: service, service.version, service.instance, backup.server
@@ -22,51 +23,64 @@ maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
```
### Modèle service.instance actuel (dans maintenance_server_data)
- equipment_id (Many2one → maintenance.equipment)
- service_id (Many2one → service, required)
- version_id (Many2one → service.version)
- service_url (Char) ← URL déjà existante, parfait pour les checks HTTP
### Pattern de création maintenance.request existant
- Vérifie si une request non-done existe déjà avant d'en créer une nouvelle
- Stocke la référence sur equipment: error_maintenance_request / warning_maintenance_request
- Stocke la référence sur equipment: error_maintenance_request /
warning_maintenance_request
- Assignation auto: employee_id, technician_user_id, maintenance_team_id
## Requirements confirmés (depuis les specs)
- [x] Modifier le module `maintenance_server_monitoring` (pas de nouveau module)
- [x] Ajouter `maintenance_server_data` aux dépendances (nécessaire pour accéder à service.instance)
- [x] Étendre `service.instance` avec: last_status_code (Integer), last_check_date (Datetime), status OK/KO (Boolean)
- [x] Nouvelle vue liste standalone pour service.instance (nom, version, URL, date check, status code, statut)
- [x] Ajouter `maintenance_server_data` aux dépendances (nécessaire pour accéder à
service.instance)
- [x] Étendre `service.instance` avec: last_status_code (Integer), last_check_date
(Datetime), status OK/KO (Boolean)
- [x] Nouvelle vue liste standalone pour service.instance (nom, version, URL, date
check, status code, statut)
- [x] Paramètres module: fréquence vérification + durée mode maintenance
- [x] Mode maintenance sur equipment: bool tracked + bandeau
- [x] Si service KO → création maintenance.request (pas de doublon si toujours KO)
## Décision architecturale : NOUVEAU MODULE
**Nom**: `maintenance_service_http_monitoring`
**Raison**: Séparation des préoccupations — monitoring infra (SSH/ping) vs monitoring applicatif (HTTP)
**Dépendances**: `base`, `maintenance`, `maintenance_server_data`
**PAS de dépendance** vers `maintenance_server_ssh` ni `maintenance_server_monitoring`
**Nom**: `maintenance_service_http_monitoring` **Raison**: Séparation des préoccupations
— monitoring infra (SSH/ping) vs monitoring applicatif (HTTP) **Dépendances**: `base`,
`maintenance`, `maintenance_server_data` **PAS de dépendance** vers
`maintenance_server_ssh` ni `maintenance_server_monitoring`
## Décisions techniques
- Paramètres via res.config.settings + ir.config_parameter (pattern standard Odoo)
- _inherit = 'service.instance' dans le nouveau module pour étendre le modèle
- \_inherit = 'service.instance' dans le nouveau module pour étendre le modèle
- Cron dédié pour les checks HTTP (fréquence configurable)
- _inherit = 'maintenance.equipment' pour ajouter maintenance_mode + http_maintenance_request
- \_inherit = 'maintenance.equipment' pour ajouter maintenance_mode +
http_maintenance_request
- mail.thread déjà hérité par maintenance.equipment dans Odoo base → tracking fonctionne
## Décisions utilisateur (interview)
1. **Mode maintenance**: HTTP uniquement — le monitoring existant (ping, SSH, mémoire, disque) continue normalement
2. **Lien maintenance.request**: Sur maintenance.equipment — nouveau champ `http_maintenance_request` (Many2one). Si plusieurs services KO sur un même equipment, UNE seule request qui liste les services KO.
3. **Menu service list**: Sous Maintenance > principal, même niveau que Équipements et Demandes
4. **Tests**: OUI — setup pytest-odoo + tests unitaires pour la logique HTTP check et création maintenance requests
1. **Mode maintenance**: HTTP uniquement — le monitoring existant (ping, SSH, mémoire,
disque) continue normalement
2. **Lien maintenance.request**: Sur maintenance.equipment — nouveau champ
`http_maintenance_request` (Many2one). Si plusieurs services KO sur un même
equipment, UNE seule request qui liste les services KO.
3. **Menu service list**: Sous Maintenance > principal, même niveau que Équipements et
Demandes
4. **Tests**: OUI — setup pytest-odoo + tests unitaires pour la logique HTTP check et
création maintenance requests
## Scope boundaries
### IN
- NOUVEAU module `maintenance_service_http_monitoring`
- Étendre service.instance avec champs monitoring HTTP
- Cron dédié pour checks HTTP (fréquence configurable)
@@ -77,6 +91,7 @@ maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
- Setup pytest-odoo + tests unitaires
### OUT
- Aucune modification de `maintenance_server_monitoring` ni de `maintenance_server_ssh`
- Pas de notification email (juste la maintenance.request)
- Pas de dashboard / reporting
@@ -84,9 +99,13 @@ maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
## Décisions Metis (post-gap-analysis)
- Services orphelins (sans equipment_id): N'existent pas en pratique, filtrés par sécurité
- Récupération service KO→OK: Rien d'automatique, close manuelle de la maintenance.request
- Définition KO: Tout échec = KO (timeout, DNS, SSL, connexion refusée, code != 200). Log le détail.
- Timeout HTTP: Hardcodé (constante, comme les seuils existants dans maintenance_server_monitoring)
- Services orphelins (sans equipment_id): N'existent pas en pratique, filtrés par
sécurité
- Récupération service KO→OK: Rien d'automatique, close manuelle de la
maintenance.request
- Définition KO: Tout échec = KO (timeout, DNS, SSL, connexion refusée, code != 200).
Log le détail.
- Timeout HTTP: Hardcodé (constante, comme les seuils existants dans
maintenance_server_monitoring)
- `requests` library: Déclarer dans external_dependencies.python
- Chaque requests.get() DOIT avoir timeout= (pylintrc enforce external-request-timeout)

View File

@@ -10,7 +10,7 @@
"data": [
"data/cron.xml",
"views/service_instance_views.xml",
"views/maintenance_equipment_views.xml"
"views/maintenance_equipment_views.xml",
],
"installable": True
"installable": True,
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="ir_cron_http_service_monitoring" model="ir.cron">
<field name="name">HTTP Service Monitoring : check all services</field>
<field name="model_id" ref="maintenance_server_data.model_service_instance"/>
<field name="model_id" ref="maintenance_server_data.model_service_instance" />
<field name="state">code</field>
<field name="code">model.cron_check_http_services()</field>
<field name="interval_number">15</field>
@@ -11,8 +11,13 @@
<field name="doall">False</field>
</record>
<record id="ir_cron_maintenance_mode_expiry" model="ir.cron">
<field name="name">HTTP Service Monitoring : deactivate expired maintenance mode</field>
<field name="model_id" ref="maintenance_server_data.model_maintenance_equipment"/>
<field
name="name"
>HTTP Service Monitoring : deactivate expired maintenance mode</field>
<field
name="model_id"
ref="maintenance_server_data.model_maintenance_equipment"
/>
<field name="state">code</field>
<field name="code">model.cron_deactivate_expired_maintenance_mode()</field>
<field name="interval_number">15</field>

View File

@@ -1,8 +1,10 @@
from datetime import timedelta
from odoo import models, fields, api
from odoo import api, fields, models
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
_inherit = "maintenance.equipment"
maintenance_mode = fields.Boolean(
string="Maintenance Mode",
@@ -19,37 +21,48 @@ class MaintenanceEquipment(models.Model):
help="Computed from start + configured duration",
)
http_maintenance_request = fields.Many2one(
'maintenance.request',
"maintenance.request",
string="HTTP Maintenance Request",
readonly=True,
)
def action_activate_maintenance_mode(self):
for rec in self:
duration = int(self.env['ir.config_parameter'].sudo().get_param(
'maintenance_service_http_monitoring.maintenance_mode_duration', 4))
duration = int(
self.env["ir.config_parameter"]
.sudo()
.get_param(
"maintenance_service_http_monitoring.maintenance_mode_duration", 4
)
)
now = fields.Datetime.now()
rec.write({
'maintenance_mode': True,
'maintenance_mode_start': now,
'maintenance_mode_end': now + timedelta(hours=duration),
})
rec.write(
{
"maintenance_mode": True,
"maintenance_mode_start": now,
"maintenance_mode_end": now + timedelta(hours=duration),
}
)
def action_deactivate_maintenance_mode(self):
for rec in self:
rec.write({
'maintenance_mode': False,
'maintenance_mode_start': False,
'maintenance_mode_end': False,
})
rec.write(
{
"maintenance_mode": False,
"maintenance_mode_start": False,
"maintenance_mode_end": False,
}
)
@api.model
def cron_deactivate_expired_maintenance_mode(self):
now = fields.Datetime.now()
expired = self.search([
('maintenance_mode', '=', True),
('maintenance_mode_end', '<=', now),
])
expired = self.search(
[
("maintenance_mode", "=", True),
("maintenance_mode_end", "<=", now),
]
)
expired.action_deactivate_maintenance_mode()
def create_http_maintenance_request(self, ko_services):
@@ -57,13 +70,13 @@ class MaintenanceEquipment(models.Model):
today = fields.Date.context_today(self)
name = f"[HTTP KO] {self.name}"
domain = [
('name', '=', name),
('equipment_id', '=', self.id),
('maintenance_type', '=', 'corrective'),
('create_date', '>=', f"{today} 00:00:00"),
('create_date', '<=', f"{today} 23:59:59"),
("name", "=", name),
("equipment_id", "=", self.id),
("maintenance_type", "=", "corrective"),
("create_date", ">=", f"{today} 00:00:00"),
("create_date", "<=", f"{today} 23:59:59"),
]
existing = self.env['maintenance.request'].search(domain, limit=1)
existing = self.env["maintenance.request"].search(domain, limit=1)
# Check if a task with same name already exist for the day, if its the case : skip
if existing:
self.http_maintenance_request = existing.id
@@ -72,26 +85,26 @@ class MaintenanceEquipment(models.Model):
if request and not request.stage_id.done:
return request
vals = {
'name': name,
'equipment_id': self.id,
'priority': '2',
'maintenance_type': 'corrective',
'description': self._build_ko_services_description(ko_services),
"name": name,
"equipment_id": self.id,
"priority": "2",
"maintenance_type": "corrective",
"description": self._build_ko_services_description(ko_services),
}
if self.employee_id:
vals['employee_id'] = self.employee_id.id
vals["employee_id"] = self.employee_id.id
if self.technician_user_id:
vals['user_id'] = self.technician_user_id.id
vals["user_id"] = self.technician_user_id.id
if self.maintenance_team_id:
vals['maintenance_team_id'] = self.maintenance_team_id.id
vals["maintenance_team_id"] = self.maintenance_team_id.id
else:
team = self.env['maintenance.team'].search([], limit=1)
team = self.env["maintenance.team"].search([], limit=1)
if team:
vals['maintenance_team_id'] = team.id
request = self.env['maintenance.request'].create(vals)
vals["maintenance_team_id"] = team.id
request = self.env["maintenance.request"].create(vals)
self.http_maintenance_request = request.id
return request
def _build_ko_services_description(self, ko_services):
lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services]
return '\n'.join(lines)
return "\n".join(lines)

View File

@@ -1,6 +1,6 @@
import logging
from datetime import datetime
from odoo import models, fields, api
from odoo import api, fields, models
try:
import requests
@@ -11,8 +11,9 @@ _logger = logging.getLogger(__name__)
HTTP_CHECK_TIMEOUT = 10 # seconds
class ServiceInstance(models.Model):
_inherit = 'service.instance'
_inherit = "service.instance"
last_http_status_code = fields.Integer(
string="Last HTTP Status Code",
@@ -34,7 +35,7 @@ class ServiceInstance(models.Model):
if not rec.service_url or not rec.equipment_id:
continue
equipment = rec.equipment_id
if getattr(equipment, 'maintenance_mode', False):
if getattr(equipment, "maintenance_mode", False):
continue
status_ok = False
status_code = -1
@@ -45,25 +46,31 @@ class ServiceInstance(models.Model):
try:
response = requests.get(url, timeout=HTTP_CHECK_TIMEOUT)
status_code = response.status_code
status_ok = (status_code == 200)
status_ok = status_code == 200
except requests.exceptions.RequestException as e:
_logger.warning(f"HTTP check failed for %s: %s", rec.service_url, e)
rec.write({
'last_http_status_code': status_code,
'last_http_check_date': now,
'http_status_ok': status_ok,
})
_logger.warning("HTTP check failed for %s: %s", rec.service_url, e)
rec.write(
{
"last_http_status_code": status_code,
"last_http_check_date": now,
"http_status_ok": status_ok,
}
)
if not status_ok:
# Delegate maintenance.request creation to equipment
if hasattr(equipment, 'create_http_maintenance_request'):
if hasattr(equipment, "create_http_maintenance_request"):
equipment.create_http_maintenance_request([rec])
@api.model
def cron_check_http_services(self):
domain = [('active', '=', True), ('service_url', '!=', False), ('equipment_id', '!=', False)]
domain = [
("active", "=", True),
("service_url", "!=", False),
("equipment_id", "!=", False),
]
services = self.search(domain)
for service in services:
equipment = service.equipment_id
if getattr(equipment, 'maintenance_mode', False):
if getattr(equipment, "maintenance_mode", False):
continue
service.check_http_status()

View File

@@ -1,29 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_equipment_form_http_monitoring" model="ir.ui.view">
<field name="name">maintenance.equipment.form.http.monitoring</field>
<field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form" />
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('maintenance_mode', '=', False)]}">
Mode maintenance actif — Vérifications HTTP désactivées.<br/>
Fin prévue : <field name="maintenance_mode_end" readonly="1"/>
<div
class="alert alert-warning"
role="alert"
attrs="{'invisible': [('maintenance_mode', '=', False)]}"
>
Mode maintenance actif — Vérifications HTTP désactivées.<br />
Fin prévue : <field name="maintenance_mode_end" readonly="1" />
</div>
</xpath>
<xpath expr="//header" position="inside">
<button name="action_activate_maintenance_mode" type="object" string="Activer le mode maintenance"
attrs="{'invisible': [('maintenance_mode', '=', True)]}" class="oe_highlight"/>
<button name="action_deactivate_maintenance_mode" type="object" string="Désactiver le mode maintenance"
attrs="{'invisible': [('maintenance_mode', '=', False)]}"/>
<button
name="action_activate_maintenance_mode"
type="object"
string="Activer le mode maintenance"
attrs="{'invisible': [('maintenance_mode', '=', True)]}"
class="oe_highlight"
/>
<button
name="action_deactivate_maintenance_mode"
type="object"
string="Désactiver le mode maintenance"
attrs="{'invisible': [('maintenance_mode', '=', False)]}"
/>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="HTTP Monitoring">
<group>
<field name="maintenance_mode"/>
<field name="maintenance_mode_start"/>
<field name="maintenance_mode_end"/>
<field name="http_maintenance_request" readonly="1"/>
<field name="maintenance_mode" />
<field name="maintenance_mode_start" />
<field name="maintenance_mode_end" />
<field name="http_maintenance_request" readonly="1" />
</group>
</page>
</xpath>
@@ -32,10 +45,10 @@
<record id="view_equipment_tree_http_monitoring" model="ir.ui.view">
<field name="name">maintenance.equipment.tree.http.monitoring</field>
<field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree" />
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<field name="maintenance_mode" optional="hide"/>
<field name="maintenance_mode" optional="hide" />
</xpath>
</field>
</record>

View File

@@ -1,19 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Inherit from base tree view to add HTTP monitoring fields -->
<record id="service_instance_http_monitoring_tree" model="ir.ui.view">
<field name="name">service.instance.http.monitoring.tree</field>
<field name="model">service.instance</field>
<field name="inherit_id" ref="maintenance_server_data.service_instance_view_tree"/>
<field
name="inherit_id"
ref="maintenance_server_data.service_instance_view_tree"
/>
<field name="arch" type="xml">
<tree position="attributes">
<attribute name="decoration-danger">http_status_ok == False</attribute>
</tree>
<field name="version_id" position="after">
<field name="service_url"/>
<field name="last_http_check_date"/>
<field name="last_http_status_code"/>
<field name="http_status_ok"/>
<field name="service_url" />
<field name="last_http_check_date" />
<field name="last_http_status_code" />
<field name="http_status_ok" />
</field>
</field>
</record>
@@ -22,18 +25,33 @@
<record id="service_instance_http_monitoring_search" model="ir.ui.view">
<field name="name">service.instance.http.monitoring.search</field>
<field name="model">service.instance</field>
<field name="inherit_id" ref="maintenance_server_data.service_instance_view_search"/>
<field
name="inherit_id"
ref="maintenance_server_data.service_instance_view_search"
/>
<field name="arch" type="xml">
<field name="service_url" position="after">
<field name="last_http_status_code" string="Status Code"/>
<field name="last_http_status_code" string="Status Code" />
</field>
<filter name="inactive" position="before">
<filter string="Status OK" name="status_ok" domain="[('http_status_ok', '=', True)]"/>
<filter string="Status Error" name="status_error" domain="[('http_status_ok', '=', False)]"/>
<separator/>
<filter
string="Status OK"
name="status_ok"
domain="[('http_status_ok', '=', True)]"
/>
<filter
string="Status Error"
name="status_error"
domain="[('http_status_ok', '=', False)]"
/>
<separator />
</filter>
<filter name="group_version" position="after">
<filter string="Status Code" name="group_status_code" context="{'group_by': 'last_http_status_code'}"/>
<filter
string="Status Code"
name="group_status_code"
context="{'group_by': 'last_http_status_code'}"
/>
</filter>
</field>
</record>