183 lines
7.1 KiB
Python
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,
|
|
)
|