feat: add intervention control team assignment, contract auto-assignment, and controller shortcuts dashboard widget
This commit is contained in:
parent
1ab3c71754
commit
74c1bf2447
36 changed files with 955 additions and 16 deletions
|
|
@ -126,9 +126,9 @@ class CompanyAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Contract)
|
@admin.register(Contract)
|
||||||
class ContractAdmin(admin.ModelAdmin):
|
class ContractAdmin(admin.ModelAdmin):
|
||||||
list_display = ('contract_number', 'description', 'company', 'is_blocked', 'is_active', 'start_date', 'end_date', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'thematics_list')
|
list_display = ('contract_number', 'description', 'company', 'is_blocked', 'is_active', 'start_date', 'end_date', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'thematics_list')
|
||||||
search_fields = ('contract_number', 'company__name', 'description', 'contact_phone')
|
search_fields = ('contract_number', 'company__name', 'description', 'contact_phone')
|
||||||
list_filter = ('is_blocked', 'is_active', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'send_daily_summary')
|
list_filter = ('is_blocked', 'is_active', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'send_daily_summary')
|
||||||
filter_horizontal = ('thematics',)
|
filter_horizontal = ('thematics',)
|
||||||
|
|
||||||
def get_inlines(self, request, obj=None):
|
def get_inlines(self, request, obj=None):
|
||||||
|
|
@ -154,7 +154,7 @@ class ContractAdmin(admin.ModelAdmin):
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
('Options de workflow', {
|
('Options de workflow', {
|
||||||
'fields': ('needs_approval', 'approval_except_urgent', 'needs_checking', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'warn_if_no_prestation_on_processed', 'allow_team_members_edit'),
|
'fields': ('needs_approval', 'approval_except_urgent', 'needs_checking', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'warn_if_no_prestation_on_processed', 'allow_team_members_edit', 'auto_assign_control_team'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2362,3 +2362,12 @@ msgstr "Target date cannot be in the past."
|
||||||
|
|
||||||
#~ msgid "Confirmer le changement immédiat"
|
#~ msgid "Confirmer le changement immédiat"
|
||||||
#~ msgstr "Confirm immediate change"
|
#~ msgstr "Confirm immediate change"
|
||||||
|
|
||||||
|
#: contracts/models.py:435
|
||||||
|
msgid "Assigner automatiquement une équipe de contrôle"
|
||||||
|
msgstr "Automatically assign a control team"
|
||||||
|
|
||||||
|
#: contracts/models.py:436
|
||||||
|
msgid "Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat."
|
||||||
|
msgstr "If enabled, a control team is automatically assigned to corrective interventions for this contract."
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2412,3 +2412,12 @@ msgstr "La date cible ne peut pas être dans le passé."
|
||||||
|
|
||||||
#~ msgid "Confirmer le changement immédiat"
|
#~ msgid "Confirmer le changement immédiat"
|
||||||
#~ msgstr "Confirmer le changement immédiat"
|
#~ msgstr "Confirmer le changement immédiat"
|
||||||
|
|
||||||
|
#: contracts/models.py:435
|
||||||
|
msgid "Assigner automatiquement une équipe de contrôle"
|
||||||
|
msgstr "Assigner automatiquement une équipe de contrôle"
|
||||||
|
|
||||||
|
#: contracts/models.py:436
|
||||||
|
msgid "Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat."
|
||||||
|
msgstr "Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat."
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2368,3 +2368,12 @@ msgstr "De doeldatum kan niet in het verleden liggen."
|
||||||
|
|
||||||
#~ msgid "Confirmer le changement immédiat"
|
#~ msgid "Confirmer le changement immédiat"
|
||||||
#~ msgstr "Onmiddellijke wijziging bevestigen"
|
#~ msgstr "Onmiddellijke wijziging bevestigen"
|
||||||
|
|
||||||
|
#: contracts/models.py:435
|
||||||
|
msgid "Assigner automatiquement une équipe de contrôle"
|
||||||
|
msgstr "Automatisch een controleteam toewijzen"
|
||||||
|
|
||||||
|
#: contracts/models.py:436
|
||||||
|
msgid "Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat."
|
||||||
|
msgstr "Indien geactiveerd wordt automatisch een controleteam toegewezen aan de correctieve interventies van dit contract."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 11:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contracts', '0008_companyteam_geom_companyteam_operation_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contract',
|
||||||
|
name='auto_assign_control_team',
|
||||||
|
field=models.BooleanField(default=False, help_text='Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat.', verbose_name='Assigner automatiquement une équipe de contrôle'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -432,6 +432,11 @@ class Contract(models.Model):
|
||||||
verbose_name=_("Modification inter-membres d'équipe autorisée"),
|
verbose_name=_("Modification inter-membres d'équipe autorisée"),
|
||||||
help_text=_("Si activé, les membres d'une même équipe peuvent modifier les interventions assignées à d'autres membres de leur équipe, y compris à partir du statut 'Pris en charge' ou au-delà."),
|
help_text=_("Si activé, les membres d'une même équipe peuvent modifier les interventions assignées à d'autres membres de leur équipe, y compris à partir du statut 'Pris en charge' ou au-delà."),
|
||||||
)
|
)
|
||||||
|
auto_assign_control_team = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("Assigner automatiquement une équipe de contrôle"),
|
||||||
|
help_text=_("Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat."),
|
||||||
|
)
|
||||||
is_blocked = models.BooleanField(
|
is_blocked = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("Marché bloqué / gelé"),
|
verbose_name=_("Marché bloqué / gelé"),
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -656,10 +656,11 @@ msgstr ""
|
||||||
msgid "En pause"
|
msgid "En pause"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:36
|
||||||
#: home/templates/home/widgets/external_interventions.html:84
|
#: home/templates/home/widgets/external_interventions.html:84
|
||||||
#: home/templates/home/widgets/external_interventions.html:85
|
#: home/templates/home/widgets/external_interventions.html:85
|
||||||
msgid "À corriger"
|
msgid "À corriger"
|
||||||
msgstr ""
|
msgstr "To be corrected"
|
||||||
|
|
||||||
#: home/templates/home/widgets/external_interventions.html:94
|
#: home/templates/home/widgets/external_interventions.html:94
|
||||||
msgid "10 dernières interventions"
|
msgid "10 dernières interventions"
|
||||||
|
|
@ -1111,3 +1112,28 @@ msgstr "Loaned items"
|
||||||
#: home/templates/home/widgets/stock_summary.html:235
|
#: home/templates/home/widgets/stock_summary.html:235
|
||||||
msgid "Aucun article en prêt en cours"
|
msgid "Aucun article en prêt en cours"
|
||||||
msgstr "No active loaned items"
|
msgstr "No active loaned items"
|
||||||
|
|
||||||
|
#: home/models.py:38
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:11
|
||||||
|
msgid "Raccourcis contrôleur"
|
||||||
|
msgstr "Controller shortcuts"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:15
|
||||||
|
msgid "Interventions à contrôler"
|
||||||
|
msgstr "Interventions to control"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:18
|
||||||
|
msgid "en attente de contrôle"
|
||||||
|
msgstr "pending control"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:43
|
||||||
|
msgid "Validées"
|
||||||
|
msgstr "Validated"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:64
|
||||||
|
msgid "Aucune intervention à contrôler"
|
||||||
|
msgstr "No interventions to control"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:72
|
||||||
|
msgid "Voir toutes les interventions à contrôler"
|
||||||
|
msgstr "View all interventions to control"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -665,10 +665,11 @@ msgstr ""
|
||||||
msgid "En pause"
|
msgid "En pause"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:36
|
||||||
#: home/templates/home/widgets/external_interventions.html:84
|
#: home/templates/home/widgets/external_interventions.html:84
|
||||||
#: home/templates/home/widgets/external_interventions.html:85
|
#: home/templates/home/widgets/external_interventions.html:85
|
||||||
msgid "À corriger"
|
msgid "À corriger"
|
||||||
msgstr ""
|
msgstr "À corriger"
|
||||||
|
|
||||||
#: home/templates/home/widgets/external_interventions.html:94
|
#: home/templates/home/widgets/external_interventions.html:94
|
||||||
msgid "10 dernières interventions"
|
msgid "10 dernières interventions"
|
||||||
|
|
@ -1120,3 +1121,28 @@ msgstr "Articles en prêt"
|
||||||
#: home/templates/home/widgets/stock_summary.html:235
|
#: home/templates/home/widgets/stock_summary.html:235
|
||||||
msgid "Aucun article en prêt en cours"
|
msgid "Aucun article en prêt en cours"
|
||||||
msgstr "Aucun article en prêt en cours"
|
msgstr "Aucun article en prêt en cours"
|
||||||
|
|
||||||
|
#: home/models.py:38
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:11
|
||||||
|
msgid "Raccourcis contrôleur"
|
||||||
|
msgstr "Raccourcis contrôleur"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:15
|
||||||
|
msgid "Interventions à contrôler"
|
||||||
|
msgstr "Interventions à contrôler"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:18
|
||||||
|
msgid "en attente de contrôle"
|
||||||
|
msgstr "en attente de contrôle"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:43
|
||||||
|
msgid "Validées"
|
||||||
|
msgstr "Validées"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:64
|
||||||
|
msgid "Aucune intervention à contrôler"
|
||||||
|
msgstr "Aucune intervention à contrôler"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:72
|
||||||
|
msgid "Voir toutes les interventions à contrôler"
|
||||||
|
msgstr "Voir toutes les interventions à contrôler"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -662,10 +662,11 @@ msgstr ""
|
||||||
msgid "En pause"
|
msgid "En pause"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:36
|
||||||
#: home/templates/home/widgets/external_interventions.html:84
|
#: home/templates/home/widgets/external_interventions.html:84
|
||||||
#: home/templates/home/widgets/external_interventions.html:85
|
#: home/templates/home/widgets/external_interventions.html:85
|
||||||
msgid "À corriger"
|
msgid "À corriger"
|
||||||
msgstr ""
|
msgstr "Te corrigeren"
|
||||||
|
|
||||||
#: home/templates/home/widgets/external_interventions.html:94
|
#: home/templates/home/widgets/external_interventions.html:94
|
||||||
msgid "10 dernières interventions"
|
msgid "10 dernières interventions"
|
||||||
|
|
@ -1117,3 +1118,28 @@ msgstr "Uitgeleende artikelen"
|
||||||
#: home/templates/home/widgets/stock_summary.html:235
|
#: home/templates/home/widgets/stock_summary.html:235
|
||||||
msgid "Aucun article en prêt en cours"
|
msgid "Aucun article en prêt en cours"
|
||||||
msgstr "Geen actieve uitgeleende artikelen"
|
msgstr "Geen actieve uitgeleende artikelen"
|
||||||
|
|
||||||
|
#: home/models.py:38
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:11
|
||||||
|
msgid "Raccourcis contrôleur"
|
||||||
|
msgstr "Snelkoppelingen controleur"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:15
|
||||||
|
msgid "Interventions à contrôler"
|
||||||
|
msgstr "Te controleren interventies"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:18
|
||||||
|
msgid "en attente de contrôle"
|
||||||
|
msgstr "in afwachting van controle"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:43
|
||||||
|
msgid "Validées"
|
||||||
|
msgstr "Gevalideerd"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:64
|
||||||
|
msgid "Aucune intervention à contrôler"
|
||||||
|
msgstr "Geen te controleren interventies"
|
||||||
|
|
||||||
|
#: home/templates/home/widgets/controller_shortcuts.html:72
|
||||||
|
msgid "Voir toutes les interventions à contrôler"
|
||||||
|
msgstr "Alle te controleren interventies bekijken"
|
||||||
|
|
|
||||||
18
loko/home/migrations/0007_alter_homewidget_widget_code.py
Normal file
18
loko/home/migrations/0007_alter_homewidget_widget_code.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 11:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0006_create_exceptional_transport_widget'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='homewidget',
|
||||||
|
name='widget_code',
|
||||||
|
field=models.CharField(choices=[('intervention_stats', 'Statistiques interventions'), ('contracts_summary', 'Résumé contrats'), ('map_today', 'Carte du jour'), ('operator_shortcuts', 'Raccourcis opérateur'), ('assets_links', 'Liens assets'), ('stock_summary', 'Résumé stock'), ('observer_shortcuts', 'Raccourcis observateur'), ('incident_counter', 'Compteur incidents à traiter'), ('intervention_to_verify', 'Compteur interventions à vérifier'), ('intervention_to_plan', 'Compteur interventions à planifier'), ('intervention_to_approve', 'Compteur interventions à approuver'), ('intervention_checked_to_approve', 'Compteur interventions vérifiées à approuver'), ('intervention_finished', 'Compteur interventions terminées'), ('intervention_on_pause', 'Compteur interventions en pause'), ('external_interventions', 'Interventions externes'), ('stock_actions', 'Actions stock'), ('exceptional_transport', 'Transport exceptionnel'), ('controller_shortcuts', 'Raccourcis contrôleur')], max_length=50, unique=True, verbose_name='Code du widget'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_controller_shortcuts_widget(apps, schema_editor):
|
||||||
|
HomeWidget = apps.get_model('home', 'HomeWidget')
|
||||||
|
HomeWidgetConditionGroup = apps.get_model('home', 'HomeWidgetConditionGroup')
|
||||||
|
HomeWidgetCondition = apps.get_model('home', 'HomeWidgetCondition')
|
||||||
|
Role = apps.get_model('common', 'Role')
|
||||||
|
|
||||||
|
widget, created = HomeWidget.objects.get_or_create(
|
||||||
|
widget_code='controller_shortcuts',
|
||||||
|
defaults={'is_active': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not widget.condition_groups.exists():
|
||||||
|
controller_role = Role.objects.filter(name='controller').first()
|
||||||
|
if controller_role:
|
||||||
|
group = HomeWidgetConditionGroup.objects.create(
|
||||||
|
widget=widget,
|
||||||
|
label="Contrôleurs",
|
||||||
|
order=15,
|
||||||
|
col_width=4,
|
||||||
|
priority=0,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
HomeWidgetCondition.objects.create(
|
||||||
|
group=group,
|
||||||
|
condition_type='role',
|
||||||
|
role=controller_role,
|
||||||
|
is_active=True,
|
||||||
|
is_negated=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_controller_shortcuts_widget(apps, schema_editor):
|
||||||
|
HomeWidget = apps.get_model('home', 'HomeWidget')
|
||||||
|
HomeWidget.objects.filter(widget_code='controller_shortcuts').delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0007_alter_homewidget_widget_code'),
|
||||||
|
('common', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_controller_shortcuts_widget, remove_controller_shortcuts_widget),
|
||||||
|
]
|
||||||
|
|
@ -21,6 +21,7 @@ WIDGET_CODE_CHOICES = [
|
||||||
('external_interventions', _('Interventions externes')),
|
('external_interventions', _('Interventions externes')),
|
||||||
('stock_actions', _('Actions stock')),
|
('stock_actions', _('Actions stock')),
|
||||||
('exceptional_transport', _('Transport exceptionnel')),
|
('exceptional_transport', _('Transport exceptionnel')),
|
||||||
|
('controller_shortcuts', _('Raccourcis contrôleur')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Condition type choices
|
# Condition type choices
|
||||||
|
|
|
||||||
72
loko/home/templates/home/widgets/controller_shortcuts.html
Normal file
72
loko/home/templates/home/widgets/controller_shortcuts.html
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{# ========== Widget raccourcis pour les contrôleurs ========== #}
|
||||||
|
<div class="card shadow-sm h-100" style="border-color: #00897b !important;">
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between text-white" style="background-color: #00897b !important;">
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}?status=processed&status=corrected"
|
||||||
|
class="text-decoration-none fw-bold text-uppercase text-white d-flex align-items-center" style="letter-spacing:.05em;">
|
||||||
|
<i class="bi bi-patch-check-fill me-2 fs-5"></i>
|
||||||
|
<span>{% translate "Interventions à contrôler" %}</span>
|
||||||
|
</a>
|
||||||
|
<span class="badge bg-white text-dark rounded-pill px-2 py-1 small fw-semibold">
|
||||||
|
{{ controller_interventions_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# --- Compteur principal et sous-raccourcis --- #}
|
||||||
|
<div class="text-center py-2 border-bottom bg-light bg-opacity-25">
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}?status=processed&status=corrected"
|
||||||
|
class="text-decoration-none d-block">
|
||||||
|
<span class="display-5 fw-bold" style="color: #00897b !important;">{{ controller_interventions_count }}</span>
|
||||||
|
<div class="small text-muted fw-semibold">{% translate "en attente de contrôle" %}</div>
|
||||||
|
</a>
|
||||||
|
<div class="d-flex justify-content-center gap-2 mt-2">
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}?status=to_be_corrected"
|
||||||
|
class="badge text-decoration-none bg-warning-subtle text-warning-emphasis border border-warning-subtle">
|
||||||
|
<i class="bi bi-arrow-repeat me-1"></i>{% translate "À corriger" %} ({{ controller_to_correct_count|default:0 }})
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}?status=validated"
|
||||||
|
class="badge text-decoration-none bg-success-subtle text-success-emphasis border border-success-subtle">
|
||||||
|
<i class="bi bi-check-all me-1"></i>{% translate "Validées" %} ({{ controller_validated_count|default:0 }})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# --- Liste des 5 premières interventions --- #}
|
||||||
|
<ul class="list-group list-group-flush flex-grow-1">
|
||||||
|
{% for itv in controller_interventions_list %}
|
||||||
|
<li class="list-group-item px-2 py-1 small">
|
||||||
|
<a href="{% url 'interventions:intervention_detail' itv.id %}" class="text-decoration-none d-block">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<span class="fw-semibold text-truncate me-2" style="max-width:70%; color: #00897b !important;">
|
||||||
|
{{ itv.code }}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted text-nowrap" style="font-size:.75rem;">
|
||||||
|
{{ itv.last_modified_time|date:"d/m H:i"|default:itv.creation_time|date:"d/m H:i" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||||
|
<span class="text-muted text-truncate" style="font-size:.75rem; max-width: 65%;">
|
||||||
|
{% if itv.title %}{{ itv.title|truncatechars:45 }}{% endif %}
|
||||||
|
{% if itv.thematic %} · <span class="badge bg-secondary-subtle text-secondary py-0 px-1">{{ itv.thematic.code }}</span>{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="badge {% if itv.status == 'corrected' %}bg-info-subtle text-info-emphasis border border-info-subtle{% else %}bg-primary-subtle text-primary-emphasis border border-primary-subtle{% endif %} py-0 px-1" style="font-size:.7rem;">
|
||||||
|
{{ itv.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="list-group-item text-center text-muted py-3 small">
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i>{% translate "Aucune intervention à contrôler" %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="card-footer text-center text-muted small bg-light">
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}?status=processed&status=corrected"
|
||||||
|
class="text-decoration-none" style="color: #00897b !important; font-weight: 500;">
|
||||||
|
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir toutes les interventions à contrôler" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -3,7 +3,7 @@ from django.test import TestCase
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from common.models import Role, Thematic, UserConfig, UserThematics
|
from common.models import Role, Thematic, UserConfig, UserThematics
|
||||||
from contracts.models import Company, CompanyTeam, Contract
|
from contracts.models import Company, CompanyTeam, Contract, CompanyMember
|
||||||
from home.models import HomeWidget, HomeWidgetConditionGroup, HomeWidgetCondition
|
from home.models import HomeWidget, HomeWidgetConditionGroup, HomeWidgetCondition
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -370,5 +370,75 @@ class HomeWidgetAdminTests(TestCase):
|
||||||
self.assertIn('Group 1', html)
|
self.assertIn('Group 1', html)
|
||||||
|
|
||||||
|
|
||||||
|
class ControllerShortcutsWidgetTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.user = User.objects.create_user('ctrl_user', password='pwd')
|
||||||
|
self.user_config = UserConfig.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
is_intern=True,
|
||||||
|
limit_interventions_to_contracts=False
|
||||||
|
)
|
||||||
|
self.role_ctrl, _ = Role.objects.get_or_create(name='controller')
|
||||||
|
self.user_config.roles.add(self.role_ctrl)
|
||||||
|
|
||||||
|
self.thematic, _ = Thematic.objects.get_or_create(code='structures', defaults={'name_fr': 'Structures'})
|
||||||
|
self.company = Company.objects.create(name='Société Contrôle')
|
||||||
|
self.team = CompanyTeam.objects.create(
|
||||||
|
name='Équipe Contrôle Test',
|
||||||
|
company=self.company,
|
||||||
|
operation_types=[CompanyTeam.OPERATION_CONTROLS]
|
||||||
|
)
|
||||||
|
self.member = CompanyMember.objects.create(user=self.user, name='Membre Contrôleur')
|
||||||
|
self.member.teams.add(self.team)
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
self.reverse = reverse
|
||||||
|
|
||||||
|
def test_controller_shortcuts_widget_context_and_rendering(self):
|
||||||
|
from interventions.models import Intervention
|
||||||
|
itv_processed = Intervention.objects.create(
|
||||||
|
code='ITV-CTRL-001',
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='processed',
|
||||||
|
assigned_control_team=self.team,
|
||||||
|
assigned_controller=self.member,
|
||||||
|
)
|
||||||
|
itv_corrected = Intervention.objects.create(
|
||||||
|
code='ITV-CTRL-002',
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='corrected',
|
||||||
|
assigned_control_team=self.team,
|
||||||
|
assigned_controller=self.member,
|
||||||
|
)
|
||||||
|
itv_in_progress = Intervention.objects.create(
|
||||||
|
code='ITV-CTRL-003',
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_progress',
|
||||||
|
assigned_control_team=self.team,
|
||||||
|
assigned_controller=self.member,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(self.reverse('home:index'), HTTP_ACCEPT_LANGUAGE='fr')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Context counts
|
||||||
|
self.assertEqual(response.context['controller_interventions_count'], 2)
|
||||||
|
ctrl_codes = [itv.code for itv in response.context['controller_interventions_list']]
|
||||||
|
self.assertIn('ITV-CTRL-001', ctrl_codes)
|
||||||
|
self.assertIn('ITV-CTRL-002', ctrl_codes)
|
||||||
|
self.assertNotIn('ITV-CTRL-003', ctrl_codes)
|
||||||
|
|
||||||
|
# Content rendered in HTML
|
||||||
|
self.assertContains(response, 'Interventions à contrôler')
|
||||||
|
self.assertContains(response, 'ITV-CTRL-001')
|
||||||
|
self.assertContains(response, 'ITV-CTRL-002')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -654,6 +654,42 @@ def index(request):
|
||||||
total_finished_count = interventions.filter(status='finished').count()
|
total_finished_count = interventions.filter(status='finished').count()
|
||||||
total_on_pause_count = interventions.filter(status='on_pause').count()
|
total_on_pause_count = interventions.filter(status='on_pause').count()
|
||||||
|
|
||||||
|
# ========== Données widget controller_shortcuts ==========
|
||||||
|
user_role_names = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
||||||
|
controller_filter = Q(status__in=['processed', 'corrected'])
|
||||||
|
if user_teams and user_teams.exists():
|
||||||
|
control_team_ids = list(user_teams.values_list('id', flat=True))
|
||||||
|
controller_q = (
|
||||||
|
Q(assigned_control_team_id__in=control_team_ids) |
|
||||||
|
Q(assigned_controller__user=request.user)
|
||||||
|
)
|
||||||
|
if not (user_role_names & {'admin', 'top_manager', 'manager'}):
|
||||||
|
controller_filter &= controller_q
|
||||||
|
|
||||||
|
controller_interventions_qs = interventions.filter(controller_filter)
|
||||||
|
controller_interventions_count = controller_interventions_qs.count()
|
||||||
|
controller_interventions_list = list(
|
||||||
|
controller_interventions_qs
|
||||||
|
.select_related('thematic', 'contract', 'assigned_control_team', 'assigned_team')
|
||||||
|
.order_by('-code')[:5]
|
||||||
|
)
|
||||||
|
|
||||||
|
controller_to_correct_qs = interventions.filter(status='to_be_corrected')
|
||||||
|
if user_teams and user_teams.exists() and not (user_role_names & {'admin', 'top_manager', 'manager'}):
|
||||||
|
controller_to_correct_qs = controller_to_correct_qs.filter(
|
||||||
|
Q(assigned_control_team_id__in=control_team_ids) |
|
||||||
|
Q(assigned_controller__user=request.user)
|
||||||
|
)
|
||||||
|
controller_to_correct_count = controller_to_correct_qs.count()
|
||||||
|
|
||||||
|
controller_validated_qs = interventions.filter(status='validated')
|
||||||
|
if user_teams and user_teams.exists() and not (user_role_names & {'admin', 'top_manager', 'manager'}):
|
||||||
|
controller_validated_qs = controller_validated_qs.filter(
|
||||||
|
Q(assigned_control_team_id__in=control_team_ids) |
|
||||||
|
Q(assigned_controller__user=request.user)
|
||||||
|
)
|
||||||
|
controller_validated_count = controller_validated_qs.count()
|
||||||
|
|
||||||
# ========== Données widget external_interventions ==========
|
# ========== Données widget external_interventions ==========
|
||||||
user_company_ids = set()
|
user_company_ids = set()
|
||||||
if user_config.company_id:
|
if user_config.company_id:
|
||||||
|
|
@ -801,6 +837,10 @@ def index(request):
|
||||||
'external_interventions_unfinished_count': external_interventions_unfinished_count,
|
'external_interventions_unfinished_count': external_interventions_unfinished_count,
|
||||||
'external_status_counts': external_status_counts,
|
'external_status_counts': external_status_counts,
|
||||||
'external_interventions_list': external_interventions_list,
|
'external_interventions_list': external_interventions_list,
|
||||||
|
'controller_interventions_count': controller_interventions_count,
|
||||||
|
'controller_interventions_list': controller_interventions_list,
|
||||||
|
'controller_to_correct_count': controller_to_correct_count,
|
||||||
|
'controller_validated_count': controller_validated_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Logging de performance (désactivé) ────────────────────────────────
|
# ── Logging de performance (désactivé) ────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,8 @@ class InterventionSubTypeAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Intervention)
|
@admin.register(Intervention)
|
||||||
class InterventionAdmin(admin.ModelAdmin):
|
class InterventionAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("code", "title")
|
search_fields = ("code", "title")
|
||||||
list_filter = ("thematic", "type", "sub_type", "status", "maintain_type")
|
list_filter = ("thematic", "type", "sub_type", "status", "maintain_type", "assigned_control_team")
|
||||||
raw_id_fields = ("intervention_manager", "sub_type")
|
raw_id_fields = ("intervention_manager", "sub_type", "assigned_team", "assigned_member", "assigned_control_team", "assigned_controller")
|
||||||
|
|
||||||
change_list_template = "admin/interventions/intervention_changelist.html"
|
change_list_template = "admin/interventions/intervention_changelist.html"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -8112,3 +8112,28 @@ msgstr "You cannot modify this intervention."
|
||||||
|
|
||||||
#~ msgid "Interventions suivies uniquement"
|
#~ msgid "Interventions suivies uniquement"
|
||||||
#~ msgstr "Followed interventions only"
|
#~ msgstr "Followed interventions only"
|
||||||
|
|
||||||
|
#: interventions/models.py:522
|
||||||
|
msgid "Équipe de contrôle assignée"
|
||||||
|
msgstr "Assigned control team"
|
||||||
|
|
||||||
|
#: interventions/models.py:523
|
||||||
|
msgid "Équipe chargée du contrôle et de la validation des postes de l'intervention."
|
||||||
|
msgstr "Team in charge of checking and validating the intervention posts."
|
||||||
|
|
||||||
|
#: interventions/models.py:531
|
||||||
|
msgid "Contrôleur assigné"
|
||||||
|
msgstr "Assigned controller"
|
||||||
|
|
||||||
|
#: interventions/models.py:532
|
||||||
|
msgid "Membre de l'équipe de contrôle en charge de la vérification."
|
||||||
|
msgstr "Control team member in charge of verification."
|
||||||
|
|
||||||
|
#: interventions/templates/interventions/detail/card_info.html:952
|
||||||
|
msgid "Contrôle :"
|
||||||
|
msgstr "Control:"
|
||||||
|
|
||||||
|
#: interventions/views/general.py:1007
|
||||||
|
msgid "Équipe de contrôle"
|
||||||
|
msgstr "Control team"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -8219,3 +8219,28 @@ msgstr "Vous suivez maintenant cette intervention."
|
||||||
|
|
||||||
#~ msgid "Interventions suivies uniquement"
|
#~ msgid "Interventions suivies uniquement"
|
||||||
#~ msgstr "Interventions suivies uniquement"
|
#~ msgstr "Interventions suivies uniquement"
|
||||||
|
|
||||||
|
#: interventions/models.py:522
|
||||||
|
msgid "Équipe de contrôle assignée"
|
||||||
|
msgstr "Équipe de contrôle assignée"
|
||||||
|
|
||||||
|
#: interventions/models.py:523
|
||||||
|
msgid "Équipe chargée du contrôle et de la validation des postes de l'intervention."
|
||||||
|
msgstr "Équipe chargée du contrôle et de la validation des postes de l'intervention."
|
||||||
|
|
||||||
|
#: interventions/models.py:531
|
||||||
|
msgid "Contrôleur assigné"
|
||||||
|
msgstr "Contrôleur assigné"
|
||||||
|
|
||||||
|
#: interventions/models.py:532
|
||||||
|
msgid "Membre de l'équipe de contrôle en charge de la vérification."
|
||||||
|
msgstr "Membre de l'équipe de contrôle en charge de la vérification."
|
||||||
|
|
||||||
|
#: interventions/templates/interventions/detail/card_info.html:952
|
||||||
|
msgid "Contrôle :"
|
||||||
|
msgstr "Contrôle :"
|
||||||
|
|
||||||
|
#: interventions/views/general.py:1007
|
||||||
|
msgid "Équipe de contrôle"
|
||||||
|
msgstr "Équipe de contrôle"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -8153,3 +8153,28 @@ msgstr "U hebt geen toestemming om deze interventie te wijzigen."
|
||||||
|
|
||||||
#~ msgid "Interventions suivies uniquement"
|
#~ msgid "Interventions suivies uniquement"
|
||||||
#~ msgstr "Enkel gevolgde interventies"
|
#~ msgstr "Enkel gevolgde interventies"
|
||||||
|
|
||||||
|
#: interventions/models.py:522
|
||||||
|
msgid "Équipe de contrôle assignée"
|
||||||
|
msgstr "Toegewezen controleteam"
|
||||||
|
|
||||||
|
#: interventions/models.py:523
|
||||||
|
msgid "Équipe chargée du contrôle et de la validation des postes de l'intervention."
|
||||||
|
msgstr "Team belast met de controle en validatie van de posten van de interventie."
|
||||||
|
|
||||||
|
#: interventions/models.py:531
|
||||||
|
msgid "Contrôleur assigné"
|
||||||
|
msgstr "Toegewezen controleur"
|
||||||
|
|
||||||
|
#: interventions/models.py:532
|
||||||
|
msgid "Membre de l'équipe de contrôle en charge de la vérification."
|
||||||
|
msgstr "Lid van het controleteam belast met de controle."
|
||||||
|
|
||||||
|
#: interventions/templates/interventions/detail/card_info.html:952
|
||||||
|
msgid "Contrôle :"
|
||||||
|
msgstr "Controle:"
|
||||||
|
|
||||||
|
#: interventions/views/general.py:1007
|
||||||
|
msgid "Équipe de contrôle"
|
||||||
|
msgstr "Controleteam"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 11:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contracts', '0009_contract_auto_assign_control_team'),
|
||||||
|
('interventions', '0012_interventionsubtype_concerned_categories_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='intervention',
|
||||||
|
name='assigned_control_team',
|
||||||
|
field=models.ForeignKey(blank=True, help_text="Équipe chargée du contrôle et de la validation des postes de l'intervention.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='controlled_interventions', to='contracts.companyteam', verbose_name='Équipe de contrôle assignée'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='intervention',
|
||||||
|
name='assigned_controller',
|
||||||
|
field=models.ForeignKey(blank=True, help_text="Membre de l'équipe de contrôle en charge de la vérification.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='controlled_interventions', to='contracts.companymember', verbose_name='Contrôleur assigné'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -514,6 +514,24 @@ class Intervention(models.Model):
|
||||||
assigned_provider = models.ForeignKey(Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_interventions")
|
assigned_provider = models.ForeignKey(Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_interventions")
|
||||||
assigned_team = models.ForeignKey(CompanyTeam, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_interventions")
|
assigned_team = models.ForeignKey(CompanyTeam, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_interventions")
|
||||||
assigned_member = models.ForeignKey(CompanyMember, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_interventions")
|
assigned_member = models.ForeignKey(CompanyMember, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_interventions")
|
||||||
|
assigned_control_team = models.ForeignKey(
|
||||||
|
CompanyTeam,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="controlled_interventions",
|
||||||
|
verbose_name=_("Équipe de contrôle assignée"),
|
||||||
|
help_text=_("Équipe chargée du contrôle et de la validation des postes de l'intervention.")
|
||||||
|
)
|
||||||
|
assigned_controller = models.ForeignKey(
|
||||||
|
CompanyMember,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="controlled_interventions",
|
||||||
|
verbose_name=_("Contrôleur assigné"),
|
||||||
|
help_text=_("Membre de l'équipe de contrôle en charge de la vérification.")
|
||||||
|
)
|
||||||
osiris = models.IntegerField(null=True, blank=True)
|
osiris = models.IntegerField(null=True, blank=True)
|
||||||
linked_folder = models.CharField(max_length=1024, null=True, blank=True)
|
linked_folder = models.CharField(max_length=1024, null=True, blank=True)
|
||||||
intervention_sheet = models.FileField(upload_to=intervention_sheet_upload_path, storage=private_storage, null=True, blank=True)
|
intervention_sheet = models.FileField(upload_to=intervention_sheet_upload_path, storage=private_storage, null=True, blank=True)
|
||||||
|
|
@ -634,6 +652,26 @@ class Intervention(models.Model):
|
||||||
user_config = UserConfig.objects.filter(user_id=self.created_by_id).first()
|
user_config = UserConfig.objects.filter(user_id=self.created_by_id).first()
|
||||||
if user_config and not user_config.is_intern:
|
if user_config and not user_config.is_intern:
|
||||||
self.created_by_provider = True
|
self.created_by_provider = True
|
||||||
|
|
||||||
|
# Attribution automatique de l'équipe de contrôle pour les interventions correctives Structures
|
||||||
|
if (
|
||||||
|
self.maintain_type == 'corrective'
|
||||||
|
and self.thematic and getattr(self.thematic, 'code', None) == 'structures'
|
||||||
|
and self.contract and getattr(self.contract, 'auto_assign_control_team', False)
|
||||||
|
and not self.assigned_control_team_id
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
from interventions.services_control_assignment import resolve_control_team_and_member_for_intervention
|
||||||
|
ctrl_team, ctrl_member = resolve_control_team_and_member_for_intervention(self)
|
||||||
|
if ctrl_team:
|
||||||
|
self.assigned_control_team = ctrl_team
|
||||||
|
self.assigned_controller = ctrl_member
|
||||||
|
except Exception:
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).exception(
|
||||||
|
f"Error auto-assigning control team for intervention {self.code}"
|
||||||
|
)
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Sync daily assignments history
|
# Sync daily assignments history
|
||||||
|
|
|
||||||
|
|
@ -312,8 +312,13 @@ def can_view_intervention(user, intervention):
|
||||||
and intervention.assigned_member
|
and intervention.assigned_member
|
||||||
and intervention.assigned_member.teams.filter(id__in=company_member.teams.all()).exists()
|
and intervention.assigned_member.teams.filter(id__in=company_member.teams.all()).exists()
|
||||||
)
|
)
|
||||||
|
is_assigned_to_control_team = bool(
|
||||||
|
intervention.assigned_control_team
|
||||||
|
and intervention.assigned_control_team in company_member.teams.all()
|
||||||
|
)
|
||||||
|
is_assigned_controller = (intervention.assigned_controller == company_member)
|
||||||
|
|
||||||
if not (is_assigned_to_user or is_assigned_to_user_team or is_unrestricted_company or is_team_member_edit_allowed):
|
if not (is_assigned_to_user or is_assigned_to_user_team or is_unrestricted_company or is_team_member_edit_allowed or is_assigned_to_control_team or is_assigned_controller):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
contract = intervention.contract
|
contract = intervention.contract
|
||||||
|
|
@ -352,10 +357,16 @@ def can_view_intervention(user, intervention):
|
||||||
can_view=True,
|
can_view=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
# -- Accès contrôleur assigné / équipe de contrôle assignée --
|
||||||
|
if company_member and (
|
||||||
|
intervention.assigned_controller == company_member
|
||||||
|
or bool(intervention.assigned_control_team and intervention.assigned_control_team in company_member.teams.all())
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
# -- Fallback prestataire assigné --
|
# -- Fallback prestataire assigné --
|
||||||
assigned_provider = intervention.assigned_provider
|
assigned_provider = intervention.assigned_provider
|
||||||
if assigned_provider:
|
if assigned_provider:
|
||||||
company_member = CompanyMember.objects.filter(user=user).prefetch_related('teams__company').first()
|
|
||||||
if company_member:
|
if company_member:
|
||||||
return company_member.teams.filter(company=assigned_provider).exists()
|
return company_member.teams.filter(company=assigned_provider).exists()
|
||||||
|
|
||||||
|
|
@ -405,7 +416,8 @@ def intervention_permission_required(view_func):
|
||||||
def can_control_intervention(user, intervention):
|
def can_control_intervention(user, intervention):
|
||||||
"""
|
"""
|
||||||
Vérifie si l'utilisateur peut contrôler (valider/corriger) l'intervention.
|
Vérifie si l'utilisateur peut contrôler (valider/corriger) l'intervention.
|
||||||
Réservé aux utilisateurs internes ayant le droit d'édition sur le statut courant.
|
Réservé aux utilisateurs internes ayant le droit d'édition sur le statut courant
|
||||||
|
ou membres de l'équipe de contrôle assignée / contrôleur assigné.
|
||||||
"""
|
"""
|
||||||
if intervention.status not in ('processed', 'corrected'):
|
if intervention.status not in ('processed', 'corrected'):
|
||||||
return False
|
return False
|
||||||
|
|
@ -418,6 +430,16 @@ def can_control_intervention(user, intervention):
|
||||||
if not user_config.is_intern:
|
if not user_config.is_intern:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Si l'utilisateur est le contrôleur assigné ou fait partie de l'équipe de contrôle assignée
|
||||||
|
from contracts.models import CompanyMember
|
||||||
|
company_member = CompanyMember.objects.filter(user=user).prefetch_related('teams').first()
|
||||||
|
if company_member:
|
||||||
|
if (
|
||||||
|
intervention.assigned_controller == company_member
|
||||||
|
or (intervention.assigned_control_team and intervention.assigned_control_team in company_member.teams.all())
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
return can_edit_intervention(user, intervention, user_config=user_config)
|
return can_edit_intervention(user, intervention, user_config=user_config)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -716,13 +738,19 @@ def filter_viewable_interventions_for_user(user, limit_to_default_thematic=False
|
||||||
company_q = (
|
company_q = (
|
||||||
Q(assigned_member=company_member) |
|
Q(assigned_member=company_member) |
|
||||||
Q(assigned_team__in=list(company_member.teams.all())) |
|
Q(assigned_team__in=list(company_member.teams.all())) |
|
||||||
Q(contract__allow_team_members_edit=True, assigned_member__teams__in=list(company_member.teams.all()))
|
Q(contract__allow_team_members_edit=True, assigned_member__teams__in=list(company_member.teams.all())) |
|
||||||
|
Q(assigned_control_team__in=list(company_member.teams.all())) |
|
||||||
|
Q(assigned_controller=company_member)
|
||||||
)
|
)
|
||||||
if unrestricted_companies:
|
if unrestricted_companies:
|
||||||
company_q |= Q(assigned_member__isnull=True, assigned_team__isnull=True, assigned_provider_id__in=unrestricted_companies)
|
company_q |= Q(assigned_member__isnull=True, assigned_team__isnull=True, assigned_provider_id__in=unrestricted_companies)
|
||||||
else:
|
else:
|
||||||
user_companies = company_member.teams.values_list('company_id', flat=True).distinct()
|
user_companies = company_member.teams.values_list('company_id', flat=True).distinct()
|
||||||
company_q = Q(assigned_provider_id__in=user_companies)
|
company_q = (
|
||||||
|
Q(assigned_provider_id__in=user_companies) |
|
||||||
|
Q(assigned_control_team__in=list(company_member.teams.all())) |
|
||||||
|
Q(assigned_controller=company_member)
|
||||||
|
)
|
||||||
|
|
||||||
if user_config.limit_interventions_to_contracts:
|
if user_config.limit_interventions_to_contracts:
|
||||||
# Limiter aux contrats configurés pour cet utilisateur afin d'exclure
|
# Limiter aux contrats configurés pour cet utilisateur afin d'exclure
|
||||||
|
|
@ -761,7 +789,9 @@ def filter_viewable_interventions_for_user(user, limit_to_default_thematic=False
|
||||||
assign_filter = (
|
assign_filter = (
|
||||||
Q(assigned_member=company_member) |
|
Q(assigned_member=company_member) |
|
||||||
Q(assigned_team__in=list(company_member.teams.all())) |
|
Q(assigned_team__in=list(company_member.teams.all())) |
|
||||||
Q(contract__allow_team_members_edit=True, assigned_member__teams__in=list(company_member.teams.all()))
|
Q(contract__allow_team_members_edit=True, assigned_member__teams__in=list(company_member.teams.all())) |
|
||||||
|
Q(assigned_control_team__in=list(company_member.teams.all())) |
|
||||||
|
Q(assigned_controller=company_member)
|
||||||
)
|
)
|
||||||
if unrestricted_companies:
|
if unrestricted_companies:
|
||||||
assign_filter |= Q(assigned_provider_id__in=unrestricted_companies)
|
assign_filter |= Q(assigned_provider_id__in=unrestricted_companies)
|
||||||
|
|
|
||||||
154
loko/interventions/services_control_assignment.py
Normal file
154
loko/interventions/services_control_assignment.py
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import logging
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_control_team_and_member(
|
||||||
|
target_geom=None,
|
||||||
|
point=None,
|
||||||
|
scheduled_date=None,
|
||||||
|
thematic=None,
|
||||||
|
locations=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Détermine l'équipe de contrôle (CompanyTeam) et le contrôleur (CompanyMember) :
|
||||||
|
1. Équipes candidates gérant l'opération 'controls' (OPERATION_CONTROLS).
|
||||||
|
2. Filtrage thématique si l'équipe restreint ses thématiques.
|
||||||
|
3. Résolution spatiale si une géométrie est définie (cible et équipes) :
|
||||||
|
- team.geom.intersects(target_geom) ou team.geom.contains(point).
|
||||||
|
- Fallback sur toutes les équipes candidates si aucune géométrie n'est définie.
|
||||||
|
4. Calendrier d'assignation à la date scheduled_date :
|
||||||
|
- Évalue le nombre d'agents planifiés et non absents.
|
||||||
|
- Si aucun agent n'est disponible ou aucune date : retourne (None, None).
|
||||||
|
- Sélectionne l'équipe ayant le plus d'agents disponibles.
|
||||||
|
- Sélectionne le premier membre disponible de cette équipe.
|
||||||
|
"""
|
||||||
|
from contracts.models import CompanyTeam, CompanyMember
|
||||||
|
from contracts.services_teams import get_teams_composition_for_period, get_member_absences_for_period
|
||||||
|
|
||||||
|
teams_qs = CompanyTeam.objects.all().select_related('company')
|
||||||
|
candidate_teams = [t for t in teams_qs if t.handles_operation(CompanyTeam.OPERATION_CONTROLS)]
|
||||||
|
|
||||||
|
if not candidate_teams:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 1. Filtrage thématique
|
||||||
|
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
|
||||||
|
|
||||||
|
# 2. Résolution spatiale si géométrie définie
|
||||||
|
has_target_geom = bool(target_geom or point or locations)
|
||||||
|
teams_with_geom = [t for t in candidate_teams if t.geom is not None]
|
||||||
|
|
||||||
|
if has_target_geom and teams_with_geom:
|
||||||
|
spatial_matches = []
|
||||||
|
for team in teams_with_geom:
|
||||||
|
matched = False
|
||||||
|
if target_geom and team.geom:
|
||||||
|
try:
|
||||||
|
matched = team.geom.intersects(target_geom)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking team {team.id} geom intersects: {e}")
|
||||||
|
|
||||||
|
if not matched and point and team.geom:
|
||||||
|
try:
|
||||||
|
matched = team.geom.contains(point)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking team {team.id} geom contains point: {e}")
|
||||||
|
|
||||||
|
if not matched and locations and team.geom:
|
||||||
|
for loc in locations:
|
||||||
|
loc_geom = getattr(loc, 'geom', None)
|
||||||
|
if loc_geom:
|
||||||
|
try:
|
||||||
|
if team.geom.intersects(loc_geom):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if matched:
|
||||||
|
spatial_matches.append(team)
|
||||||
|
|
||||||
|
eligible_teams = spatial_matches
|
||||||
|
else:
|
||||||
|
# Fallback sans géométrie : toutes les équipes candidates
|
||||||
|
eligible_teams = candidate_teams
|
||||||
|
|
||||||
|
if not eligible_teams:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 3. Calendrier d'assignation
|
||||||
|
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))
|
||||||
|
|
||||||
|
scored_teams.sort(key=lambda item: (-item[0], item[1].name))
|
||||||
|
best_len, best_team, best_members = scored_teams[0]
|
||||||
|
|
||||||
|
# Règle stricte : si aucun agent disponible ce jour-là, ne pas assigner
|
||||||
|
if best_len == 0 or not best_members:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
best_member_id = best_members[0]['id']
|
||||||
|
best_member = CompanyMember.objects.filter(id=best_member_id).first()
|
||||||
|
|
||||||
|
return best_team, best_member
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_control_team_and_member_for_intervention(intervention):
|
||||||
|
"""
|
||||||
|
Extrait la géométrie, la localisation et la date de l'intervention
|
||||||
|
puis appelle resolve_control_team_and_member.
|
||||||
|
"""
|
||||||
|
point = None
|
||||||
|
if intervention.lon and intervention.lat:
|
||||||
|
try:
|
||||||
|
p = Point(float(intervention.lon), float(intervention.lat), srid=4326)
|
||||||
|
p.transform(3812)
|
||||||
|
point = p
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error transforming intervention coordinates to 3812: {e}")
|
||||||
|
|
||||||
|
target_date = (
|
||||||
|
intervention.planned_begin_time
|
||||||
|
or intervention.begin_time
|
||||||
|
or intervention.creation_time
|
||||||
|
or timezone.now().date()
|
||||||
|
)
|
||||||
|
|
||||||
|
locs = list(intervention.locations.all()) if (intervention.pk and hasattr(intervention, 'locations')) else []
|
||||||
|
|
||||||
|
return resolve_control_team_and_member(
|
||||||
|
target_geom=intervention.geom,
|
||||||
|
point=point,
|
||||||
|
scheduled_date=target_date,
|
||||||
|
thematic=intervention.thematic,
|
||||||
|
locations=locs,
|
||||||
|
)
|
||||||
|
|
@ -946,6 +946,20 @@
|
||||||
</span>
|
</span>
|
||||||
<span id="memberAssignmentSuccess" class="text-success ms-2 d-none">✓</span>
|
<span id="memberAssignmentSuccess" class="text-success ms-2 d-none">✓</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if intervention.assigned_control_team or intervention.assigned_controller %}
|
||||||
|
<div class="d-flex align-items-center mt-1">
|
||||||
|
<span class="btn btn-sm border-0 disabled text-muted"><i class="bi bi-patch-check"></i></span>
|
||||||
|
<span class="label mx-1">{% translate "Contrôle :" %}</span>
|
||||||
|
<span class="badge bg-secondary-subtle text-secondary border">
|
||||||
|
{% if intervention.assigned_controller %}
|
||||||
|
{{ intervention.assigned_controller.name }}
|
||||||
|
{% if intervention.assigned_control_team %}({{ intervention.assigned_control_team.name }}){% endif %}
|
||||||
|
{% elif intervention.assigned_control_team %}
|
||||||
|
{{ intervention.assigned_control_team.name }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="modal fade" id="editAssignmentMemberModal" tabindex="-1" aria-labelledby="editAssignmentMemberModalLabel" aria-hidden="true">
|
<div class="modal fade" id="editAssignmentMemberModal" tabindex="-1" aria-labelledby="editAssignmentMemberModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
|
||||||
208
loko/interventions/tests/test_control_assignment.py
Normal file
208
loko/interventions/tests/test_control_assignment.py
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.gis.geos import MultiPolygon, Polygon, Point
|
||||||
|
|
||||||
|
from common.models import Role, UserConfig, Thematic, UserContractAccess
|
||||||
|
from contracts.models import (
|
||||||
|
Company, CompanyTeam, CompanyMember, CompanyMemberHistory, CompanyMemberAbsence, Contract
|
||||||
|
)
|
||||||
|
from interventions.models import Intervention, InterventionContractPost, ContractPost
|
||||||
|
from interventions.permissions import can_view_intervention, can_control_intervention
|
||||||
|
from interventions.services_control_assignment import (
|
||||||
|
resolve_control_team_and_member,
|
||||||
|
resolve_control_team_and_member_for_intervention,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ControlAssignmentTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.User = get_user_model()
|
||||||
|
self.role_controller, _ = Role.objects.get_or_create(name='controller')
|
||||||
|
self.role_tech, _ = Role.objects.get_or_create(name='technician')
|
||||||
|
|
||||||
|
# Thématiques
|
||||||
|
self.thematic_structures, _ = Thematic.objects.get_or_create(
|
||||||
|
code='structures', defaults={'name_fr': 'Ouvrages d\'art', 'name_nl': 'Kunstwerken'}
|
||||||
|
)
|
||||||
|
self.thematic_roads, _ = Thematic.objects.get_or_create(
|
||||||
|
code='roads', defaults={'name_fr': 'Voirie', 'name_nl': 'Wegen'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Société & Prestataire
|
||||||
|
self.company = Company.objects.create(name='Prestataire Structures')
|
||||||
|
self.org_intern = Company.objects.create(name='Organisme Interne Contrôle')
|
||||||
|
|
||||||
|
# Équipes opérationnelles terrain
|
||||||
|
self.team_ops = CompanyTeam.objects.create(
|
||||||
|
name='Équipe Opérationnelle',
|
||||||
|
company=self.company,
|
||||||
|
operation_types=[CompanyTeam.OPERATION_INTERVENTIONS]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Équipe de contrôle avec géométrie (SRID 3812)
|
||||||
|
# Bbox approximative autour de Bruxelles en Lambert 72
|
||||||
|
poly = Polygon(((140000, 160000), (160000, 160000), (160000, 180000), (140000, 180000), (140000, 160000)))
|
||||||
|
self.ctrl_geom = MultiPolygon([poly], srid=3812)
|
||||||
|
self.team_ctrl = CompanyTeam.objects.create(
|
||||||
|
name='Équipe Contrôle Structures',
|
||||||
|
company=self.org_intern,
|
||||||
|
operation_types=[CompanyTeam.OPERATION_CONTROLS],
|
||||||
|
geom=self.ctrl_geom
|
||||||
|
)
|
||||||
|
|
||||||
|
# Membre contrôleur
|
||||||
|
self.user_ctrl = self.User.objects.create_user(username='ctrl_agent', password='pwd')
|
||||||
|
self.config_ctrl = UserConfig.objects.create(user=self.user_ctrl, is_intern=True)
|
||||||
|
self.config_ctrl.roles.add(self.role_controller)
|
||||||
|
self.member_ctrl = CompanyMember.objects.create(user=self.user_ctrl, name='Agent Contrôle 1')
|
||||||
|
self.member_ctrl.teams.add(self.team_ctrl)
|
||||||
|
|
||||||
|
# Planification du contrôleur dans l'historique
|
||||||
|
self.today = timezone.now().date()
|
||||||
|
CompanyMemberHistory.objects.create(
|
||||||
|
member=self.member_ctrl,
|
||||||
|
team=self.team_ctrl,
|
||||||
|
join_date=self.today - timedelta(days=30),
|
||||||
|
leave_date=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Contrat avec attribution automatique du contrôle
|
||||||
|
self.contract_auto = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number='C-STRUCT-AUTO',
|
||||||
|
start_date=self.today - timedelta(days=100),
|
||||||
|
end_date=self.today + timedelta(days=100),
|
||||||
|
is_active=True,
|
||||||
|
auto_assign_control_team=True,
|
||||||
|
)
|
||||||
|
self.contract_auto.thematics.add(self.thematic_structures)
|
||||||
|
|
||||||
|
# Contrat SANS attribution automatique
|
||||||
|
self.contract_no_auto = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number='C-STRUCT-NOAUTO',
|
||||||
|
start_date=self.today - timedelta(days=100),
|
||||||
|
end_date=self.today + timedelta(days=100),
|
||||||
|
is_active=True,
|
||||||
|
auto_assign_control_team=False,
|
||||||
|
)
|
||||||
|
self.contract_no_auto.thematics.add(self.thematic_structures)
|
||||||
|
|
||||||
|
def test_auto_assignment_structures_corrective(self):
|
||||||
|
"""Vérifie l'attribution automatique pour une intervention corrective Structures avec contrat activé."""
|
||||||
|
# Coordonnées au centre de la zone (Bruxelles ~ 4.35, 50.85 -> en Lambert 72 ~ 150000, 170000)
|
||||||
|
p_poly = Polygon(((149000, 169000), (151000, 169000), (151000, 171000), (149000, 171000), (149000, 169000)))
|
||||||
|
geom = MultiPolygon([p_poly], srid=3812)
|
||||||
|
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic_structures,
|
||||||
|
contract=self.contract_auto,
|
||||||
|
geom=geom,
|
||||||
|
planned_begin_time=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(itv.assigned_control_team, self.team_ctrl)
|
||||||
|
self.assertEqual(itv.assigned_controller, self.member_ctrl)
|
||||||
|
|
||||||
|
def test_no_auto_assignment_if_not_corrective(self):
|
||||||
|
"""Une intervention préventive ne doit pas recevoir d'attribution automatique de contrôle."""
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='preventive',
|
||||||
|
thematic=self.thematic_structures,
|
||||||
|
contract=self.contract_auto,
|
||||||
|
planned_begin_time=timezone.now(),
|
||||||
|
)
|
||||||
|
self.assertIsNone(itv.assigned_control_team)
|
||||||
|
self.assertIsNone(itv.assigned_controller)
|
||||||
|
|
||||||
|
def test_no_auto_assignment_if_not_structures(self):
|
||||||
|
"""Une intervention d'une autre thématique ne doit pas recevoir d'attribution automatique de contrôle."""
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic_roads,
|
||||||
|
contract=self.contract_auto,
|
||||||
|
planned_begin_time=timezone.now(),
|
||||||
|
)
|
||||||
|
self.assertIsNone(itv.assigned_control_team)
|
||||||
|
self.assertIsNone(itv.assigned_controller)
|
||||||
|
|
||||||
|
def test_no_auto_assignment_if_contract_not_enabled(self):
|
||||||
|
"""Un contrat sans auto_assign_control_team ne doit pas déclencher l'attribution."""
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic_structures,
|
||||||
|
contract=self.contract_no_auto,
|
||||||
|
planned_begin_time=timezone.now(),
|
||||||
|
)
|
||||||
|
self.assertIsNone(itv.assigned_control_team)
|
||||||
|
self.assertIsNone(itv.assigned_controller)
|
||||||
|
|
||||||
|
def test_fallback_calendar_without_geometry(self):
|
||||||
|
"""Si aucune géométrie n'est définie, le fallback calendrier doit sélectionner l'équipe candidate."""
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic_structures,
|
||||||
|
contract=self.contract_auto,
|
||||||
|
geom=None,
|
||||||
|
planned_begin_time=timezone.now(),
|
||||||
|
)
|
||||||
|
self.assertEqual(itv.assigned_control_team, self.team_ctrl)
|
||||||
|
self.assertEqual(itv.assigned_controller, self.member_ctrl)
|
||||||
|
|
||||||
|
def test_strict_fallback_when_no_member_available(self):
|
||||||
|
"""Si le contrôleur est absent à la date cible et aucun autre agent disponible, attribue (None, None)."""
|
||||||
|
target_date = self.today + timedelta(days=5)
|
||||||
|
# Enregistrer une absence
|
||||||
|
CompanyMemberAbsence.objects.create(
|
||||||
|
member=self.member_ctrl,
|
||||||
|
date=target_date,
|
||||||
|
reason='conge'
|
||||||
|
)
|
||||||
|
|
||||||
|
team, member = resolve_control_team_and_member(
|
||||||
|
target_geom=self.ctrl_geom,
|
||||||
|
scheduled_date=target_date,
|
||||||
|
thematic=self.thematic_structures
|
||||||
|
)
|
||||||
|
self.assertIsNone(team)
|
||||||
|
self.assertIsNone(member)
|
||||||
|
|
||||||
|
def test_does_not_overwrite_manual_control_assignment(self):
|
||||||
|
"""Ne doit pas écraser une équipe de contrôle déjà assignée manuellement."""
|
||||||
|
team_other = CompanyTeam.objects.create(
|
||||||
|
name='Autre Contrôle',
|
||||||
|
company=self.org_intern,
|
||||||
|
operation_types=[CompanyTeam.OPERATION_CONTROLS]
|
||||||
|
)
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic_structures,
|
||||||
|
contract=self.contract_auto,
|
||||||
|
assigned_control_team=team_other,
|
||||||
|
planned_begin_time=timezone.now(),
|
||||||
|
)
|
||||||
|
self.assertEqual(itv.assigned_control_team, team_other)
|
||||||
|
|
||||||
|
def test_controller_permissions_and_control(self):
|
||||||
|
"""Le contrôleur assigné doit pouvoir visualiser et contrôler l'intervention au statut processed."""
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
maintain_type='corrective',
|
||||||
|
thematic=self.thematic_structures,
|
||||||
|
contract=self.contract_auto,
|
||||||
|
status='processed',
|
||||||
|
assigned_control_team=self.team_ctrl,
|
||||||
|
assigned_controller=self.member_ctrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Permissions de vue
|
||||||
|
self.assertTrue(can_view_intervention(self.user_ctrl, itv))
|
||||||
|
# Permissions de contrôle (statut processed)
|
||||||
|
self.assertTrue(can_control_intervention(self.user_ctrl, itv))
|
||||||
|
|
||||||
|
# Au statut in_progress, le contrôle n'est pas encore ouvert
|
||||||
|
itv.status = 'in_progress'
|
||||||
|
itv.save(update_fields=['status'])
|
||||||
|
self.assertFalse(can_control_intervention(self.user_ctrl, itv))
|
||||||
|
|
@ -2480,7 +2480,14 @@ def intervention_posts_bulk_update(request, intervention_id):
|
||||||
|
|
||||||
# Vérifier les permissions selon le type de modification
|
# Vérifier les permissions selon le type de modification
|
||||||
can_update_posts = user_config.roles.filter(name__in=['admin', 'external_manager']).exists()
|
can_update_posts = user_config.roles.filter(name__in=['admin', 'external_manager']).exists()
|
||||||
can_control_posts = user_config.roles.filter(name__in=['admin', 'manager', 'controller']).exists()
|
is_in_control_team = False
|
||||||
|
company_member = CompanyMember.objects.filter(user=request.user).first()
|
||||||
|
if company_member:
|
||||||
|
is_in_control_team = (
|
||||||
|
intervention.assigned_controller == company_member
|
||||||
|
or bool(intervention.assigned_control_team and intervention.assigned_control_team in company_member.teams.all())
|
||||||
|
)
|
||||||
|
can_control_posts = user_config.roles.filter(name__in=['admin', 'manager', 'controller']).exists() or is_in_control_team
|
||||||
|
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,8 @@ def index(request):
|
||||||
'selected_contracts': request.GET.getlist('contract'),
|
'selected_contracts': request.GET.getlist('contract'),
|
||||||
'selected_teams': request.GET.getlist('assigned_team'),
|
'selected_teams': request.GET.getlist('assigned_team'),
|
||||||
'selected_members': request.GET.getlist('assigned_member'),
|
'selected_members': request.GET.getlist('assigned_member'),
|
||||||
|
'selected_control_teams': request.GET.getlist('assigned_control_team'),
|
||||||
|
'selected_controllers': request.GET.getlist('assigned_controller'),
|
||||||
'selected_pause_reasons': pause_reasons,
|
'selected_pause_reasons': pause_reasons,
|
||||||
'selected_preparation_states': preparation_states,
|
'selected_preparation_states': preparation_states,
|
||||||
'begin_date_min': begin_date_min or '',
|
'begin_date_min': begin_date_min or '',
|
||||||
|
|
@ -465,6 +467,8 @@ def interventions_geojson(request):
|
||||||
codes = _expand_csv_params(request.GET.getlist('code') + request.GET.getlist('codes'))
|
codes = _expand_csv_params(request.GET.getlist('code') + request.GET.getlist('codes'))
|
||||||
assigned_teams = _expand_csv_params(request.GET.getlist('assigned_team'))
|
assigned_teams = _expand_csv_params(request.GET.getlist('assigned_team'))
|
||||||
assigned_members = _expand_csv_params(request.GET.getlist('assigned_member'))
|
assigned_members = _expand_csv_params(request.GET.getlist('assigned_member'))
|
||||||
|
assigned_control_teams = _expand_csv_params(request.GET.getlist('assigned_control_team'))
|
||||||
|
assigned_controllers = _expand_csv_params(request.GET.getlist('assigned_controller'))
|
||||||
pause_reasons = _expand_csv_params(request.GET.getlist('pause_reason'))
|
pause_reasons = _expand_csv_params(request.GET.getlist('pause_reason'))
|
||||||
intervention_managers = _expand_csv_params(request.GET.getlist('intervention_manager'))
|
intervention_managers = _expand_csv_params(request.GET.getlist('intervention_manager'))
|
||||||
preparation_states = _expand_csv_params(request.GET.getlist('preparation_state'))
|
preparation_states = _expand_csv_params(request.GET.getlist('preparation_state'))
|
||||||
|
|
@ -545,6 +549,10 @@ def interventions_geojson(request):
|
||||||
qs = qs.filter(assigned_member_id__in=assigned_members)
|
qs = qs.filter(assigned_member_id__in=assigned_members)
|
||||||
if assigned_teams:
|
if assigned_teams:
|
||||||
qs = qs.filter(Q(assigned_team_id__in=assigned_teams) | Q(assigned_member__teams__in=assigned_teams)).distinct()
|
qs = qs.filter(Q(assigned_team_id__in=assigned_teams) | Q(assigned_member__teams__in=assigned_teams)).distinct()
|
||||||
|
if assigned_controllers:
|
||||||
|
qs = qs.filter(assigned_controller_id__in=assigned_controllers)
|
||||||
|
if assigned_control_teams:
|
||||||
|
qs = qs.filter(Q(assigned_control_team_id__in=assigned_control_teams) | Q(assigned_controller__teams__in=assigned_control_teams)).distinct()
|
||||||
if intervention_managers:
|
if intervention_managers:
|
||||||
qs = qs.filter(intervention_manager_id__in=intervention_managers)
|
qs = qs.filter(intervention_manager_id__in=intervention_managers)
|
||||||
if followed_only == '1' and request.user.is_authenticated:
|
if followed_only == '1' and request.user.is_authenticated:
|
||||||
|
|
@ -1007,6 +1015,8 @@ def export_interventions(request):
|
||||||
(_("Fournisseur assigné"), lambda i: (i.assigned_provider.name if i.assigned_provider else '')),
|
(_("Fournisseur assigné"), lambda i: (i.assigned_provider.name if i.assigned_provider else '')),
|
||||||
(_("Equipe assignée"), lambda i: (i.assigned_team.name if i.assigned_team else '')),
|
(_("Equipe assignée"), lambda i: (i.assigned_team.name if i.assigned_team else '')),
|
||||||
(_("Membre assigné"), lambda i: (i.assigned_member.name if i.assigned_member else '')),
|
(_("Membre assigné"), lambda i: (i.assigned_member.name if i.assigned_member else '')),
|
||||||
|
(_("Équipe de contrôle"), lambda i: (i.assigned_control_team.name if i.assigned_control_team else '')),
|
||||||
|
(_("Contrôleur assigné"), lambda i: (i.assigned_controller.name if i.assigned_controller else '')),
|
||||||
(_("Contrat"), lambda i: (i.contract.contract_number if i.contract else '')),
|
(_("Contrat"), lambda i: (i.contract.contract_number if i.contract else '')),
|
||||||
(_("Commande"), lambda i: (i.order.order_code if i.order else '')),
|
(_("Commande"), lambda i: (i.order.order_code if i.order else '')),
|
||||||
(_("Début prévu"), lambda i: dj_format_date(i.expected_begin_time, 'Y-m-d H:i') if i.expected_begin_time else ''),
|
(_("Début prévu"), lambda i: dj_format_date(i.expected_begin_time, 'Y-m-d H:i') if i.expected_begin_time else ''),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue