Compare commits
2 commits
4d0682efc1
...
c20a8c4142
| Author | SHA1 | Date | |
|---|---|---|---|
| c20a8c4142 | |||
| a0ffebc489 |
30 changed files with 1119 additions and 198 deletions
|
|
@ -88,6 +88,8 @@ COPY loko/mobile/ /app/mobile/
|
||||||
COPY loko/observations/ /app/observations/
|
COPY loko/observations/ /app/observations/
|
||||||
COPY loko/controls/ /app/controls/
|
COPY loko/controls/ /app/controls/
|
||||||
COPY loko/interventions/ /app/interventions/
|
COPY loko/interventions/ /app/interventions/
|
||||||
|
COPY loko/inspections/ /app/inspections/
|
||||||
|
COPY loko/exports/ /app/exports/
|
||||||
COPY loko/assets/ /app/assets/
|
COPY loko/assets/ /app/assets/
|
||||||
COPY loko/panoramax/ /app/panoramax/
|
COPY loko/panoramax/ /app/panoramax/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,18 +264,61 @@ class CompanyTeamMemberInline(admin.TabularInline):
|
||||||
verbose_name_plural = "Membres"
|
verbose_name_plural = "Membres"
|
||||||
|
|
||||||
|
|
||||||
|
from django.contrib.gis import admin as gis_admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyTeamAdminForm(forms.ModelForm):
|
||||||
|
operation_types = forms.MultipleChoiceField(
|
||||||
|
choices=CompanyTeam.OPERATION_TYPES,
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
label=_("Types d'opérations gérées"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CompanyTeam
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
self.initial['operation_types'] = self.instance.operation_types or []
|
||||||
|
|
||||||
|
def clean_operation_types(self):
|
||||||
|
return list(self.cleaned_data.get('operation_types') or [])
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CompanyTeam)
|
@admin.register(CompanyTeam)
|
||||||
class CompanyTeamAdmin(admin.ModelAdmin):
|
class CompanyTeamAdmin(gis_admin.GISModelAdmin):
|
||||||
list_display = ('name', 'company', 'thematics_list', 'members_count', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'visibility_start_hour', 'restrict_to_assigned_interventions')
|
form = CompanyTeamAdminForm
|
||||||
|
list_display = (
|
||||||
|
'name', 'company', 'thematics_list', 'operation_types_list', 'has_geom',
|
||||||
|
'members_count', 'can_generate_daily_report', 'photos_required',
|
||||||
|
'restrict_today_interventions_visibility', 'visibility_start_hour',
|
||||||
|
'restrict_to_assigned_interventions'
|
||||||
|
)
|
||||||
search_fields = ('name', 'company__name')
|
search_fields = ('name', 'company__name')
|
||||||
list_filter = ('company', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions')
|
list_filter = (
|
||||||
filter_horizontal = ('thematics',)
|
'company', 'can_generate_daily_report', 'photos_required',
|
||||||
|
'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions'
|
||||||
|
)
|
||||||
|
filter_horizontal = ('thematics', 'asset_categories')
|
||||||
inlines = [CompanyTeamMemberInline]
|
inlines = [CompanyTeamMemberInline]
|
||||||
|
|
||||||
def thematics_list(self, obj):
|
def thematics_list(self, obj):
|
||||||
return ", ".join([thematic.name for thematic in obj.thematics.all()])
|
return ", ".join([thematic.name for thematic in obj.thematics.all()])
|
||||||
thematics_list.short_description = 'Thématiques'
|
thematics_list.short_description = 'Thématiques'
|
||||||
|
|
||||||
|
def operation_types_list(self, obj):
|
||||||
|
return obj.get_operation_types_display() or "—"
|
||||||
|
operation_types_list.short_description = _("Opérations gérées")
|
||||||
|
|
||||||
|
def has_geom(self, obj):
|
||||||
|
return bool(obj.geom)
|
||||||
|
has_geom.boolean = True
|
||||||
|
has_geom.short_description = _("Zone définie")
|
||||||
|
|
||||||
def members_count(self, obj):
|
def members_count(self, obj):
|
||||||
return obj.members.count()
|
return obj.members.count()
|
||||||
members_count.short_description = 'Nb membres'
|
members_count.short_description = 'Nb membres'
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2270,6 +2270,90 @@ msgstr "You do not have permission to generate documents for this order."
|
||||||
msgid "Déclaration de créance créée et sauvegardée."
|
msgid "Déclaration de créance créée et sauvegardée."
|
||||||
msgstr "Debt declaration created and saved."
|
msgstr "Debt declaration created and saved."
|
||||||
|
|
||||||
|
#: contracts/models.py:73
|
||||||
|
msgid "Inspections"
|
||||||
|
msgstr "Inspections"
|
||||||
|
|
||||||
|
#: contracts/models.py:75
|
||||||
|
msgid "Contrôles"
|
||||||
|
msgstr "Controls"
|
||||||
|
|
||||||
|
#: contracts/models.py:76
|
||||||
|
msgid "Observations"
|
||||||
|
msgstr "Observations"
|
||||||
|
|
||||||
|
#: contracts/models.py:93
|
||||||
|
msgid "Zone géographique"
|
||||||
|
msgstr "Geographic zone"
|
||||||
|
|
||||||
|
#: contracts/models.py:94
|
||||||
|
msgid "Emprise géographique multi-polygone pour l'attribution automatique."
|
||||||
|
msgstr "Multi-polygon geographic footprint for automatic assignment."
|
||||||
|
|
||||||
|
#: contracts/models.py:99 contracts/admin.py:274
|
||||||
|
msgid "Types d'opérations gérées"
|
||||||
|
msgstr "Managed operation types"
|
||||||
|
|
||||||
|
#: contracts/models.py:100
|
||||||
|
msgid "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)."
|
||||||
|
msgstr "Operation types supported by this team (e.g. Inspections, Interventions, Controls, Observations)."
|
||||||
|
|
||||||
|
#: contracts/admin.py:312
|
||||||
|
msgid "Opérations gérées"
|
||||||
|
msgstr "Managed operations"
|
||||||
|
|
||||||
|
#: contracts/admin.py:318
|
||||||
|
msgid "Zone définie"
|
||||||
|
msgstr "Defined zone"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:679
|
||||||
|
msgid "1 Mois"
|
||||||
|
msgstr "1 Month"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:684
|
||||||
|
msgid "Portée :"
|
||||||
|
msgstr "Scope:"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:685
|
||||||
|
msgid "Durée appliquée lors de l'assignation ou du retrait d'un membre"
|
||||||
|
msgstr "Duration applied when assigning or removing a member"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:686
|
||||||
|
msgid "Jour"
|
||||||
|
msgstr "Day"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:687
|
||||||
|
msgid "Semaine"
|
||||||
|
msgstr "Week"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:688
|
||||||
|
msgid "Mois"
|
||||||
|
msgstr "Month"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:887
|
||||||
|
msgid "Un mois complet"
|
||||||
|
msgstr "A full month"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:924
|
||||||
|
msgid "Mois source"
|
||||||
|
msgstr "Source month"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:928
|
||||||
|
msgid "Mois cible"
|
||||||
|
msgstr "Target month"
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:648 contracts/views_teams.py:664
|
||||||
|
msgid "Impossible de modifier une période passée."
|
||||||
|
msgstr "Cannot modify a past period."
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:790
|
||||||
|
msgid "Le mois cible ne peut pas être dans le passé."
|
||||||
|
msgstr "Target month cannot be in the past."
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:792
|
||||||
|
msgid "La date cible ne peut pas être dans le passé."
|
||||||
|
msgstr "Target date cannot be in the past."
|
||||||
|
|
||||||
#~ msgid "Ce déplacement modifie l'équipe de"
|
#~ msgid "Ce déplacement modifie l'équipe de"
|
||||||
#~ msgstr "This move changes the team of"
|
#~ msgstr "This move changes the team of"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2320,6 +2320,90 @@ msgstr ""
|
||||||
msgid "Déclaration de créance créée et sauvegardée."
|
msgid "Déclaration de créance créée et sauvegardée."
|
||||||
msgstr "Déclaration de créance créée et sauvegardée."
|
msgstr "Déclaration de créance créée et sauvegardée."
|
||||||
|
|
||||||
|
#: contracts/models.py:73
|
||||||
|
msgid "Inspections"
|
||||||
|
msgstr "Inspections"
|
||||||
|
|
||||||
|
#: contracts/models.py:75
|
||||||
|
msgid "Contrôles"
|
||||||
|
msgstr "Contrôles"
|
||||||
|
|
||||||
|
#: contracts/models.py:76
|
||||||
|
msgid "Observations"
|
||||||
|
msgstr "Observations"
|
||||||
|
|
||||||
|
#: contracts/models.py:93
|
||||||
|
msgid "Zone géographique"
|
||||||
|
msgstr "Zone géographique"
|
||||||
|
|
||||||
|
#: contracts/models.py:94
|
||||||
|
msgid "Emprise géographique multi-polygone pour l'attribution automatique."
|
||||||
|
msgstr "Emprise géographique multi-polygone pour l'attribution automatique."
|
||||||
|
|
||||||
|
#: contracts/models.py:99 contracts/admin.py:274
|
||||||
|
msgid "Types d'opérations gérées"
|
||||||
|
msgstr "Types d'opérations gérées"
|
||||||
|
|
||||||
|
#: contracts/models.py:100
|
||||||
|
msgid "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)."
|
||||||
|
msgstr "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)."
|
||||||
|
|
||||||
|
#: contracts/admin.py:312
|
||||||
|
msgid "Opérations gérées"
|
||||||
|
msgstr "Opérations gérées"
|
||||||
|
|
||||||
|
#: contracts/admin.py:318
|
||||||
|
msgid "Zone définie"
|
||||||
|
msgstr "Zone définie"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:679
|
||||||
|
msgid "1 Mois"
|
||||||
|
msgstr "1 Mois"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:684
|
||||||
|
msgid "Portée :"
|
||||||
|
msgstr "Portée :"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:685
|
||||||
|
msgid "Durée appliquée lors de l'assignation ou du retrait d'un membre"
|
||||||
|
msgstr "Durée appliquée lors de l'assignation ou du retrait d'un membre"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:686
|
||||||
|
msgid "Jour"
|
||||||
|
msgstr "Jour"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:687
|
||||||
|
msgid "Semaine"
|
||||||
|
msgstr "Semaine"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:688
|
||||||
|
msgid "Mois"
|
||||||
|
msgstr "Mois"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:887
|
||||||
|
msgid "Un mois complet"
|
||||||
|
msgstr "Un mois complet"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:924
|
||||||
|
msgid "Mois source"
|
||||||
|
msgstr "Mois source"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:928
|
||||||
|
msgid "Mois cible"
|
||||||
|
msgstr "Mois cible"
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:648 contracts/views_teams.py:664
|
||||||
|
msgid "Impossible de modifier une période passée."
|
||||||
|
msgstr "Impossible de modifier une période passée."
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:790
|
||||||
|
msgid "Le mois cible ne peut pas être dans le passé."
|
||||||
|
msgstr "Le mois cible ne peut pas être dans le passé."
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:792
|
||||||
|
msgid "La date cible ne peut pas être dans le passé."
|
||||||
|
msgstr "La date cible ne peut pas être dans le passé."
|
||||||
|
|
||||||
#~ msgid "Ce déplacement modifie l'équipe de"
|
#~ msgid "Ce déplacement modifie l'équipe de"
|
||||||
#~ msgstr "Ce déplacement modifie l'équipe de"
|
#~ msgstr "Ce déplacement modifie l'équipe de"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2276,6 +2276,90 @@ msgstr ""
|
||||||
msgid "Déclaration de créance créée et sauvegardée."
|
msgid "Déclaration de créance créée et sauvegardée."
|
||||||
msgstr "Schuldvordering aangemaakt en opgeslagen."
|
msgstr "Schuldvordering aangemaakt en opgeslagen."
|
||||||
|
|
||||||
|
#: contracts/models.py:73
|
||||||
|
msgid "Inspections"
|
||||||
|
msgstr "Inspecties"
|
||||||
|
|
||||||
|
#: contracts/models.py:75
|
||||||
|
msgid "Contrôles"
|
||||||
|
msgstr "Controles"
|
||||||
|
|
||||||
|
#: contracts/models.py:76
|
||||||
|
msgid "Observations"
|
||||||
|
msgstr "Observaties"
|
||||||
|
|
||||||
|
#: contracts/models.py:93
|
||||||
|
msgid "Zone géographique"
|
||||||
|
msgstr "Geografische zone"
|
||||||
|
|
||||||
|
#: contracts/models.py:94
|
||||||
|
msgid "Emprise géographique multi-polygone pour l'attribution automatique."
|
||||||
|
msgstr "Geografische multi-polygoondekking voor automatische toewijzing."
|
||||||
|
|
||||||
|
#: contracts/models.py:99 contracts/admin.py:274
|
||||||
|
msgid "Types d'opérations gérées"
|
||||||
|
msgstr "Beheerde bewerkingstypes"
|
||||||
|
|
||||||
|
#: contracts/models.py:100
|
||||||
|
msgid "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)."
|
||||||
|
msgstr "Bewerkingstypes die door dit team worden ondersteund (bijv. Inspecties, Interventies, Controles, Observaties)."
|
||||||
|
|
||||||
|
#: contracts/admin.py:312
|
||||||
|
msgid "Opérations gérées"
|
||||||
|
msgstr "Beheerde bewerkingen"
|
||||||
|
|
||||||
|
#: contracts/admin.py:318
|
||||||
|
msgid "Zone définie"
|
||||||
|
msgstr "Gedefinieerde zone"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:679
|
||||||
|
msgid "1 Mois"
|
||||||
|
msgstr "1 Maand"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:684
|
||||||
|
msgid "Portée :"
|
||||||
|
msgstr "Bereik:"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:685
|
||||||
|
msgid "Durée appliquée lors de l'assignation ou du retrait d'un membre"
|
||||||
|
msgstr "Duur toegepast bij de toewijzing of verwijdering van een lid"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:686
|
||||||
|
msgid "Jour"
|
||||||
|
msgstr "Dag"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:687
|
||||||
|
msgid "Semaine"
|
||||||
|
msgstr "Week"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:688
|
||||||
|
msgid "Mois"
|
||||||
|
msgstr "Maand"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:887
|
||||||
|
msgid "Un mois complet"
|
||||||
|
msgstr "Een volledige maand"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:924
|
||||||
|
msgid "Mois source"
|
||||||
|
msgstr "Bronmaand"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/teams_index.html:928
|
||||||
|
msgid "Mois cible"
|
||||||
|
msgstr "Doelmaand"
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:648 contracts/views_teams.py:664
|
||||||
|
msgid "Impossible de modifier une période passée."
|
||||||
|
msgstr "Kan een verstreken periode niet wijzigen."
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:790
|
||||||
|
msgid "Le mois cible ne peut pas être dans le passé."
|
||||||
|
msgstr "De doelmaand kan niet in het verleden liggen."
|
||||||
|
|
||||||
|
#: contracts/views_teams.py:792
|
||||||
|
msgid "La date cible ne peut pas être dans le passé."
|
||||||
|
msgstr "De doeldatum kan niet in het verleden liggen."
|
||||||
|
|
||||||
#~ msgid "Ce déplacement modifie l'équipe de"
|
#~ msgid "Ce déplacement modifie l'équipe de"
|
||||||
#~ msgstr "Deze verplaatsing wijzigt het team van"
|
#~ msgstr "Deze verplaatsing wijzigt het team van"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 08:32
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contracts', '0007_contract_approval_except_urgent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companyteam',
|
||||||
|
name='geom',
|
||||||
|
field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, help_text="Emprise géographique multi-polygone pour l'attribution automatique.", null=True, srid=3812, verbose_name='Zone géographique'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companyteam',
|
||||||
|
name='operation_types',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text="Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations).", verbose_name="Types d'opérations gérées"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from django.contrib.gis.db import models as gis_models
|
||||||
|
|
||||||
from common.models import Thematic
|
from common.models import Thematic
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -54,12 +56,6 @@ class MaintainType(models.Model):
|
||||||
return self.name_nl or self.name_fr
|
return self.name_nl or self.name_fr
|
||||||
return self.name_fr
|
return self.name_fr
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
lang = get_language()
|
|
||||||
if lang and lang.startswith('nl'):
|
|
||||||
return self.name_nl or self.name_fr
|
|
||||||
return self.name_fr
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Type de maintenance")
|
verbose_name = _("Type de maintenance")
|
||||||
verbose_name_plural = _("Types de maintenance")
|
verbose_name_plural = _("Types de maintenance")
|
||||||
|
|
@ -68,6 +64,18 @@ class MaintainType(models.Model):
|
||||||
|
|
||||||
class CompanyTeam(models.Model):
|
class CompanyTeam(models.Model):
|
||||||
"""Modèle représentant une équipe au sein d'une société ou d'un organisme"""
|
"""Modèle représentant une équipe au sein d'une société ou d'un organisme"""
|
||||||
|
|
||||||
|
OPERATION_INSPECTIONS = 'inspections'
|
||||||
|
OPERATION_INTERVENTIONS = 'interventions'
|
||||||
|
OPERATION_CONTROLS = 'controls'
|
||||||
|
OPERATION_OBSERVATIONS = 'observations'
|
||||||
|
OPERATION_TYPES = [
|
||||||
|
(OPERATION_INSPECTIONS, _('Inspections')),
|
||||||
|
(OPERATION_INTERVENTIONS, _('Interventions')),
|
||||||
|
(OPERATION_CONTROLS, _('Contrôles')),
|
||||||
|
(OPERATION_OBSERVATIONS, _('Observations')),
|
||||||
|
]
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="teams")
|
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="teams")
|
||||||
thematics = models.ManyToManyField('common.Thematic', blank=True, related_name="company_teams")
|
thematics = models.ManyToManyField('common.Thematic', blank=True, related_name="company_teams")
|
||||||
|
|
@ -78,6 +86,19 @@ class CompanyTeam(models.Model):
|
||||||
verbose_name=_("Catégories d'assets gérées"),
|
verbose_name=_("Catégories d'assets gérées"),
|
||||||
help_text=_("Catégories d'assets dont cette équipe est responsable."),
|
help_text=_("Catégories d'assets dont cette équipe est responsable."),
|
||||||
)
|
)
|
||||||
|
geom = gis_models.MultiPolygonField(
|
||||||
|
srid=3812,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Zone géographique"),
|
||||||
|
help_text=_("Emprise géographique multi-polygone pour l'attribution automatique.")
|
||||||
|
)
|
||||||
|
operation_types = models.JSONField(
|
||||||
|
default=list,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Types d'opérations gérées"),
|
||||||
|
help_text=_("Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations).")
|
||||||
|
)
|
||||||
is_assets_manager = models.BooleanField(
|
is_assets_manager = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("Gestionnaire d'assets"),
|
verbose_name=_("Gestionnaire d'assets"),
|
||||||
|
|
@ -115,6 +136,17 @@ class CompanyTeam(models.Model):
|
||||||
help_text=_("Si activé, les membres de cette équipe ne voient que les interventions assignées à l'équipe ou directement à un membre."),
|
help_text=_("Si activé, les membres de cette équipe ne voient que les interventions assignées à l'équipe ou directement à un membre."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def handles_operation(self, op_type: str) -> bool:
|
||||||
|
"""Indique si l'équipe prend en charge le type d'opération donné."""
|
||||||
|
if not self.operation_types:
|
||||||
|
return False
|
||||||
|
return op_type in self.operation_types
|
||||||
|
|
||||||
|
def get_operation_types_display(self) -> str:
|
||||||
|
"""Retourne la liste lisible des types d'opérations gérées."""
|
||||||
|
mapping = dict(self.OPERATION_TYPES)
|
||||||
|
return ", ".join(str(mapping.get(op, op)) for op in (self.operation_types or []))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.company.name})"
|
return f"{self.name} ({self.company.name})"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Service de gestion temporelle et de planification des équipes.
|
Service de gestion temporelle et de planification des équipes.
|
||||||
Gère les appartenances continues, l'historique d'équipe, les absences journalières et la duplication.
|
Gère les appartenances continues, l'historique d'équipe, les absences journalières et la duplication.
|
||||||
"""
|
"""
|
||||||
|
import calendar
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
from typing import Optional, List, Dict, Set, Any
|
from typing import Optional, List, Dict, Set, Any
|
||||||
|
|
@ -182,6 +183,51 @@ def set_member_team_schedule(
|
||||||
return new_history
|
return new_history
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def set_member_team_period_schedule(
|
||||||
|
member: CompanyMember,
|
||||||
|
team_id: Optional[int],
|
||||||
|
start_date: date,
|
||||||
|
end_date: date,
|
||||||
|
action: str = 'move',
|
||||||
|
unassign_team_id: Optional[int] = None,
|
||||||
|
changed_by=None,
|
||||||
|
weekdays_only: bool = False,
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Affecte ou désaffecte un membre sur une plage de dates continue [start_date, end_date].
|
||||||
|
Permet de définir des semaines complètes ou des mois entiers en une seule opération.
|
||||||
|
Ne modifie pas les jours passés (< aujourd'hui).
|
||||||
|
Retourne le nombre de jours mis à jour.
|
||||||
|
"""
|
||||||
|
today = timezone.now().date()
|
||||||
|
num_days = (end_date - start_date).days + 1
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
for i in range(num_days):
|
||||||
|
d = start_date + timedelta(days=i)
|
||||||
|
if d < today:
|
||||||
|
continue
|
||||||
|
if weekdays_only and d.weekday() >= 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
set_member_team_schedule(
|
||||||
|
member=member,
|
||||||
|
team_id=team_id,
|
||||||
|
target_date=d,
|
||||||
|
changed_by=changed_by,
|
||||||
|
sync_current=False,
|
||||||
|
action=action,
|
||||||
|
unassign_team_id=unassign_team_id,
|
||||||
|
)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
if start_date <= today <= end_date:
|
||||||
|
sync_member_current_teams(member, today)
|
||||||
|
|
||||||
|
return updated_count
|
||||||
|
|
||||||
|
|
||||||
def _sync_teams_if_needed(member: CompanyMember, target_date: date, today: date, sync_current: bool):
|
def _sync_teams_if_needed(member: CompanyMember, target_date: date, today: date, sync_current: bool):
|
||||||
"""Synchronise member.teams si la date modifiée impacte la date d'aujourd'hui."""
|
"""Synchronise member.teams si la date modifiée impacte la date d'aujourd'hui."""
|
||||||
if sync_current and target_date <= today:
|
if sync_current and target_date <= today:
|
||||||
|
|
@ -486,3 +532,40 @@ def duplicate_week_schedule(
|
||||||
total_absences += res['absences_count']
|
total_absences += res['absences_count']
|
||||||
|
|
||||||
return {'assigned_count': total_assigned, 'absences_count': total_absences}
|
return {'assigned_count': total_assigned, 'absences_count': total_absences}
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def duplicate_month_schedule(
|
||||||
|
source_year: int,
|
||||||
|
source_month: int,
|
||||||
|
target_year: int,
|
||||||
|
target_month: int,
|
||||||
|
allowed_team_ids: Optional[Set[int]] = None,
|
||||||
|
copy_absences: bool = False,
|
||||||
|
changed_by=None,
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Duplique la configuration des équipes d'un mois source vers un mois cible (jour par jour).
|
||||||
|
Gère automatiquement les longueurs de mois différentes (28 à 31 jours).
|
||||||
|
"""
|
||||||
|
_, src_days = calendar.monthrange(source_year, source_month)
|
||||||
|
_, tgt_days = calendar.monthrange(target_year, target_month)
|
||||||
|
days_to_copy = min(src_days, tgt_days)
|
||||||
|
|
||||||
|
total_assigned = 0
|
||||||
|
total_absences = 0
|
||||||
|
|
||||||
|
for day_num in range(1, days_to_copy + 1):
|
||||||
|
s_day = date(source_year, source_month, day_num)
|
||||||
|
t_day = date(target_year, target_month, day_num)
|
||||||
|
res = duplicate_day_schedule(
|
||||||
|
source_date=s_day,
|
||||||
|
target_date=t_day,
|
||||||
|
allowed_team_ids=allowed_team_ids,
|
||||||
|
copy_absences=copy_absences,
|
||||||
|
changed_by=changed_by,
|
||||||
|
)
|
||||||
|
total_assigned += res['assigned_count']
|
||||||
|
total_absences += res['absences_count']
|
||||||
|
|
||||||
|
return {'assigned_count': total_assigned, 'absences_count': total_absences}
|
||||||
|
|
|
||||||
|
|
@ -676,6 +676,17 @@
|
||||||
<button type="button" class="btn btn-outline-primary" data-mode="work_week">{% trans "5j (Ouvré)" %}</button>
|
<button type="button" class="btn btn-outline-primary" data-mode="work_week">{% trans "5j (Ouvré)" %}</button>
|
||||||
<button type="button" class="btn btn-primary" data-mode="week">{% trans "1 Semaine (7j)" %}</button>
|
<button type="button" class="btn btn-primary" data-mode="week">{% trans "1 Semaine (7j)" %}</button>
|
||||||
<button type="button" class="btn btn-outline-primary" data-mode="two_weeks">{% trans "2 Semaines" %}</button>
|
<button type="button" class="btn btn-outline-primary" data-mode="two_weeks">{% trans "2 Semaines" %}</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" data-mode="month">{% trans "1 Mois" %}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sélecteur global de portée d'assignation -->
|
||||||
|
<div class="d-flex align-items-center gap-1 ms-1">
|
||||||
|
<span class="text-muted small text-nowrap"><i class="bi bi-clock-history me-1"></i>{% trans "Portée :" %}</span>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="planning-assign-scope-group" title="{% trans "Durée appliquée lors de l'assignation ou du retrait d'un membre" %}">
|
||||||
|
<button type="button" class="btn btn-primary" data-scope="day">{% trans "Jour" %}</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" data-scope="week">{% trans "Semaine" %}</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" data-scope="month">{% trans "Mois" %}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -862,7 +873,7 @@
|
||||||
<!-- Mode de duplication -->
|
<!-- Mode de duplication -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">{% trans "Que souhaitez-vous dupliquer ?" %}</label>
|
<label class="form-label fw-semibold">{% trans "Que souhaitez-vous dupliquer ?" %}</label>
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3 flex-wrap">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="dupMode" id="dupModeDay" value="day" checked>
|
<input class="form-check-input" type="radio" name="dupMode" id="dupModeDay" value="day" checked>
|
||||||
<label class="form-check-label" for="dupModeDay">{% trans "Une journée" %}</label>
|
<label class="form-check-label" for="dupModeDay">{% trans "Une journée" %}</label>
|
||||||
|
|
@ -871,6 +882,10 @@
|
||||||
<input class="form-check-input" type="radio" name="dupMode" id="dupModeWeek" value="week">
|
<input class="form-check-input" type="radio" name="dupMode" id="dupModeWeek" value="week">
|
||||||
<label class="form-check-label" for="dupModeWeek">{% trans "Une semaine complète" %}</label>
|
<label class="form-check-label" for="dupModeWeek">{% trans "Une semaine complète" %}</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="dupMode" id="dupModeMonth" value="month">
|
||||||
|
<label class="form-check-label" for="dupModeMonth">{% trans "Un mois complet" %}</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -879,11 +894,11 @@
|
||||||
<div class="row g-2 mb-3">
|
<div class="row g-2 mb-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label small fw-semibold">{% trans "Jour source" %}</label>
|
<label class="form-label small fw-semibold">{% trans "Jour source" %}</label>
|
||||||
<input type="text" class="form-control form-control-sm" id="dupSourceDate">
|
<input type="date" class="form-control form-control-sm" id="dupSourceDate">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label small fw-semibold">{% trans "Jour cible" %}</label>
|
<label class="form-label small fw-semibold">{% trans "Jour cible" %}</label>
|
||||||
<input type="text" class="form-control form-control-sm" id="dupTargetDate">
|
<input type="date" class="form-control form-control-sm" id="dupTargetDate">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -893,11 +908,25 @@
|
||||||
<div class="row g-2 mb-3">
|
<div class="row g-2 mb-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label small fw-semibold">{% trans "Semaine source (Lundi)" %}</label>
|
<label class="form-label small fw-semibold">{% trans "Semaine source (Lundi)" %}</label>
|
||||||
<input type="text" class="form-control form-control-sm" id="dupSourceWeek">
|
<input type="date" class="form-control form-control-sm" id="dupSourceWeek">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label small fw-semibold">{% trans "Semaine cible (Lundi)" %}</label>
|
<label class="form-label small fw-semibold">{% trans "Semaine cible (Lundi)" %}</label>
|
||||||
<input type="text" class="form-control form-control-sm" id="dupTargetWeek">
|
<input type="date" class="form-control form-control-sm" id="dupTargetWeek">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section Mois -->
|
||||||
|
<div id="dupSectionMonth" class="d-none">
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label small fw-semibold">{% trans "Mois source" %}</label>
|
||||||
|
<input type="month" class="form-control form-control-sm" id="dupSourceMonth">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label small fw-semibold">{% trans "Mois cible" %}</label>
|
||||||
|
<input type="month" class="form-control form-control-sm" id="dupTargetMonth">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1572,6 +1601,7 @@ const initialViewMode = (function() {
|
||||||
let _planningState = {
|
let _planningState = {
|
||||||
anchorDate: initialDate,
|
anchorDate: initialDate,
|
||||||
viewMode: initialViewMode,
|
viewMode: initialViewMode,
|
||||||
|
assignScope: getStoredSetting('assign_scope', 'day'),
|
||||||
companyId: getStoredSetting('company_id', ''),
|
companyId: getStoredSetting('company_id', ''),
|
||||||
data: null,
|
data: null,
|
||||||
members: [],
|
members: [],
|
||||||
|
|
@ -1584,6 +1614,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||||
$(`#planning-viewmode-group button[data-mode="${_planningState.viewMode}"]`).removeClass('btn-outline-primary').addClass('btn-primary');
|
$(`#planning-viewmode-group button[data-mode="${_planningState.viewMode}"]`).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||||
|
|
||||||
|
// Restaurer le sélecteur global de portée d'assignation
|
||||||
|
$('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||||
|
$(`#planning-assign-scope-group button[data-scope="${_planningState.assignScope}"]`).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||||
|
|
||||||
|
// Gestion du sélecteur de portée d'assignation
|
||||||
|
$('#planning-assign-scope-group button').on('click', function() {
|
||||||
|
$('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||||
|
$(this).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||||
|
_planningState.assignScope = $(this).data('scope');
|
||||||
|
setStoredSetting('assign_scope', _planningState.assignScope);
|
||||||
|
});
|
||||||
|
|
||||||
// Restaurer les filtres de la sidebar
|
// Restaurer les filtres de la sidebar
|
||||||
const savedSearch = getStoredSetting('search', '');
|
const savedSearch = getStoredSetting('search', '');
|
||||||
if (savedSearch) $('#planning-member-search').val(savedSearch);
|
if (savedSearch) $('#planning-member-search').val(savedSearch);
|
||||||
|
|
@ -1621,7 +1663,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadPlanningData();
|
loadPlanningData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Changement de mode de vue (5j / 7j / 14j)
|
// Changement de mode de vue (5j / 7j / 14j / mois)
|
||||||
$('#planning-viewmode-group button').on('click', function() {
|
$('#planning-viewmode-group button').on('click', function() {
|
||||||
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||||
$(this).removeClass('btn-outline-primary').addClass('btn-primary');
|
$(this).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||||
|
|
@ -1677,13 +1719,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// Mode duplication modal switch
|
// Mode duplication modal switch
|
||||||
$('input[name="dupMode"]').on('change', function() {
|
$('input[name="dupMode"]').on('change', function() {
|
||||||
if ($(this).val() === 'week') {
|
const val = $(this).val();
|
||||||
$('#dupSectionDay').addClass('d-none');
|
$('#dupSectionDay').toggleClass('d-none', val !== 'day');
|
||||||
$('#dupSectionWeek').removeClass('d-none');
|
$('#dupSectionWeek').toggleClass('d-none', val !== 'week');
|
||||||
} else {
|
$('#dupSectionMonth').toggleClass('d-none', val !== 'month');
|
||||||
$('#dupSectionDay').removeClass('d-none');
|
|
||||||
$('#dupSectionWeek').addClass('d-none');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#dupSourceDate').on('change', function() {
|
$('#dupSourceDate').on('change', function() {
|
||||||
|
|
@ -1721,8 +1760,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
function navigatePlanningDate(direction) {
|
function navigatePlanningDate(direction) {
|
||||||
const d = new Date(_planningState.anchorDate);
|
const d = new Date(_planningState.anchorDate);
|
||||||
|
if (_planningState.viewMode === 'month') {
|
||||||
|
d.setDate(1);
|
||||||
|
d.setMonth(d.getMonth() + direction);
|
||||||
|
} else {
|
||||||
const step = _planningState.viewMode === 'two_weeks' ? 14 : 7;
|
const step = _planningState.viewMode === 'two_weeks' ? 14 : 7;
|
||||||
d.setDate(d.getDate() + (direction * step));
|
d.setDate(d.getDate() + (direction * step));
|
||||||
|
}
|
||||||
_planningState.anchorDate = d.toISOString().split('T')[0];
|
_planningState.anchorDate = d.toISOString().split('T')[0];
|
||||||
setStoredSetting('anchor_date', _planningState.anchorDate);
|
setStoredSetting('anchor_date', _planningState.anchorDate);
|
||||||
_planningState.flatpickrInstance.setDate(_planningState.anchorDate);
|
_planningState.flatpickrInstance.setDate(_planningState.anchorDate);
|
||||||
|
|
@ -1982,6 +2026,7 @@ function executePlanningAssign(memberId, targetTeamId, targetDate, action) {
|
||||||
team_id: parseInt(targetTeamId),
|
team_id: parseInt(targetTeamId),
|
||||||
date: targetDate,
|
date: targetDate,
|
||||||
action: action || 'move',
|
action: action || 'move',
|
||||||
|
scope: _planningState.assignScope || 'day',
|
||||||
},
|
},
|
||||||
function(resp) {
|
function(resp) {
|
||||||
loadPlanningData();
|
loadPlanningData();
|
||||||
|
|
@ -2171,6 +2216,7 @@ function unassignMemberFromDate(memberId, dateStr, teamId) {
|
||||||
team_id: null,
|
team_id: null,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
unassign_team_id: teamId ? parseInt(teamId) : null,
|
unassign_team_id: teamId ? parseInt(teamId) : null,
|
||||||
|
scope: _planningState.assignScope || 'day',
|
||||||
},
|
},
|
||||||
function(resp) {
|
function(resp) {
|
||||||
loadPlanningData();
|
loadPlanningData();
|
||||||
|
|
@ -2273,6 +2319,15 @@ function openDuplicateModal() {
|
||||||
const nextWeekDay = String(dWeek.getDate()).padStart(2, '0');
|
const nextWeekDay = String(dWeek.getDate()).padStart(2, '0');
|
||||||
$('#dupTargetWeek').val(`${nextWeekYear}-${nextWeekMonth}-${nextWeekDay}`);
|
$('#dupTargetWeek').val(`${nextWeekYear}-${nextWeekMonth}-${nextWeekDay}`);
|
||||||
|
|
||||||
|
// Initialiser les mois source et cible
|
||||||
|
const srcMonthVal = `${parts[0]}-${parts[1]}`;
|
||||||
|
$('#dupSourceMonth').val(srcMonthVal);
|
||||||
|
const dMonth = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, 1);
|
||||||
|
dMonth.setMonth(dMonth.getMonth() + 1);
|
||||||
|
const nextMonthYear = dMonth.getFullYear();
|
||||||
|
const nextMonthVal = `${nextMonthYear}-${String(dMonth.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
$('#dupTargetMonth').val(nextMonthVal);
|
||||||
|
|
||||||
$('#dupModalAlert').addClass('d-none');
|
$('#dupModalAlert').addClass('d-none');
|
||||||
new bootstrap.Modal(document.getElementById('duplicatePlanningModal')).show();
|
new bootstrap.Modal(document.getElementById('duplicatePlanningModal')).show();
|
||||||
}
|
}
|
||||||
|
|
@ -2307,8 +2362,17 @@ function quickDuplicateToNextDay(sourceDateStr) {
|
||||||
|
|
||||||
function submitDuplicatePlanning() {
|
function submitDuplicatePlanning() {
|
||||||
const mode = $('input[name="dupMode"]:checked').val();
|
const mode = $('input[name="dupMode"]:checked').val();
|
||||||
const sourceDate = mode === 'week' ? $('#dupSourceWeek').val() : $('#dupSourceDate').val();
|
let sourceDate, targetDate;
|
||||||
const targetDate = mode === 'week' ? $('#dupTargetWeek').val() : $('#dupTargetDate').val();
|
if (mode === 'month') {
|
||||||
|
sourceDate = $('#dupSourceMonth').val();
|
||||||
|
targetDate = $('#dupTargetMonth').val();
|
||||||
|
} else if (mode === 'week') {
|
||||||
|
sourceDate = $('#dupSourceWeek').val();
|
||||||
|
targetDate = $('#dupTargetWeek').val();
|
||||||
|
} else {
|
||||||
|
sourceDate = $('#dupSourceDate').val();
|
||||||
|
targetDate = $('#dupTargetDate').val();
|
||||||
|
}
|
||||||
const copyAbsences = $('#dupCopyAbsences').is(':checked');
|
const copyAbsences = $('#dupCopyAbsences').is(':checked');
|
||||||
|
|
||||||
if (!sourceDate || !targetDate) {
|
if (!sourceDate || !targetDate) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ from contracts.services_teams import (
|
||||||
toggle_member_absence,
|
toggle_member_absence,
|
||||||
duplicate_day_schedule,
|
duplicate_day_schedule,
|
||||||
duplicate_week_schedule,
|
duplicate_week_schedule,
|
||||||
|
duplicate_month_schedule,
|
||||||
|
set_member_team_period_schedule,
|
||||||
sync_member_current_teams,
|
sync_member_current_teams,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -176,6 +178,41 @@ class TeamPlanningServicesTests(TestCase):
|
||||||
self.assertIn(self.team_b, teams_day2)
|
self.assertIn(self.team_b, teams_day2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_member_team_period_schedule(self):
|
||||||
|
"""Tester l'affectation sur une période continue."""
|
||||||
|
start_d = date(2026, 10, 5) # Lundi
|
||||||
|
end_d = date(2026, 10, 11) # Dimanche
|
||||||
|
count = set_member_team_period_schedule(
|
||||||
|
member=self.member_alice,
|
||||||
|
team_id=self.team_a.id,
|
||||||
|
start_date=start_d,
|
||||||
|
end_date=end_d,
|
||||||
|
)
|
||||||
|
self.assertEqual(count, 7)
|
||||||
|
for day_offset in range(7):
|
||||||
|
d = start_d + timedelta(days=day_offset)
|
||||||
|
self.assertEqual(get_member_team_for_date(self.member_alice, d), self.team_a)
|
||||||
|
|
||||||
|
def test_duplicate_month_schedule(self):
|
||||||
|
"""Tester la duplication d'un mois entier vers un autre mois."""
|
||||||
|
# Configurer Alice dans Team A tous les jours d'octobre 2026 (31 jours)
|
||||||
|
for day in range(1, 32):
|
||||||
|
d = date(2026, 10, day)
|
||||||
|
set_member_team_schedule(self.member_alice, self.team_a.id, d)
|
||||||
|
|
||||||
|
# Dupliquer vers novembre 2026 (30 jours)
|
||||||
|
res = duplicate_month_schedule(
|
||||||
|
source_year=2026,
|
||||||
|
source_month=10,
|
||||||
|
target_year=2026,
|
||||||
|
target_month=11,
|
||||||
|
)
|
||||||
|
self.assertEqual(res['assigned_count'], 30)
|
||||||
|
|
||||||
|
# Vérifier que le 15 novembre Alice est bien dans Team A
|
||||||
|
self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 11, 15)), self.team_a)
|
||||||
|
|
||||||
|
|
||||||
class TeamPlanningAjaxViewsTests(TestCase):
|
class TeamPlanningAjaxViewsTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from common.models import UserConfig, Role
|
from common.models import UserConfig, Role
|
||||||
|
|
@ -255,8 +292,12 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
||||||
|
|
||||||
def test_planning_duplicate_friday_to_monday_ajax(self):
|
def test_planning_duplicate_friday_to_monday_ajax(self):
|
||||||
"""Vérifie la duplication d'un vendredi vers le lundi suivant via l'endpoint AJAX."""
|
"""Vérifie la duplication d'un vendredi vers le lundi suivant via l'endpoint AJAX."""
|
||||||
friday = date(2026, 9, 11)
|
today = timezone.now().date()
|
||||||
monday = date(2026, 9, 14)
|
days_ahead_to_friday = (4 - today.weekday()) % 7
|
||||||
|
if days_ahead_to_friday == 0:
|
||||||
|
days_ahead_to_friday = 7
|
||||||
|
friday = today + timedelta(days=days_ahead_to_friday)
|
||||||
|
monday = friday + timedelta(days=3)
|
||||||
set_member_team_schedule(self.member_alice, self.team_b.id, friday)
|
set_member_team_schedule(self.member_alice, self.team_b.id, friday)
|
||||||
toggle_member_absence(self.member_alice, friday, is_absent=True, reason='conge', comment='Weekend prolonge')
|
toggle_member_absence(self.member_alice, friday, is_absent=True, reason='conge', comment='Weekend prolonge')
|
||||||
|
|
||||||
|
|
@ -346,7 +387,8 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
||||||
'target_date': past_date,
|
'target_date': past_date,
|
||||||
}), content_type='application/json')
|
}), content_type='application/json')
|
||||||
self.assertEqual(resp_dup.status_code, 400)
|
self.assertEqual(resp_dup.status_code, 400)
|
||||||
self.assertIn('passé', resp_dup.json().get('error', '').lower())
|
error_msg = resp_dup.json().get('error', '').lower()
|
||||||
|
self.assertTrue('passé' in error_msg or 'past' in error_msg)
|
||||||
|
|
||||||
def test_reassign_future_member_to_same_team_as_previous_day(self):
|
def test_reassign_future_member_to_same_team_as_previous_day(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -559,5 +601,58 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
||||||
modal_member_ids = [m['id'] for m in resp_modal.json()['current_members']]
|
modal_member_ids = [m['id'] for m in resp_modal.json()['current_members']]
|
||||||
self.assertIn(member_olivier.id, modal_member_ids)
|
self.assertIn(member_olivier.id, modal_member_ids)
|
||||||
|
|
||||||
|
def test_planning_data_ajax_month_view(self):
|
||||||
|
"""Tester le retour du mode 'month' dans team_planning_data_ajax."""
|
||||||
|
url = reverse('contracts:team_planning_data_ajax')
|
||||||
|
response = self.client.get(url, {'date': '2026-09-15', 'view_mode': 'month'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(len(data['days']), 30) # Septembre compte 30 jours
|
||||||
|
self.assertEqual(data['days'][0]['date'], '2026-09-01')
|
||||||
|
self.assertEqual(data['days'][-1]['date'], '2026-09-30')
|
||||||
|
|
||||||
|
def test_planning_assign_member_ajax_week_scope(self):
|
||||||
|
"""Tester l'assignation avec portée 'week' via l'API AJAX."""
|
||||||
|
# Prendre un lundi dans le futur
|
||||||
|
today = timezone.now().date()
|
||||||
|
next_monday = today + timedelta(days=(7 - today.weekday()))
|
||||||
|
url = reverse('contracts:team_planning_assign_member_ajax')
|
||||||
|
payload = {
|
||||||
|
'member_id': self.member_alice.id,
|
||||||
|
'team_id': self.team_a.id,
|
||||||
|
'date': next_monday.isoformat(),
|
||||||
|
'scope': 'week',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.json()['success'])
|
||||||
|
self.assertEqual(response.json()['scope'], 'week')
|
||||||
|
self.assertEqual(response.json()['updated_count'], 7)
|
||||||
|
|
||||||
|
# Vérifier que le mercredi et le dimanche de cette semaine ont bien Alice
|
||||||
|
self.assertEqual(get_member_team_for_date(self.member_alice, next_monday + timedelta(days=2)), self.team_a)
|
||||||
|
self.assertEqual(get_member_team_for_date(self.member_alice, next_monday + timedelta(days=6)), self.team_a)
|
||||||
|
|
||||||
|
def test_planning_duplicate_ajax_month_mode(self):
|
||||||
|
"""Tester la duplication de mois via l'API AJAX."""
|
||||||
|
# Créer des affectations pour octobre 2026
|
||||||
|
oct_1 = date(2026, 10, 1)
|
||||||
|
set_member_team_schedule(self.member_alice, self.team_b.id, oct_1)
|
||||||
|
|
||||||
|
url = reverse('contracts:team_planning_duplicate_ajax')
|
||||||
|
payload = {
|
||||||
|
'mode': 'month',
|
||||||
|
'source_date': '2026-10',
|
||||||
|
'target_date': '2026-11',
|
||||||
|
'copy_absences': False,
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
self.assertEqual(data['mode'], 'month')
|
||||||
|
self.assertGreaterEqual(data['assigned_count'], 1)
|
||||||
|
self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 11, 1)), self.team_b)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import calendar
|
||||||
import json
|
import json
|
||||||
from datetime import date, timedelta, datetime
|
from datetime import date, timedelta, datetime
|
||||||
|
|
||||||
|
|
@ -21,12 +22,14 @@ from contracts.permissions import (
|
||||||
)
|
)
|
||||||
from contracts.services_teams import (
|
from contracts.services_teams import (
|
||||||
set_member_team_schedule,
|
set_member_team_schedule,
|
||||||
|
set_member_team_period_schedule,
|
||||||
get_member_team_for_date,
|
get_member_team_for_date,
|
||||||
get_teams_composition_for_period,
|
get_teams_composition_for_period,
|
||||||
get_member_absences_for_period,
|
get_member_absences_for_period,
|
||||||
toggle_member_absence,
|
toggle_member_absence,
|
||||||
duplicate_day_schedule,
|
duplicate_day_schedule,
|
||||||
duplicate_week_schedule,
|
duplicate_week_schedule,
|
||||||
|
duplicate_month_schedule,
|
||||||
sync_member_current_teams,
|
sync_member_current_teams,
|
||||||
sync_all_members_current_teams,
|
sync_all_members_current_teams,
|
||||||
)
|
)
|
||||||
|
|
@ -456,6 +459,11 @@ def team_planning_data_ajax(request):
|
||||||
num_days = 14
|
num_days = 14
|
||||||
start_date = monday
|
start_date = monday
|
||||||
end_date = monday + timedelta(days=13)
|
end_date = monday + timedelta(days=13)
|
||||||
|
elif view_mode == 'month':
|
||||||
|
_, days_in_month = calendar.monthrange(anchor_date.year, anchor_date.month)
|
||||||
|
num_days = days_in_month
|
||||||
|
start_date = date(anchor_date.year, anchor_date.month, 1)
|
||||||
|
end_date = date(anchor_date.year, anchor_date.month, days_in_month)
|
||||||
else: # 'week' (default)
|
else: # 'week' (default)
|
||||||
num_days = 7
|
num_days = 7
|
||||||
start_date = monday
|
start_date = monday
|
||||||
|
|
@ -602,6 +610,7 @@ def team_planning_assign_member_ajax(request):
|
||||||
member_id = data.get("member_id")
|
member_id = data.get("member_id")
|
||||||
raw_team_id = data.get("team_id")
|
raw_team_id = data.get("team_id")
|
||||||
date_str = data.get("date")
|
date_str = data.get("date")
|
||||||
|
scope = data.get("scope", "day") # 'day', 'week', 'work_week', 'month'
|
||||||
|
|
||||||
if not member_id or not date_str:
|
if not member_id or not date_str:
|
||||||
return JsonResponse({"error": "Paramètres member_id et date requis."}, status=400)
|
return JsonResponse({"error": "Paramètres member_id et date requis."}, status=400)
|
||||||
|
|
@ -611,7 +620,7 @@ def team_planning_assign_member_ajax(request):
|
||||||
return JsonResponse({"error": "Date invalide."}, status=400)
|
return JsonResponse({"error": "Date invalide."}, status=400)
|
||||||
|
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
if target_date < today:
|
if scope == "day" and target_date < today:
|
||||||
return JsonResponse({"error": "Impossible de modifier la planification d'un jour passé."}, status=400)
|
return JsonResponse({"error": "Impossible de modifier la planification d'un jour passé."}, status=400)
|
||||||
|
|
||||||
member = get_object_or_404(CompanyMember, id=member_id)
|
member = get_object_or_404(CompanyMember, id=member_id)
|
||||||
|
|
@ -631,7 +640,38 @@ def team_planning_assign_member_ajax(request):
|
||||||
raw_unassign_team_id = data.get("unassign_team_id")
|
raw_unassign_team_id = data.get("unassign_team_id")
|
||||||
unassign_team_id = int(raw_unassign_team_id) if raw_unassign_team_id is not None else None
|
unassign_team_id = int(raw_unassign_team_id) if raw_unassign_team_id is not None else None
|
||||||
|
|
||||||
# Effectuer la planification
|
# Effectuer la planification selon la portée choisie
|
||||||
|
if scope in ("week", "work_week"):
|
||||||
|
monday = target_date - timedelta(days=target_date.weekday())
|
||||||
|
week_end = monday + timedelta(days=4 if scope == "work_week" else 6)
|
||||||
|
if week_end < today:
|
||||||
|
return JsonResponse({"error": _("Impossible de modifier une période passée.")}, status=400)
|
||||||
|
updated_count = set_member_team_period_schedule(
|
||||||
|
member=member,
|
||||||
|
team_id=team_id,
|
||||||
|
start_date=monday,
|
||||||
|
end_date=week_end,
|
||||||
|
action=action,
|
||||||
|
unassign_team_id=unassign_team_id,
|
||||||
|
changed_by=request.user,
|
||||||
|
weekdays_only=(scope == "work_week"),
|
||||||
|
)
|
||||||
|
elif scope == "month":
|
||||||
|
_, days_in_month = calendar.monthrange(target_date.year, target_date.month)
|
||||||
|
month_start = date(target_date.year, target_date.month, 1)
|
||||||
|
month_end = date(target_date.year, target_date.month, days_in_month)
|
||||||
|
if month_end < today:
|
||||||
|
return JsonResponse({"error": _("Impossible de modifier une période passée.")}, status=400)
|
||||||
|
updated_count = set_member_team_period_schedule(
|
||||||
|
member=member,
|
||||||
|
team_id=team_id,
|
||||||
|
start_date=month_start,
|
||||||
|
end_date=month_end,
|
||||||
|
action=action,
|
||||||
|
unassign_team_id=unassign_team_id,
|
||||||
|
changed_by=request.user,
|
||||||
|
)
|
||||||
|
else:
|
||||||
set_member_team_schedule(
|
set_member_team_schedule(
|
||||||
member=member,
|
member=member,
|
||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
|
|
@ -641,6 +681,7 @@ def team_planning_assign_member_ajax(request):
|
||||||
action=action,
|
action=action,
|
||||||
unassign_team_id=unassign_team_id,
|
unassign_team_id=unassign_team_id,
|
||||||
)
|
)
|
||||||
|
updated_count = 1
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
@ -648,6 +689,8 @@ def team_planning_assign_member_ajax(request):
|
||||||
"member_name": member.name,
|
"member_name": member.name,
|
||||||
"team_id": team_id,
|
"team_id": team_id,
|
||||||
"date": target_date.isoformat(),
|
"date": target_date.isoformat(),
|
||||||
|
"scope": scope,
|
||||||
|
"updated_count": updated_count,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -728,6 +771,13 @@ def team_planning_duplicate_ajax(request):
|
||||||
if not source_date_str or not target_date_str:
|
if not source_date_str or not target_date_str:
|
||||||
return JsonResponse({"error": "Dates source et cible requises."}, status=400)
|
return JsonResponse({"error": "Dates source et cible requises."}, status=400)
|
||||||
|
|
||||||
|
# Si mode == 'month' et dates au format 'YYYY-MM', normaliser avec le 1er du mois
|
||||||
|
if mode == "month":
|
||||||
|
if len(source_date_str) == 7:
|
||||||
|
source_date_str = f"{source_date_str}-01"
|
||||||
|
if len(target_date_str) == 7:
|
||||||
|
target_date_str = f"{target_date_str}-01"
|
||||||
|
|
||||||
source_date = parse_date(source_date_str)
|
source_date = parse_date(source_date_str)
|
||||||
target_date = parse_date(target_date_str)
|
target_date = parse_date(target_date_str)
|
||||||
|
|
||||||
|
|
@ -735,8 +785,11 @@ def team_planning_duplicate_ajax(request):
|
||||||
return JsonResponse({"error": "Format de date invalide."}, status=400)
|
return JsonResponse({"error": "Format de date invalide."}, status=400)
|
||||||
|
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
if target_date < today:
|
if mode == "month":
|
||||||
return JsonResponse({"error": "La date cible ne peut pas être dans le passé."}, status=400)
|
if (target_date.year, target_date.month) < (today.year, today.month):
|
||||||
|
return JsonResponse({"error": _("Le mois cible ne peut pas être dans le passé.")}, status=400)
|
||||||
|
elif target_date < today:
|
||||||
|
return JsonResponse({"error": _("La date cible ne peut pas être dans le passé.")}, status=400)
|
||||||
|
|
||||||
# Récupérer les équipes que l'utilisateur a le droit d'éditer
|
# Récupérer les équipes que l'utilisateur a le droit d'éditer
|
||||||
viewable = get_viewable_teams_for_user(request.user)
|
viewable = get_viewable_teams_for_user(request.user)
|
||||||
|
|
@ -752,7 +805,17 @@ def team_planning_duplicate_ajax(request):
|
||||||
else:
|
else:
|
||||||
allowed_team_ids = set(viewable.values_list('id', flat=True))
|
allowed_team_ids = set(viewable.values_list('id', flat=True))
|
||||||
|
|
||||||
if mode == "week":
|
if mode == "month":
|
||||||
|
res = duplicate_month_schedule(
|
||||||
|
source_year=source_date.year,
|
||||||
|
source_month=source_date.month,
|
||||||
|
target_year=target_date.year,
|
||||||
|
target_month=target_date.month,
|
||||||
|
allowed_team_ids=allowed_team_ids,
|
||||||
|
copy_absences=copy_absences,
|
||||||
|
changed_by=request.user,
|
||||||
|
)
|
||||||
|
elif mode == "week":
|
||||||
# S'assurer de pointer sur les lundis
|
# S'assurer de pointer sur les lundis
|
||||||
source_monday = source_date - timedelta(days=source_date.weekday())
|
source_monday = source_date - timedelta(days=source_date.weekday())
|
||||||
target_monday = target_date - timedelta(days=target_date.weekday())
|
target_monday = target_date - timedelta(days=target_date.weekday())
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.gis import admin as gis_admin
|
from .models import Inspection, ParkingSpotInspection, GiepInspection, HotBoxInspection, InspectionDocument
|
||||||
from .models import Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, HotBoxInspection, InspectionDocument
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InspectionZone)
|
|
||||||
class InspectionZoneAdmin(gis_admin.GISModelAdmin):
|
|
||||||
list_display = ("code", "name_fr", "thematic", "is_active", "created_at")
|
|
||||||
list_filter = ("thematic", "is_active")
|
|
||||||
search_fields = ("code", "name_fr", "name_nl")
|
|
||||||
filter_horizontal = ("municipalities", "inspectors")
|
|
||||||
|
|
||||||
|
|
||||||
class InspectionDocumentInline(admin.TabularInline):
|
class InspectionDocumentInline(admin.TabularInline):
|
||||||
|
|
@ -35,21 +26,22 @@ def make_compliant(modeladmin, request, queryset):
|
||||||
@admin.register(Inspection)
|
@admin.register(Inspection)
|
||||||
class InspectionAdmin(admin.ModelAdmin):
|
class InspectionAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"id", "asset", "status", "zone", "scheduled_start_date", "scheduled_due_date",
|
"id", "asset", "status", "assigned_team", "assigned_member",
|
||||||
|
"scheduled_start_date", "scheduled_due_date",
|
||||||
"inspector", "inspection_date",
|
"inspector", "inspection_date",
|
||||||
"result_status", "mission_intervention", "checked_intervention",
|
"result_status", "mission_intervention", "checked_intervention",
|
||||||
"apply_changes_to_asset"
|
"apply_changes_to_asset"
|
||||||
)
|
)
|
||||||
list_filter = (
|
list_filter = (
|
||||||
"status", "result_status", "nature", "zone", "inspector_role",
|
"status", "result_status", "nature", "assigned_team", "inspector_role",
|
||||||
"apply_changes_to_asset", "inspection_date"
|
"apply_changes_to_asset", "inspection_date"
|
||||||
)
|
)
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"notes", "inspector__username", "inspector__first_name",
|
"notes", "inspector__username", "inspector__first_name",
|
||||||
"inspector__last_name", "mission_intervention__code",
|
"inspector__last_name", "assigned_team__name", "assigned_member__name",
|
||||||
"checked_intervention__code"
|
"mission_intervention__code", "checked_intervention__code"
|
||||||
)
|
)
|
||||||
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "zone")
|
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "assigned_team", "assigned_member")
|
||||||
inlines = [InspectionDocumentInline]
|
inlines = [InspectionDocumentInline]
|
||||||
actions = [make_cancelled, make_compliant]
|
actions = [make_cancelled, make_compliant]
|
||||||
|
|
||||||
|
|
@ -95,7 +87,7 @@ class HotBoxInspectionAdmin(admin.ModelAdmin):
|
||||||
"cracking", "water_drainage", "inspection_date"
|
"cracking", "water_drainage", "inspection_date"
|
||||||
)
|
)
|
||||||
search_fields = ("notes", "inspector__username", "inspector__first_name", "inspector__last_name", "checked_intervention__code")
|
search_fields = ("notes", "inspector__username", "inspector__first_name", "inspector__last_name", "checked_intervention__code")
|
||||||
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "zone")
|
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "assigned_team", "assigned_member")
|
||||||
inlines = [InspectionDocumentInline]
|
inlines = [InspectionDocumentInline]
|
||||||
actions = [make_cancelled, make_compliant]
|
actions = [make_cancelled, make_compliant]
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1970,6 +1970,19 @@ msgid ""
|
||||||
"d'évaluation)."
|
"d'évaluation)."
|
||||||
msgstr "Visual checks not yet filled in (scheduled or pending evaluation)."
|
msgstr "Visual checks not yet filled in (scheduled or pending evaluation)."
|
||||||
|
|
||||||
|
#: inspections/models.py:163
|
||||||
|
#: inspections/templates/inspections/inspections_list_mobile.html:309
|
||||||
|
msgid "Équipe assignée"
|
||||||
|
msgstr "Assigned team"
|
||||||
|
|
||||||
|
#: inspections/models.py:171
|
||||||
|
msgid "Membre assigné"
|
||||||
|
msgstr "Assigned member"
|
||||||
|
|
||||||
|
#: inspections/templates/inspections/components/edit_inspection_modal.html:675
|
||||||
|
msgid "Équipe"
|
||||||
|
msgstr "Team"
|
||||||
|
|
||||||
#~ msgid "Renforcement de l'entretien préventif"
|
#~ msgid "Renforcement de l'entretien préventif"
|
||||||
#~ msgstr "Reinforcement of preventive maintenance"
|
#~ msgstr "Reinforcement of preventive maintenance"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1980,6 +1980,19 @@ msgstr ""
|
||||||
"Contrôles visuels non encore renseignés (inspection planifiée ou en attente "
|
"Contrôles visuels non encore renseignés (inspection planifiée ou en attente "
|
||||||
"d'évaluation)."
|
"d'évaluation)."
|
||||||
|
|
||||||
|
#: inspections/models.py:163
|
||||||
|
#: inspections/templates/inspections/inspections_list_mobile.html:309
|
||||||
|
msgid "Équipe assignée"
|
||||||
|
msgstr "Équipe assignée"
|
||||||
|
|
||||||
|
#: inspections/models.py:171
|
||||||
|
msgid "Membre assigné"
|
||||||
|
msgstr "Membre assigné"
|
||||||
|
|
||||||
|
#: inspections/templates/inspections/components/edit_inspection_modal.html:675
|
||||||
|
msgid "Équipe"
|
||||||
|
msgstr "Équipe"
|
||||||
|
|
||||||
#~ msgid "Renforcement de l'entretien préventif"
|
#~ msgid "Renforcement de l'entretien préventif"
|
||||||
#~ msgstr "Renforcement de l’entretien préventif"
|
#~ msgstr "Renforcement de l’entretien préventif"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1971,6 +1971,19 @@ msgstr ""
|
||||||
"Visuele controles nog niet ingevuld (geplande inspectie of in afwachting van"
|
"Visuele controles nog niet ingevuld (geplande inspectie of in afwachting van"
|
||||||
" evaluatie)."
|
" evaluatie)."
|
||||||
|
|
||||||
|
#: inspections/models.py:163
|
||||||
|
#: inspections/templates/inspections/inspections_list_mobile.html:309
|
||||||
|
msgid "Équipe assignée"
|
||||||
|
msgstr "Toegewezen team"
|
||||||
|
|
||||||
|
#: inspections/models.py:171
|
||||||
|
msgid "Membre assigné"
|
||||||
|
msgstr "Toegewezen lid"
|
||||||
|
|
||||||
|
#: inspections/templates/inspections/components/edit_inspection_modal.html:675
|
||||||
|
msgid "Équipe"
|
||||||
|
msgstr "Team"
|
||||||
|
|
||||||
#~ msgid "Renforcement de l'entretien préventif"
|
#~ msgid "Renforcement de l'entretien préventif"
|
||||||
#~ msgstr "Versterking van preventief onderhoud"
|
#~ msgstr "Versterking van preventief onderhoud"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 08:32
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contracts', '0008_companyteam_geom_companyteam_operation_types'),
|
||||||
|
('inspections', '0013_promote_existing_hotbox_inspections'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='zone',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='assigned_member',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_inspections', to='contracts.companymember', verbose_name='Membre assigné'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='assigned_team',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_inspections', to='contracts.companyteam', verbose_name='Équipe assignée'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='InspectionZone',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -77,53 +77,6 @@ INSPECTOR_ROLE_CHOICES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InspectionZone(models.Model):
|
|
||||||
"""
|
|
||||||
Zone / District géographique d'inspection pour une thématique donnée.
|
|
||||||
Permet d'affecter automatiquement les inspections de durabilité et de filtrer les tournées des inspecteurs.
|
|
||||||
"""
|
|
||||||
code = models.CharField(max_length=50, unique=True, verbose_name=_("Code du secteur / zone"))
|
|
||||||
name_fr = models.CharField(max_length=200, verbose_name=_("Nom (FR)"))
|
|
||||||
name_nl = models.CharField(max_length=200, blank=True, verbose_name=_("Nom (NL)"))
|
|
||||||
thematic = models.ForeignKey(
|
|
||||||
'common.Thematic',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='inspection_zones',
|
|
||||||
verbose_name=_("Thématique")
|
|
||||||
)
|
|
||||||
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise géographique"))
|
|
||||||
municipalities = models.ManyToManyField(
|
|
||||||
'common.Municipality',
|
|
||||||
blank=True,
|
|
||||||
related_name='inspection_zones',
|
|
||||||
verbose_name=_("Communes associées")
|
|
||||||
)
|
|
||||||
inspectors = models.ManyToManyField(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
blank=True,
|
|
||||||
related_name='assigned_inspection_zones',
|
|
||||||
verbose_name=_("Inspecteurs assignés")
|
|
||||||
)
|
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Actif"))
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Zone / District d'inspection")
|
|
||||||
verbose_name_plural = _("Zones / Districts d'inspection")
|
|
||||||
ordering = ['thematic', 'name_fr']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name_fr} ({self.thematic.name})"
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
from django.utils.translation import get_language
|
|
||||||
lang = get_language()
|
|
||||||
if lang == 'nl' and self.name_nl:
|
|
||||||
return self.name_nl
|
|
||||||
return self.name_fr
|
|
||||||
|
|
||||||
|
|
||||||
class Inspection(models.Model):
|
class Inspection(models.Model):
|
||||||
"""
|
"""
|
||||||
Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'une cible (Asset, Localisation, Intervention).
|
Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'une cible (Asset, Localisation, Intervention).
|
||||||
|
|
@ -201,13 +154,21 @@ class Inspection(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Échéance limite d'inspection")
|
verbose_name=_("Échéance limite d'inspection")
|
||||||
)
|
)
|
||||||
zone = models.ForeignKey(
|
assigned_team = models.ForeignKey(
|
||||||
InspectionZone,
|
'contracts.CompanyTeam',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='inspections',
|
related_name='assigned_inspections',
|
||||||
verbose_name=_("Zone / District d'inspection")
|
verbose_name=_("Équipe assignée")
|
||||||
|
)
|
||||||
|
assigned_member = models.ForeignKey(
|
||||||
|
'contracts.CompanyMember',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='assigned_inspections',
|
||||||
|
verbose_name=_("Membre assigné")
|
||||||
)
|
)
|
||||||
non_compliance_notified = models.BooleanField(
|
non_compliance_notified = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ def can_edit_inspection(user, inspection):
|
||||||
|
|
||||||
# Pour une inspection planifiée / à réaliser :
|
# Pour une inspection planifiée / à réaliser :
|
||||||
if inspection and getattr(inspection, 'status', None) == 'scheduled':
|
if inspection and getattr(inspection, 'status', None) == 'scheduled':
|
||||||
# Inspecteur faisant partie de la zone d'inspection
|
# Membre faisant partie de l'équipe assignée à l'inspection
|
||||||
if inspection.zone_id and inspection.zone.inspectors.filter(id=user.id).exists():
|
if inspection.assigned_team_id and inspection.assigned_team.members.filter(user=user).exists():
|
||||||
return True
|
return True
|
||||||
# Inspecteur ayant la permission can_inspect sur la thématique
|
# Inspecteur ayant la permission can_inspect sur la thématique
|
||||||
insp_thematic = get_inspection_thematic(inspection)
|
insp_thematic = get_inspection_thematic(inspection)
|
||||||
|
|
@ -136,6 +136,10 @@ def can_view_inspection(user, inspection):
|
||||||
insp_thematic = get_inspection_thematic(inspection)
|
insp_thematic = get_inspection_thematic(inspection)
|
||||||
return bool(insp_thematic and insp_thematic.id in viewable_thematic_ids)
|
return bool(insp_thematic and insp_thematic.id in viewable_thematic_ids)
|
||||||
|
|
||||||
|
# 0. Équipe assignée
|
||||||
|
if inspection.assigned_team_id and inspection.assigned_team.members.filter(user=user).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
# 1. Auteur / Inspecteur de l'inspection
|
# 1. Auteur / Inspecteur de l'inspection
|
||||||
if inspection.inspector_id == user.id:
|
if inspection.inspector_id == user.id:
|
||||||
return True
|
return True
|
||||||
|
|
@ -329,9 +333,9 @@ def filter_viewable_inspections_for_user(user, qs=None):
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# 1. Auteur / Inspecteur de l'inspection ou inspecteur de la zone
|
# 1. Auteur / Inspecteur de l'inspection ou membre de l'équipe assignée
|
||||||
conditions.append(Q(inspector=user))
|
conditions.append(Q(inspector=user))
|
||||||
conditions.append(Q(zone__inspectors=user))
|
conditions.append(Q(assigned_team__members__user=user))
|
||||||
|
|
||||||
# 2. Interventions liées visibles (missions d'intervention, contrôles post-travaux, ou cible intervention)
|
# 2. Interventions liées visibles (missions d'intervention, contrôles post-travaux, ou cible intervention)
|
||||||
from interventions.permissions import filter_viewable_interventions_for_user
|
from interventions.permissions import filter_viewable_interventions_for_user
|
||||||
|
|
|
||||||
|
|
@ -130,12 +130,130 @@ def _resolve_inspection_target(intervention, sub_type):
|
||||||
return ContentType.objects.get_for_model(intervention), intervention.pk
|
return ContentType.objects.get_for_model(intervention), intervention.pk
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_team_and_member_for_inspection(target_geom=None, point=None, scheduled_date=None, thematic=None, municipalities=None):
|
||||||
|
"""
|
||||||
|
Détermine automatiquement l'équipe et le membre assigné pour une inspection :
|
||||||
|
1. Filtre les équipes CompanyTeam actives gérant l'opération 'inspections'.
|
||||||
|
2. Filtre par thématique si l'équipe a des thématiques restreintes.
|
||||||
|
3. Résolution spatiale si une géométrie est définie (sur la cible et sur les équipes) :
|
||||||
|
- Tente de faire correspondre géographiquement (intersects target_geom ou contains point).
|
||||||
|
- Si des équipes correspondent spatialement, la sélection s'effectue parmi elles.
|
||||||
|
4. Fallback sur le calendrier :
|
||||||
|
- Si aucune géométrie n'est définie (ni sur la cible, ni sur les équipes),
|
||||||
|
on évalue l'ensemble des équipes candidates gérant les inspections.
|
||||||
|
5. Prise en compte du calendrier d'assignation à la date scheduled_date :
|
||||||
|
- Évalue le nombre d'agents planifiés et disponibles (non absents) à scheduled_date.
|
||||||
|
- Si aucune équipe n'a d'agent disponible ou si aucune date ne correspond, AUCUNE équipe n'est assignée (None, None).
|
||||||
|
- Sinon, choisit l'équipe ayant le plus d'agents disponibles (Option A).
|
||||||
|
- Pré-assigne le membre disponible de cette équipe si applicable.
|
||||||
|
"""
|
||||||
|
from contracts.models import CompanyTeam, CompanyMember
|
||||||
|
from contracts.services_teams import get_teams_composition_for_period, get_member_absences_for_period
|
||||||
|
|
||||||
|
# 1. Équipes candidates gérant les inspections
|
||||||
|
teams_qs = CompanyTeam.objects.all().select_related('company')
|
||||||
|
candidate_teams = [t for t in teams_qs if t.handles_operation(CompanyTeam.OPERATION_INSPECTIONS)]
|
||||||
|
|
||||||
|
if not candidate_teams:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 2. Filtrage thématique si applicable
|
||||||
|
if thematic:
|
||||||
|
filtered_thematic = [
|
||||||
|
t for t in candidate_teams
|
||||||
|
if not t.thematics.exists() or t.thematics.filter(id=thematic.id).exists()
|
||||||
|
]
|
||||||
|
if filtered_thematic:
|
||||||
|
candidate_teams = filtered_thematic
|
||||||
|
|
||||||
|
# 3. Résolution spatiale si géométrie définie
|
||||||
|
has_target_geom = bool(target_geom or point or municipalities)
|
||||||
|
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 municipalities and team.geom:
|
||||||
|
for muni in municipalities:
|
||||||
|
muni_geom = getattr(muni, 'geom', None)
|
||||||
|
if muni_geom:
|
||||||
|
try:
|
||||||
|
if team.geom.intersects(muni_geom):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if matched:
|
||||||
|
spatial_matches.append(team)
|
||||||
|
|
||||||
|
eligible_teams = spatial_matches
|
||||||
|
else:
|
||||||
|
# Aucune géométrie définie (sur la cible ou sur les équipes) -> fallback sur toutes les équipes candidates
|
||||||
|
eligible_teams = candidate_teams
|
||||||
|
|
||||||
|
if not eligible_teams:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 4. Calendrier d'assignation
|
||||||
|
# Si aucune date n'est fournie, alors aucune date ne correspond -> ne pas assigner d'équipe
|
||||||
|
if not scheduled_date:
|
||||||
|
return 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))
|
||||||
|
|
||||||
|
# Trier par nb de membres disponibles décroissant, puis nom de l'équipe
|
||||||
|
scored_teams.sort(key=lambda item: (-item[0], item[1].name))
|
||||||
|
best_len, best_team, best_members = scored_teams[0]
|
||||||
|
|
||||||
|
# Si aucune date ne correspond (aucun agent planifié/disponible à cette date), ne pas assigner d'équipe
|
||||||
|
if best_len == 0:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
assigned_member = None
|
||||||
|
if best_members:
|
||||||
|
assigned_member = CompanyMember.objects.filter(id=best_members[0]['id']).first()
|
||||||
|
|
||||||
|
return best_team, assigned_member
|
||||||
|
|
||||||
|
|
||||||
def schedule_post_intervention_inspection(intervention):
|
def schedule_post_intervention_inspection(intervention):
|
||||||
"""
|
"""
|
||||||
Planifie automatiquement une inspection de durabilité pour une intervention
|
Planifie automatiquement une inspection de durabilité pour une intervention
|
||||||
lorsque son sous-type requiert une inspection post-travaux (ex: Hot Box).
|
lorsque son sous-type requiert une inspection post-travaux (ex: Hot Box).
|
||||||
"""
|
"""
|
||||||
from .models import Inspection, InspectionZone
|
from .models import Inspection
|
||||||
|
|
||||||
sub_type = getattr(intervention, 'sub_type', None)
|
sub_type = getattr(intervention, 'sub_type', None)
|
||||||
if not sub_type or not getattr(sub_type, 'requires_post_inspection', False):
|
if not sub_type or not getattr(sub_type, 'requires_post_inspection', False):
|
||||||
|
|
@ -164,47 +282,41 @@ def schedule_post_intervention_inspection(intervention):
|
||||||
scheduled_start_date = ref_date.date() + timedelta(days=min_days)
|
scheduled_start_date = ref_date.date() + timedelta(days=min_days)
|
||||||
scheduled_due_date = ref_date.date() + timedelta(days=max_days)
|
scheduled_due_date = ref_date.date() + timedelta(days=max_days)
|
||||||
|
|
||||||
# Détermination de la zone d'inspection
|
# Point pour résolution spatiale
|
||||||
thematic = intervention.thematic
|
point = None
|
||||||
zones = InspectionZone.objects.filter(thematic=thematic, is_active=True) if thematic else InspectionZone.objects.none()
|
if intervention.lon and intervention.lat:
|
||||||
zone = None
|
|
||||||
|
|
||||||
# 1. Résolution spatiale par intersection / inclusion
|
|
||||||
if zones.exists():
|
|
||||||
if intervention.geom:
|
|
||||||
zone = zones.filter(geom__intersects=intervention.geom).first()
|
|
||||||
|
|
||||||
if not zone and intervention.lon and intervention.lat:
|
|
||||||
try:
|
try:
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
p = Point(float(intervention.lon), float(intervention.lat), srid=4326)
|
p = Point(float(intervention.lon), float(intervention.lat), srid=4326)
|
||||||
p.transform(3812)
|
p.transform(3812)
|
||||||
zone = zones.filter(geom__contains=p).first()
|
point = p
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error resolving inspection zone via coordinates: {e}")
|
logger.debug(f"Error transforming intervention coordinates to 3812: {e}")
|
||||||
|
|
||||||
# 2. Résolution administrative par commune associée
|
# Récupérer les communes associées
|
||||||
if not zone:
|
munis = []
|
||||||
from common.models import Municipality
|
from common.models import Municipality
|
||||||
for loc in intervention.locations.all():
|
for loc in intervention.locations.all():
|
||||||
loc_obj = loc.location
|
loc_obj = loc.location
|
||||||
muni_to_check = loc_obj if isinstance(loc_obj, Municipality) else getattr(loc_obj, 'municipality', None)
|
muni_obj = loc_obj if isinstance(loc_obj, Municipality) else getattr(loc_obj, 'municipality', None)
|
||||||
if muni_to_check:
|
if muni_obj:
|
||||||
zone = zones.filter(municipalities=muni_to_check).first()
|
munis.append(muni_obj)
|
||||||
if zone:
|
|
||||||
break
|
# Résolution automatique de l'équipe et du membre
|
||||||
|
assigned_team, assigned_member = resolve_team_and_member_for_inspection(
|
||||||
|
target_geom=intervention.geom,
|
||||||
|
point=point,
|
||||||
|
scheduled_date=scheduled_start_date,
|
||||||
|
thematic=intervention.thematic,
|
||||||
|
municipalities=munis,
|
||||||
|
)
|
||||||
|
|
||||||
|
assigned_inspector = assigned_member.user if (assigned_member and assigned_member.user) else None
|
||||||
|
|
||||||
# Détermination de la cible (Asset, Localisation ou Intervention elle-même)
|
# Détermination de la cible (Asset, Localisation ou Intervention elle-même)
|
||||||
target_ct, target_id = _resolve_inspection_target(intervention, sub_type)
|
target_ct, target_id = _resolve_inspection_target(intervention, sub_type)
|
||||||
|
|
||||||
# Pré-assignation si un seul inspecteur est assigné à la zone
|
from .models import Inspection, HotBoxInspection
|
||||||
assigned_inspector = None
|
|
||||||
if zone:
|
|
||||||
zone_inspectors = list(zone.inspectors.all()[:2])
|
|
||||||
if len(zone_inspectors) == 1:
|
|
||||||
assigned_inspector = zone_inspectors[0]
|
|
||||||
|
|
||||||
from .models import Inspection, InspectionZone, HotBoxInspection
|
|
||||||
|
|
||||||
is_hotbox = any(k in (sub_type.code or '').lower() or k in (sub_type.name_fr or '').lower() for k in ('hotbox', 'hot_box', 'hot box'))
|
is_hotbox = any(k in (sub_type.code or '').lower() or k in (sub_type.name_fr or '').lower() for k in ('hotbox', 'hot_box', 'hot box'))
|
||||||
inspection_cls = HotBoxInspection if is_hotbox else Inspection
|
inspection_cls = HotBoxInspection if is_hotbox else Inspection
|
||||||
|
|
@ -220,9 +332,10 @@ def schedule_post_intervention_inspection(intervention):
|
||||||
result_status='pending',
|
result_status='pending',
|
||||||
scheduled_start_date=scheduled_start_date,
|
scheduled_start_date=scheduled_start_date,
|
||||||
scheduled_due_date=scheduled_due_date,
|
scheduled_due_date=scheduled_due_date,
|
||||||
zone=zone,
|
assigned_team=assigned_team,
|
||||||
|
assigned_member=assigned_member,
|
||||||
inspector=assigned_inspector,
|
inspector=assigned_inspector,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
logger.info(f"Scheduled post-intervention inspection {inspection.pk} created for intervention {intervention.code}")
|
logger.info(f"Scheduled post-intervention inspection {inspection.pk} created for intervention {intervention.code} (Team: {assigned_team})")
|
||||||
return inspection
|
return inspection
|
||||||
|
|
|
||||||
|
|
@ -672,8 +672,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
titleModal.innerHTML = `<i class="bi bi-clipboard-check text-success me-2"></i>{% translate "Réaliser l'inspection" %}`;
|
titleModal.innerHTML = `<i class="bi bi-clipboard-check text-success me-2"></i>{% translate "Réaliser l'inspection" %}`;
|
||||||
}
|
}
|
||||||
let schedInfo = `{% translate "Fenêtre" %} : ${data.scheduled_start_date} → ${data.scheduled_due_date}`;
|
let schedInfo = `{% translate "Fenêtre" %} : ${data.scheduled_start_date} → ${data.scheduled_due_date}`;
|
||||||
if (data.zone_name) {
|
if (data.assigned_team_name || data.zone_name) {
|
||||||
schedInfo += ` | {% translate "Zone" %} : ${data.zone_name}`;
|
schedInfo += ` | {% translate "Équipe" %} : ${data.assigned_team_name || data.zone_name}`;
|
||||||
}
|
}
|
||||||
if (data.checked_intervention_code) {
|
if (data.checked_intervention_code) {
|
||||||
schedInfo += ` | ${data.checked_intervention_code}`;
|
schedInfo += ` | ${data.checked_intervention_code}`;
|
||||||
|
|
|
||||||
|
|
@ -158,9 +158,9 @@
|
||||||
<i class="bi {{ insp.thematic_icon }} me-1 text-primary"></i>
|
<i class="bi {{ insp.thematic_icon }} me-1 text-primary"></i>
|
||||||
{{ insp.asset_content_type.name }}
|
{{ insp.asset_content_type.name }}
|
||||||
</span>
|
</span>
|
||||||
{% if insp.zone %}
|
{% if insp.assigned_team %}
|
||||||
<span class="badge bg-light text-secondary border">
|
<span class="badge bg-light text-secondary border">
|
||||||
<i class="bi bi-geo me-1"></i>{{ insp.zone.name_fr }}
|
<i class="bi bi-people me-1"></i>{{ insp.assigned_team.name }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -304,10 +304,10 @@
|
||||||
<span class="text-muted"><i class="bi bi-calendar-range me-1"></i>{% translate "Fenêtre d'inspection" %}</span>
|
<span class="text-muted"><i class="bi bi-calendar-range me-1"></i>{% translate "Fenêtre d'inspection" %}</span>
|
||||||
<span class="fw-semibold">{{ insp.scheduled_start_date|date:"d/m/Y" }} → {{ insp.scheduled_due_date|date:"d/m/Y" }}</span>
|
<span class="fw-semibold">{{ insp.scheduled_start_date|date:"d/m/Y" }} → {{ insp.scheduled_due_date|date:"d/m/Y" }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% if insp.zone %}
|
{% if insp.assigned_team %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
<span class="text-muted"><i class="bi bi-geo me-1"></i>{% translate "District / Zone" %}</span>
|
<span class="text-muted"><i class="bi bi-people me-1"></i>{% translate "Équipe assignée" %}</span>
|
||||||
<span class="fw-semibold">{{ insp.zone.name_fr }}</span>
|
<span class="fw-semibold">{{ insp.assigned_team.name }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if insp.checked_intervention %}
|
{% if insp.checked_intervention %}
|
||||||
|
|
|
||||||
|
|
@ -1288,7 +1288,7 @@ class ViewerInspectionPermissionsAndGeojsonTestCase(TestCase):
|
||||||
|
|
||||||
# Number of queries should be bounded and independent of inspection count
|
# Number of queries should be bounded and independent of inspection count
|
||||||
# (O(1) queries instead of O(N), unaffected by inspection count)
|
# (O(1) queries instead of O(N), unaffected by inspection count)
|
||||||
self.assertLessEqual(len(ctx), 28)
|
self.assertLessEqual(len(ctx), 32)
|
||||||
|
|
||||||
|
|
||||||
class GiepInspectionStatusCalculationTestCase(TestCase):
|
class GiepInspectionStatusCalculationTestCase(TestCase):
|
||||||
|
|
@ -1574,41 +1574,118 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
|
||||||
self.assertEqual(insp.timeliness, 'future')
|
self.assertEqual(insp.timeliness, 'future')
|
||||||
|
|
||||||
def test_zone_and_single_inspector_auto_assignment(self):
|
def test_zone_and_single_inspector_auto_assignment(self):
|
||||||
from common.models import Municipality
|
from datetime import timedelta
|
||||||
from interventions.models import InterventionLocation
|
from contracts.models import Company, CompanyTeam, CompanyMember
|
||||||
from .models import InspectionZone
|
from contracts.services_teams import set_member_team_schedule
|
||||||
|
from django.contrib.gis.geos import Polygon, MultiPolygon
|
||||||
|
|
||||||
zone = InspectionZone.objects.create(
|
company = Company.objects.create(name="Voirie Nord Test")
|
||||||
code='DIST_NORD_TEST',
|
poly = MultiPolygon(Polygon(((100000, 100000), (100000, 200000), (200000, 200000), (200000, 100000), (100000, 100000))))
|
||||||
name_fr='District Nord Test',
|
team = CompanyTeam.objects.create(
|
||||||
thematic=self.roads_thematic
|
name="Inspection Zone 1 Test",
|
||||||
|
company=company,
|
||||||
|
geom=poly,
|
||||||
|
operation_types=['inspections'],
|
||||||
)
|
)
|
||||||
zone.inspectors.add(self.user)
|
team.thematics.add(self.roads_thematic)
|
||||||
muni = Municipality.objects.create(name_fr='Schaerbeek Test', name_nl='Schaarbeek Test', niscode='21099', fid='21099')
|
member = CompanyMember.objects.create(name="Inspecteur Alice", user=self.user)
|
||||||
zone.municipalities.add(muni)
|
|
||||||
|
target_date = (timezone.now() + timedelta(days=30)).date()
|
||||||
|
set_member_team_schedule(member, team.id, target_date)
|
||||||
|
|
||||||
intervention = Intervention.objects.create(
|
intervention = Intervention.objects.create(
|
||||||
thematic=self.roads_thematic,
|
thematic=self.roads_thematic,
|
||||||
type='repair',
|
type='repair',
|
||||||
sub_type=self.hotbox_subtype,
|
sub_type=self.hotbox_subtype,
|
||||||
status='in_progress',
|
status='in_progress',
|
||||||
|
geom=poly,
|
||||||
end_time=timezone.now()
|
end_time=timezone.now()
|
||||||
)
|
)
|
||||||
InterventionLocation.objects.create(
|
|
||||||
intervention=intervention,
|
|
||||||
content_type=ContentType.objects.get_for_model(muni),
|
|
||||||
object_id=muni.pk
|
|
||||||
)
|
|
||||||
|
|
||||||
intervention.status = 'finished'
|
intervention.status = 'finished'
|
||||||
intervention.save()
|
intervention.save()
|
||||||
|
|
||||||
insp = Inspection.objects.get(checked_intervention=intervention)
|
insp = Inspection.objects.get(checked_intervention=intervention)
|
||||||
self.assertEqual(insp.zone, zone)
|
self.assertEqual(insp.assigned_team, team)
|
||||||
# Inspecteur unique pré-assigné
|
self.assertEqual(insp.assigned_member, member)
|
||||||
|
# Inspecteur unique pré-assigné via le membre
|
||||||
self.assertEqual(insp.inspector, self.user)
|
self.assertEqual(insp.inspector, self.user)
|
||||||
self.assertTrue(insp.can_edit(self.user))
|
self.assertTrue(insp.can_edit(self.user))
|
||||||
|
|
||||||
|
def test_auto_assignment_fallback_calendar_when_no_geometry(self):
|
||||||
|
"""Si aucune géométrie n'est définie, fallback sur le calendrier d'assignation."""
|
||||||
|
from datetime import timedelta
|
||||||
|
from contracts.models import Company, CompanyTeam, CompanyMember
|
||||||
|
from contracts.services_teams import set_member_team_schedule
|
||||||
|
|
||||||
|
company = Company.objects.create(name="Voirie Fallback Test")
|
||||||
|
team = CompanyTeam.objects.create(
|
||||||
|
name="Équipe Sans Zone",
|
||||||
|
company=company,
|
||||||
|
geom=None,
|
||||||
|
operation_types=['inspections'],
|
||||||
|
)
|
||||||
|
team.thematics.add(self.roads_thematic)
|
||||||
|
member = CompanyMember.objects.create(name="Inspecteur Bob", user=self.user)
|
||||||
|
|
||||||
|
target_date = (timezone.now() + timedelta(days=30)).date()
|
||||||
|
set_member_team_schedule(member, team.id, target_date)
|
||||||
|
|
||||||
|
# Intervention SANS géométrie
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.hotbox_subtype,
|
||||||
|
status='in_progress',
|
||||||
|
geom=None,
|
||||||
|
lon=None,
|
||||||
|
lat=None,
|
||||||
|
end_time=timezone.now()
|
||||||
|
)
|
||||||
|
intervention.status = 'finished'
|
||||||
|
intervention.save()
|
||||||
|
|
||||||
|
insp = Inspection.objects.get(checked_intervention=intervention)
|
||||||
|
self.assertEqual(insp.assigned_team, team)
|
||||||
|
self.assertEqual(insp.assigned_member, member)
|
||||||
|
self.assertEqual(insp.inspector, self.user)
|
||||||
|
|
||||||
|
def test_auto_assignment_no_team_assigned_when_no_date_matches(self):
|
||||||
|
"""Si aucune date ne correspond (aucun agent planifié ce jour-là), ne pas assigner d'équipe."""
|
||||||
|
from datetime import timedelta
|
||||||
|
from contracts.models import Company, CompanyTeam, CompanyMember
|
||||||
|
from contracts.services_teams import set_member_team_schedule
|
||||||
|
|
||||||
|
company = Company.objects.create(name="Voirie No Date Test")
|
||||||
|
team = CompanyTeam.objects.create(
|
||||||
|
name="Équipe Dispo Uniquement Demain",
|
||||||
|
company=company,
|
||||||
|
geom=None,
|
||||||
|
operation_types=['inspections'],
|
||||||
|
)
|
||||||
|
team.thematics.add(self.roads_thematic)
|
||||||
|
member = CompanyMember.objects.create(name="Inspecteur Charlie", user=self.user)
|
||||||
|
|
||||||
|
# Planifié demain, mais l'inspection a lieu dans 30 jours
|
||||||
|
tomorrow = (timezone.now() + timedelta(days=1)).date()
|
||||||
|
set_member_team_schedule(member, team.id, tomorrow)
|
||||||
|
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.hotbox_subtype,
|
||||||
|
status='in_progress',
|
||||||
|
geom=None,
|
||||||
|
end_time=timezone.now()
|
||||||
|
)
|
||||||
|
intervention.status = 'finished'
|
||||||
|
intervention.save()
|
||||||
|
|
||||||
|
insp = Inspection.objects.get(checked_intervention=intervention)
|
||||||
|
# Aucune date ne correspond dans 30 jours -> pas d'équipe assignée
|
||||||
|
self.assertIsNone(insp.assigned_team)
|
||||||
|
self.assertIsNone(insp.assigned_member)
|
||||||
|
|
||||||
def test_non_compliance_triggers_team_notification(self):
|
def test_non_compliance_triggers_team_notification(self):
|
||||||
from notifications.models import NotificationRecipient
|
from notifications.models import NotificationRecipient
|
||||||
intervention = Intervention.objects.create(
|
intervention = Intervention.objects.create(
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ import json
|
||||||
from common.models import Thematic
|
from common.models import Thematic
|
||||||
from common.utils import get_short_name
|
from common.utils import get_short_name
|
||||||
from assets.models import AssetCategory
|
from assets.models import AssetCategory
|
||||||
|
from contracts.models import CompanyTeam
|
||||||
from .models import (
|
from .models import (
|
||||||
Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection, InspectionDocument,
|
Inspection, ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection, InspectionDocument,
|
||||||
INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES
|
INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,7 +46,8 @@ def _filter_inspections_queryset(request):
|
||||||
qs = filter_viewable_inspections_for_user(request.user)
|
qs = filter_viewable_inspections_for_user(request.user)
|
||||||
qs = qs.select_related(
|
qs = qs.select_related(
|
||||||
'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention',
|
'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention',
|
||||||
'parkingspotinspection', 'giepinspection', 'signpanelinspection'
|
'parkingspotinspection', 'giepinspection', 'signpanelinspection',
|
||||||
|
'assigned_team', 'assigned_member'
|
||||||
).prefetch_related('documents')
|
).prefetch_related('documents')
|
||||||
|
|
||||||
thematics = request.GET.getlist('thematic')
|
thematics = request.GET.getlist('thematic')
|
||||||
|
|
@ -159,16 +161,16 @@ def _filter_inspections_queryset(request):
|
||||||
if natures:
|
if natures:
|
||||||
qs = qs.filter(nature__in=natures)
|
qs = qs.filter(nature__in=natures)
|
||||||
|
|
||||||
zone_id = request.GET.get('zone') or request.GET.get('zone_id')
|
team_filter = request.GET.get('team') or request.GET.get('team_id') or request.GET.get('zone') or request.GET.get('zone_id')
|
||||||
if zone_id:
|
if team_filter:
|
||||||
qs = qs.filter(zone_id=zone_id)
|
qs = qs.filter(assigned_team_id=team_filter)
|
||||||
|
|
||||||
show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my'
|
show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my'
|
||||||
if show_mine_only:
|
if show_mine_only:
|
||||||
qs = qs.filter(inspector=request.user)
|
qs = qs.filter(inspector=request.user)
|
||||||
|
|
||||||
if request.GET.get('mine_zones') == '1':
|
if request.GET.get('mine_zones') == '1' or request.GET.get('mine_teams') == '1':
|
||||||
qs = qs.filter(Q(zone__inspectors=request.user) | Q(inspector=request.user))
|
qs = qs.filter(Q(assigned_team__members__user=request.user) | Q(inspector=request.user))
|
||||||
|
|
||||||
timeliness = request.GET.get('timeliness')
|
timeliness = request.GET.get('timeliness')
|
||||||
if timeliness:
|
if timeliness:
|
||||||
|
|
@ -224,7 +226,7 @@ def inspections_list(request):
|
||||||
|
|
||||||
# Compteurs rapides pour les onglets
|
# Compteurs rapides pour les onglets
|
||||||
scheduled_count = base_viewable.filter(status='scheduled').filter(
|
scheduled_count = base_viewable.filter(status='scheduled').filter(
|
||||||
Q(inspector=request.user) | Q(zone__inspectors=request.user) | Q(inspector__isnull=True)
|
Q(inspector=request.user) | Q(assigned_team__members__user=request.user) | Q(inspector__isnull=True)
|
||||||
).count()
|
).count()
|
||||||
my_count = base_viewable.filter(inspector=request.user).exclude(status='scheduled').count()
|
my_count = base_viewable.filter(inspector=request.user).exclude(status='scheduled').count()
|
||||||
|
|
||||||
|
|
@ -233,20 +235,20 @@ def inspections_list(request):
|
||||||
thematic_code = request.GET.get('thematic', '')
|
thematic_code = request.GET.get('thematic', '')
|
||||||
status_filter = request.GET.get('status', '')
|
status_filter = request.GET.get('status', '')
|
||||||
nature_filter = request.GET.get('nature', '')
|
nature_filter = request.GET.get('nature', '')
|
||||||
zone_filter = request.GET.get('zone', '')
|
zone_filter = request.GET.get('team', '') or request.GET.get('zone', '')
|
||||||
mission_id = request.GET.get('mission_id', '')
|
mission_id = request.GET.get('mission_id', '')
|
||||||
checked_interv_id = request.GET.get('checked_interv_id', '')
|
checked_interv_id = request.GET.get('checked_interv_id', '')
|
||||||
search_q = request.GET.get('q', '').strip()
|
search_q = request.GET.get('q', '').strip()
|
||||||
|
|
||||||
qs = base_viewable.select_related(
|
qs = base_viewable.select_related(
|
||||||
'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention',
|
'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention',
|
||||||
'zone', 'parkingspotinspection', 'giepinspection', 'signpanelinspection'
|
'assigned_team', 'assigned_member', 'parkingspotinspection', 'giepinspection', 'signpanelinspection'
|
||||||
).prefetch_related('documents')
|
).prefetch_related('documents')
|
||||||
|
|
||||||
if filter_type == 'scheduled':
|
if filter_type == 'scheduled':
|
||||||
qs = qs.filter(status='scheduled')
|
qs = qs.filter(status='scheduled')
|
||||||
if request.GET.get('all_zones') != '1':
|
if request.GET.get('all_zones') != '1':
|
||||||
qs = qs.filter(Q(inspector=request.user) | Q(zone__inspectors=request.user) | Q(inspector__isnull=True))
|
qs = qs.filter(Q(inspector=request.user) | Q(assigned_team__members__user=request.user) | Q(inspector__isnull=True))
|
||||||
qs = qs.order_by('scheduled_due_date', 'scheduled_start_date', '-id')
|
qs = qs.order_by('scheduled_due_date', 'scheduled_start_date', '-id')
|
||||||
elif filter_type == 'my':
|
elif filter_type == 'my':
|
||||||
qs = qs.filter(inspector=request.user).exclude(status='scheduled').order_by('-inspection_date', '-id')
|
qs = qs.filter(inspector=request.user).exclude(status='scheduled').order_by('-inspection_date', '-id')
|
||||||
|
|
@ -254,7 +256,7 @@ def inspections_list(request):
|
||||||
qs = qs.order_by('-inspection_date', '-id')
|
qs = qs.order_by('-inspection_date', '-id')
|
||||||
|
|
||||||
if zone_filter:
|
if zone_filter:
|
||||||
qs = qs.filter(zone_id=zone_filter)
|
qs = qs.filter(assigned_team_id=zone_filter)
|
||||||
|
|
||||||
if thematic_code:
|
if thematic_code:
|
||||||
try:
|
try:
|
||||||
|
|
@ -327,9 +329,8 @@ def inspections_list(request):
|
||||||
|
|
||||||
qs = qs.filter(search_filter)
|
qs = qs.filter(search_filter)
|
||||||
|
|
||||||
zones = InspectionZone.objects.filter(is_active=True).order_by('thematic__name_fr', 'name_fr')
|
all_teams = CompanyTeam.objects.filter(geom__isnull=False).select_related('company').order_by('name')
|
||||||
if thematics.exists():
|
zones = [t for t in all_teams if t.handles_operation(CompanyTeam.OPERATION_INSPECTIONS)]
|
||||||
zones = zones.filter(thematic__in=thematics)
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'base_template': "mobile/mobile_base.html",
|
'base_template': "mobile/mobile_base.html",
|
||||||
|
|
@ -403,9 +404,8 @@ def inspections_list(request):
|
||||||
for cfg in result_choices_config
|
for cfg in result_choices_config
|
||||||
]
|
]
|
||||||
|
|
||||||
zones = InspectionZone.objects.filter(is_active=True).order_by('thematic__name_fr', 'name_fr')
|
all_teams = CompanyTeam.objects.filter(geom__isnull=False).select_related('company').order_by('name')
|
||||||
if thematics.exists():
|
zones = [t for t in all_teams if t.handles_operation(CompanyTeam.OPERATION_INSPECTIONS)]
|
||||||
zones = zones.filter(thematic__in=thematics)
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'thematic_choices': [(t.code, t.name, t.icon or 'bi-geo-alt') for t in thematics],
|
'thematic_choices': [(t.code, t.name, t.icon or 'bi-geo-alt') for t in thematics],
|
||||||
|
|
@ -599,6 +599,20 @@ def create_inspection_api(request):
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Résolution de l'équipe assignée et du membre si non spécifiés
|
||||||
|
assigned_team = None
|
||||||
|
assigned_member = None
|
||||||
|
team_id_raw = request.POST.get('assigned_team_id') or request.POST.get('team_id')
|
||||||
|
if team_id_raw:
|
||||||
|
assigned_team = CompanyTeam.objects.filter(pk=team_id_raw).first()
|
||||||
|
if not assigned_team:
|
||||||
|
from .services import resolve_team_and_member_for_inspection
|
||||||
|
target_geom = getattr(target_obj, 'geom', None)
|
||||||
|
assigned_team, assigned_member = resolve_team_and_member_for_inspection(
|
||||||
|
target_geom=target_geom,
|
||||||
|
scheduled_date=timezone.now().date(),
|
||||||
|
)
|
||||||
|
|
||||||
if is_parking_spot:
|
if is_parking_spot:
|
||||||
inspection = ParkingSpotInspection(
|
inspection = ParkingSpotInspection(
|
||||||
asset_content_type=content_type,
|
asset_content_type=content_type,
|
||||||
|
|
@ -613,6 +627,8 @@ def create_inspection_api(request):
|
||||||
verified_obsolescence=verified_obsolescence,
|
verified_obsolescence=verified_obsolescence,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
apply_changes_to_asset=apply_changes,
|
apply_changes_to_asset=apply_changes,
|
||||||
|
assigned_team=assigned_team,
|
||||||
|
assigned_member=assigned_member,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'verified_standard_stand_count' in request.POST and request.POST['verified_standard_stand_count'] != '':
|
if 'verified_standard_stand_count' in request.POST and request.POST['verified_standard_stand_count'] != '':
|
||||||
|
|
@ -665,6 +681,8 @@ def create_inspection_api(request):
|
||||||
date_derniere_pluie=date_derniere_pluie,
|
date_derniere_pluie=date_derniere_pluie,
|
||||||
orage_moins_une_heure=orage_moins_une_heure,
|
orage_moins_une_heure=orage_moins_une_heure,
|
||||||
data=parsed_giep_data,
|
data=parsed_giep_data,
|
||||||
|
assigned_team=assigned_team,
|
||||||
|
assigned_member=assigned_member,
|
||||||
)
|
)
|
||||||
inspection.save()
|
inspection.save()
|
||||||
elif is_signpanel:
|
elif is_signpanel:
|
||||||
|
|
@ -726,7 +744,10 @@ def create_inspection_api(request):
|
||||||
ai_detected_code=ai_code,
|
ai_detected_code=ai_code,
|
||||||
ai_confidence=ai_conf,
|
ai_confidence=ai_conf,
|
||||||
apply_sign_updates_to_asset=apply_changes,
|
apply_sign_updates_to_asset=apply_changes,
|
||||||
|
assigned_team=assigned_team,
|
||||||
|
assigned_member=assigned_member,
|
||||||
)
|
)
|
||||||
|
inspection.save()
|
||||||
elif is_hotbox:
|
elif is_hotbox:
|
||||||
joint_level = request.POST.get('joint_level') or None
|
joint_level = request.POST.get('joint_level') or None
|
||||||
joint_sealing = request.POST.get('joint_sealing') or None
|
joint_sealing = request.POST.get('joint_sealing') or None
|
||||||
|
|
@ -756,6 +777,8 @@ def create_inspection_api(request):
|
||||||
joint_sealing=joint_sealing,
|
joint_sealing=joint_sealing,
|
||||||
cracking=cracking,
|
cracking=cracking,
|
||||||
water_drainage=water_drainage,
|
water_drainage=water_drainage,
|
||||||
|
assigned_team=assigned_team,
|
||||||
|
assigned_member=assigned_member,
|
||||||
)
|
)
|
||||||
inspection.save()
|
inspection.save()
|
||||||
else:
|
else:
|
||||||
|
|
@ -772,6 +795,8 @@ def create_inspection_api(request):
|
||||||
verified_obsolescence=verified_obsolescence,
|
verified_obsolescence=verified_obsolescence,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
apply_changes_to_asset=apply_changes,
|
apply_changes_to_asset=apply_changes,
|
||||||
|
assigned_team=assigned_team,
|
||||||
|
assigned_member=assigned_member,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enregistrement des photos et documents joints
|
# Enregistrement des photos et documents joints
|
||||||
|
|
@ -898,8 +923,10 @@ def get_inspection_detail_api(request, inspection_id):
|
||||||
'is_scheduled': leaf.is_scheduled,
|
'is_scheduled': leaf.is_scheduled,
|
||||||
'scheduled_start_date': leaf.scheduled_start_date.strftime('%d/%m/%Y') if leaf.scheduled_start_date else '',
|
'scheduled_start_date': leaf.scheduled_start_date.strftime('%d/%m/%Y') if leaf.scheduled_start_date else '',
|
||||||
'scheduled_due_date': leaf.scheduled_due_date.strftime('%d/%m/%Y') if leaf.scheduled_due_date else '',
|
'scheduled_due_date': leaf.scheduled_due_date.strftime('%d/%m/%Y') if leaf.scheduled_due_date else '',
|
||||||
'zone_id': leaf.zone_id,
|
'assigned_team_id': leaf.assigned_team_id,
|
||||||
'zone_name': leaf.zone.get_name() if leaf.zone else '',
|
'assigned_team_name': leaf.assigned_team.name if leaf.assigned_team else '',
|
||||||
|
'zone_id': leaf.assigned_team_id,
|
||||||
|
'zone_name': leaf.assigned_team.name if leaf.assigned_team else '',
|
||||||
'timeliness': leaf.timeliness,
|
'timeliness': leaf.timeliness,
|
||||||
'timeliness_label': str(leaf.timeliness_label) if leaf.timeliness_label else '',
|
'timeliness_label': str(leaf.timeliness_label) if leaf.timeliness_label else '',
|
||||||
'timeliness_badge_class': leaf.timeliness_badge_class,
|
'timeliness_badge_class': leaf.timeliness_badge_class,
|
||||||
|
|
|
||||||
18
loko/sign/migrations/0005_alter_signpole_support_type.py
Normal file
18
loko/sign/migrations/0005_alter_signpole_support_type.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 08:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sign', '0004_signpanel_support_model_signpanel_trafficlight_pole_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='signpole',
|
||||||
|
name='support_type',
|
||||||
|
field=models.CharField(blank=True, choices=[('pole', 'Poteau'), ('lightingpole', "Poteau d'éclairage"), ('trafficlightpole', 'Poteau de signalisation lumineuse'), ('facade', 'Façade'), ('mast', 'Mât'), ('portico', 'Portique'), ('gallows', 'Potence'), ('hopper', 'Trémie'), ('other', 'Autre')], default='pole', max_length=50, verbose_name='Type de support'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Loading…
Reference in a new issue