fix(meters): close open schedules on replacement
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-24 00:37:38 +00:00
parent a66d937e96
commit e870b070aa
3 changed files with 19 additions and 5 deletions

View file

@ -28,6 +28,19 @@ TREATED_WITHOUT_READING = "TRAITÉE SANS RELEVÉ"
CANCELLED = "ANNULÉE" CANCELLED = "ANNULÉE"
def cancel_open_occurrences_for_meter(meter, *, commit=False):
"""Clôture explicitement les échéances ouvertes d'un compteur remplacé."""
schedules = MeterReadingSchedule.query.filter_by(meter_id=meter.id).all()
for schedule in schedules:
schedule.is_active = False
for occurrence in MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id, status=TODO).all():
occurrence.status = CANCELLED
occurrence.no_reading_reason = "METER_REPLACED"
occurrence.comment = "Occurrence annulée automatiquement : compteur remplacé ou inactif."
if commit:
db.session.commit()
def _add_months(value, months): def _add_months(value, months):
index = value.year * 12 + value.month - 1 + months index = value.year * 12 + value.month - 1 + months
year, month = divmod(index, 12) year, month = divmod(index, 12)
@ -152,10 +165,7 @@ def generate_occurrences(*, start_date, end_date, schedule_ids=None, commit=True
created = [] created = []
for schedule in query.all(): for schedule in query.all():
if schedule.meter.status == "replaced" or not schedule.meter.is_active: if schedule.meter.status == "replaced" or not schedule.meter.is_active:
for old in MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id, status=TODO).all(): cancel_open_occurrences_for_meter(schedule.meter)
old.status = CANCELLED
old.no_reading_reason = "METER_REPLACED"
old.comment = "Occurrence annulée automatiquement : compteur remplacé ou inactif."
continue continue
for target_date in _target_dates(schedule, start_date, end_date): for target_date in _target_dates(schedule, start_date, end_date):
occurrence = MeterReadingOccurrence.query.filter_by( occurrence = MeterReadingOccurrence.query.filter_by(

View file

@ -176,6 +176,10 @@ def replace_meter(*, old_meter, name, user_id=None, commit=True, **kwargs):
) )
old_meter.status = "replaced" old_meter.status = "replaced"
old_meter.is_active = False old_meter.is_active = False
# C2 : les échéances ouvertes sont clôturées dans la même transaction que
# le remplacement ; l'historique reste consultable.
from .meter_reading_planning import cancel_open_occurrences_for_meter
cancel_open_occurrences_for_meter(old_meter)
db.session.add(new_meter) db.session.add(new_meter)
if commit: if commit:
db.session.commit() db.session.commit()

View file

@ -92,7 +92,7 @@ Les décisions sont ajoutées au fil des checkpoints. Les modèles existants son
La date cible reste distincte de la date opérationnelle. L'anticipation réutilise le moteur horaire/calendrier existant pour les règles scolaires ; les logements sont hors calendrier scolaire par défaut. Sans horaires explicites, le système conserve la date cible plutôt que d'inventer un jour travaillé. La date cible reste distincte de la date opérationnelle. L'anticipation réutilise le moteur horaire/calendrier existant pour les règles scolaires ; les logements sont hors calendrier scolaire par défaut. Sans horaires explicites, le système conserve la date cible plutôt que d'inventer un jour travaillé.
**TRAITEMENT** : une occurrence avec relevé appelle le service C1 `record_meter_reading`. Une occurrence sans relevé conserve un motif distinguant notamment inaccessible, absent, refus et relevé non connu, sans créer de mesure artificielle. Une occurrence ouverte d'un compteur remplacé est annulée avec une raison système et reste historisée. **TRAITEMENT** : une occurrence avec relevé appelle le service C1 `record_meter_reading`. Une occurrence sans relevé conserve un motif distinguant notamment inaccessible, absent, refus et relevé non connu, sans créer de mesure artificielle. Une occurrence ouverte d'un compteur remplacé est annulée immédiatement dans la transaction de remplacement avec une raison système et reste historisée.
**INTÉGRATION** : les occurrences sont une source du `DayPlanner`, avec responsable, durée et retard calculé ; elles ne créent pas d'intervention et ne simulent pas de créneau si le planificateur ne sait pas en attribuer un. Les routes de configuration sont administrateur ; le traitement d'une occurrence est limité à l'utilisateur assigné ou à un administrateur. **INTÉGRATION** : les occurrences sont une source du `DayPlanner`, avec responsable, durée et retard calculé ; elles ne créent pas d'intervention et ne simulent pas de créneau si le planificateur ne sait pas en attribuer un. Les routes de configuration sont administrateur ; le traitement d'une occurrence est limité à l'utilisateur assigné ou à un administrateur.