loko/loko/interventions/services_control_assignment.py

183 lines
7.1 KiB
Python

import logging
from django.utils import timezone
from django.contrib.gis.geos import Point
logger = logging.getLogger(__name__)
def resolve_control_team_and_member(
target_geom=None,
point=None,
scheduled_date=None,
thematic=None,
maintain_type=None,
locations=None,
target_team=None,
):
"""
Détermine l'équipe de contrôle (CompanyTeam) et le contrôleur (CompanyMember) :
1. Si target_team est fourni, restreint la recherche des membres à cette équipe.
Sinon :
- Équipes candidates gérant l'opération 'controls' (OPERATION_CONTROLS).
- Filtrage thématique si l'équipe restreint ses thématiques.
- Filtrage par type de maintenance (corrective, preventive, ameliorative) si l'équipe restreint ses types.
- Résolution spatiale si une géométrie est définie (cible et équipes) :
- team.geom.intersects(target_geom) ou team.geom.contains(point).
- Fallback sur toutes les équipes candidates si aucune géométrie n'est définie.
2. Calendrier d'assignation à la date scheduled_date :
- Évalue le nombre d'agents planifiés et non absents.
- Si aucun agent n'est disponible ou aucune date : retourne (target_team, None) ou (None, None).
- Sélectionne l'équipe ayant le plus d'agents disponibles.
- Sélectionne le premier membre disponible de cette équipe.
"""
from contracts.models import CompanyTeam, CompanyMember
from contracts.services_teams import get_teams_composition_for_period, get_member_absences_for_period
if target_team is not None:
eligible_teams = [target_team]
else:
teams_qs = CompanyTeam.objects.all().select_related('company').prefetch_related('thematics', 'maintain_types')
candidate_teams = [t for t in teams_qs if t.handles_operation(CompanyTeam.OPERATION_CONTROLS)]
if not candidate_teams:
return None, None
# 1. Filtrage thématique si l'équipe restreint ses thématiques
if thematic:
candidate_teams = [
t for t in candidate_teams
if not t.thematics.all() or any(thm.id == thematic.id for thm in t.thematics.all())
]
if not candidate_teams:
return None, None
# 2. Filtrage par type de maintenance si l'équipe restreint ses types de maintenance
if maintain_type:
candidate_teams = [
t for t in candidate_teams
if t.handles_maintain_type(maintain_type)
]
if not candidate_teams:
return None, None
# 3. Résolution spatiale si géométrie définie
has_target_geom = bool(target_geom or point or locations)
teams_with_geom = [t for t in candidate_teams if t.geom is not None]
if has_target_geom and teams_with_geom:
spatial_matches = []
for team in teams_with_geom:
matched = False
if target_geom and team.geom:
try:
matched = team.geom.intersects(target_geom)
except Exception as e:
logger.debug(f"Error checking team {team.id} geom intersects: {e}")
if not matched and point and team.geom:
try:
matched = team.geom.contains(point)
except Exception as e:
logger.debug(f"Error checking team {team.id} geom contains point: {e}")
if not matched and locations and team.geom:
for loc in locations:
loc_geom = getattr(loc, 'geom', None)
if loc_geom:
try:
if team.geom.intersects(loc_geom):
matched = True
break
except Exception:
pass
if matched:
spatial_matches.append(team)
eligible_teams = spatial_matches
else:
# Fallback sans géométrie : toutes les équipes candidates
eligible_teams = candidate_teams
if not eligible_teams:
return None, None
# 4. Calendrier d'assignation
if not scheduled_date:
return (target_team if target_team else (eligible_teams[0] if eligible_teams else None)), None
if hasattr(scheduled_date, 'date'):
target_d = scheduled_date.date()
else:
target_d = scheduled_date
team_ids = {t.id for t in eligible_teams}
compositions = get_teams_composition_for_period(team_ids, target_d, target_d)
all_member_ids = {m['id'] for m_list in compositions.values() for m in m_list}
absences = get_member_absences_for_period(all_member_ids, target_d, target_d) if all_member_ids else {}
scored_teams = []
for team in eligible_teams:
day_members = compositions.get((team.id, target_d), [])
available_members = [
m for m in day_members
if (m['id'], target_d) not in absences
]
scored_teams.append((len(available_members), team, available_members))
scored_teams.sort(key=lambda item: (-item[0], item[1].name))
best_len, best_team, best_members = scored_teams[0]
# Règle stricte pour le contrôleur : si aucun agent disponible ce jour-là, membre = None
if best_len == 0 or not best_members:
return (target_team, None) if target_team else (None, None)
best_member_id = best_members[0]['id']
best_member = CompanyMember.objects.filter(id=best_member_id).first()
return best_team, best_member
def resolve_control_team_and_member_for_intervention(intervention, target_date=None, target_team=None):
"""
Extrait la géométrie, la localisation et la date de l'intervention
puis appelle resolve_control_team_and_member.
"""
point = None
if intervention.lon and intervention.lat:
try:
p = Point(float(intervention.lon), float(intervention.lat), srid=4326)
p.transform(3812)
point = p
except Exception as e:
logger.debug(f"Error transforming intervention coordinates to 3812: {e}")
if target_date is None:
if intervention.status == 'finished':
target_date = (
getattr(intervention, 'end_time', None)
or getattr(intervention, 'planned_end_time', None)
or timezone.now().date()
)
else:
target_date = (
intervention.planned_begin_time
or intervention.begin_time
or intervention.creation_time
or timezone.now().date()
)
locs = list(intervention.locations.all()) if (intervention.pk and hasattr(intervention, 'locations')) else []
return resolve_control_team_and_member(
target_geom=intervention.geom,
point=point,
scheduled_date=target_date,
thematic=intervention.thematic,
maintain_type=intervention.maintain_type,
locations=locs,
target_team=target_team,
)