feat(contracts,home): include finished status in controller shortcuts and display assigned controller in order interventions

- Add 'finished' status to controller shortcuts widget filter and links
- Add conditional 'Contrôleur assigné' column in order interventions table for contracts with auto_assign_control_team
- Optimize order interventions query with select_related for controllers
- Update and compile translations (fr, nl, en)
- Add unit tests for order detail controller display and widget status inclusion
This commit is contained in:
kdeterme 2026-09-30 18:17:30 +02:00
parent aa75bcbfeb
commit b86d805cb5
12 changed files with 186 additions and 36 deletions

View file

@ -2371,3 +2371,7 @@ msgstr "Automatically assign a control team"
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."
#: contracts/templates/contracts/contracts_orders_detail.html:755
msgid "Contrôleur assigné"
msgstr "Assigned controller"

View file

@ -2421,3 +2421,7 @@ msgstr "Assigner automatiquement une équipe de contrôle"
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."
#: contracts/templates/contracts/contracts_orders_detail.html:755
msgid "Contrôleur assigné"
msgstr "Contrôleur assigné"

View file

@ -2377,3 +2377,7 @@ msgstr "Automatisch een controleteam toewijzen"
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."
#: contracts/templates/contracts/contracts_orders_detail.html:755
msgid "Contrôleur assigné"
msgstr "Toegewezen controleur"

View file

@ -751,18 +751,35 @@
<th data-field="title">{% translate "Titre" %}</th>
<th data-field="begin_date">{% translate "Début" %}</th>
<th data-field="statut">{% translate "Statut" %}</th>
{% if order.contract.auto_assign_control_team %}
<th data-field="assigned_controller">{% translate "Contrôleur assigné" %}</th>
{% endif %}
</tr>
<tbody>
{% for intervention in interventions %}
<tr>
<td><a href="{% url 'interventions:interventions_index' %}{{ intervention.id }}">{{ intervention.code }}</a></td>
<td>{{ intervention.title }}</td>
<td>{{ intervention.begin_time|date:"d-m-Y H:i" }}</td>
<td><span class="badge status-{{ intervention.status }}">{{ intervention.get_status_display }}</span></td>
</tr>
{% endfor %}
</tbody>
</thead>
<tbody>
{% for intervention in interventions %}
<tr>
<td><a href="{% url 'interventions:interventions_index' %}{{ intervention.id }}">{{ intervention.code }}</a></td>
<td>{{ intervention.title }}</td>
<td>{{ intervention.begin_time|date:"d-m-Y H:i" }}</td>
<td><span class="badge status-{{ intervention.status }}">{{ intervention.get_status_display }}</span></td>
{% if order.contract.auto_assign_control_team %}
<td>
{% if intervention.assigned_controller %}
{{ intervention.assigned_controller.name }}
{% if intervention.assigned_control_team %}
<span class="text-muted small">({{ intervention.assigned_control_team.name }})</span>
{% endif %}
{% elif intervention.assigned_control_team %}
<span class="text-muted">{{ intervention.assigned_control_team.name }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View file

@ -344,3 +344,119 @@ class GenerateClaimDeclarationTimelineTests(TestCase):
self.assertEqual(timeline_event_2.event_type, "status_change")
self.assertEqual(timeline_event_2.event_user, self.user)
class ContractOrderDetailControllerDisplayTests(TestCase):
def setUp(self):
from datetime import date
from contracts.models import Contract, ContractOrder, Company, CompanyTeam, CompanyMember
from common.models import UserContractAccess
from interventions.models import Intervention
self.client = Client()
self.user = User.objects.create_user(username="order_admin_user", password="password")
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
self.admin_role, _ = Role.objects.get_or_create(name="admin")
self.user_config.roles.add(self.admin_role)
self.client.login(username="order_admin_user", password="password")
self.company = Company.objects.create(name="Société Test Contrôle")
self.team_ctrl = CompanyTeam.objects.create(
name="Équipe Contrôle Nord",
company=self.company,
operation_types=[CompanyTeam.OPERATION_CONTROLS],
)
ctrl_user = User.objects.create_user(username="ctrl_member_user", password="password")
self.controller_member = CompanyMember.objects.create(
user=ctrl_user,
name="Jean Contrôleur",
)
self.controller_member.teams.add(self.team_ctrl)
self.contract_with_ctrl = Contract.objects.create(
company=self.company,
contract_number="CTR-CTRL-001",
start_date=date(2025, 1, 1),
end_date=date(2026, 1, 1),
auto_assign_control_team=True,
)
UserContractAccess.objects.create(
user_config=self.user_config,
contract=self.contract_with_ctrl,
can_view_interventions=True,
)
self.order_with_ctrl = ContractOrder.objects.create(
contract=self.contract_with_ctrl,
order_code="ORD-CTRL-01",
order_status="pending",
order_date=date(2025, 1, 1),
delivery_date=date(2025, 1, 15),
description="Commande avec marché contrôlé",
)
self.itv1 = Intervention.objects.create(
code="ITV-C01",
title="Intervention avec contrôleur",
contract=self.contract_with_ctrl,
order=self.order_with_ctrl,
assigned_control_team=self.team_ctrl,
assigned_controller=self.controller_member,
status="finished",
)
self.itv2 = Intervention.objects.create(
code="ITV-C02",
title="Intervention équipe seule",
contract=self.contract_with_ctrl,
order=self.order_with_ctrl,
assigned_control_team=self.team_ctrl,
status="processed",
)
self.contract_without_ctrl = Contract.objects.create(
company=self.company,
contract_number="CTR-NO-CTRL-002",
start_date=date(2025, 1, 1),
end_date=date(2026, 1, 1),
auto_assign_control_team=False,
)
UserContractAccess.objects.create(
user_config=self.user_config,
contract=self.contract_without_ctrl,
can_view_interventions=True,
)
self.order_without_ctrl = ContractOrder.objects.create(
contract=self.contract_without_ctrl,
order_code="ORD-NO-CTRL-02",
order_status="pending",
order_date=date(2025, 1, 1),
delivery_date=date(2025, 1, 15),
description="Commande sans marché contrôlé",
)
self.itv3 = Intervention.objects.create(
code="ITV-NC01",
title="Intervention classique",
contract=self.contract_without_ctrl,
order=self.order_without_ctrl,
status="finished",
)
def test_order_detail_shows_controller_when_contract_has_auto_assign_control_team(self):
url = reverse("contracts:contracts_orders_detail", kwargs={"order_id": self.order_with_ctrl.id})
response = self.client.get(url, HTTP_ACCEPT_LANGUAGE='fr')
self.assertEqual(response.status_code, 200)
# Check th exists
self.assertContains(response, 'data-field="assigned_controller"')
self.assertContains(response, 'Contrôleur assigné')
# Check td contents
self.assertContains(response, 'Jean Contrôleur')
self.assertContains(response, 'Équipe Contrôle Nord')
def test_order_detail_hides_controller_when_contract_does_not_have_auto_assign_control_team(self):
url = reverse("contracts:contracts_orders_detail", kwargs={"order_id": self.order_without_ctrl.id})
response = self.client.get(url, HTTP_ACCEPT_LANGUAGE='fr')
self.assertEqual(response.status_code, 200)
# Check th does not exist
self.assertNotContains(response, 'data-field="assigned_controller"')
self.assertNotContains(response, 'Contrôleur assigné')

View file

@ -108,8 +108,8 @@ def contracts_detail(request, contract_id):
@order_view_required
def contracts_orders_detail(request, order_id):
order = ContractOrder.objects.get(id=order_id)
interventions = order.interventions.all()
order = ContractOrder.objects.select_related('contract').get(id=order_id)
interventions = order.interventions.all().select_related('assigned_controller', 'assigned_control_team')
# Normalize numeric values to Decimal to avoid float/Decimal mixing errors
total = order.total_amount

View file

@ -3,7 +3,7 @@
{# ========== 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"
<a href="{% url 'interventions:interventions_index' %}?status=finished&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>
@ -15,7 +15,7 @@
{# --- 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"
<a href="{% url 'interventions:interventions_index' %}?status=finished&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>
@ -50,7 +50,7 @@
{% if itv.title %}{{ itv.title|truncatechars:45 }}{% endif %}
{% if itv.thematic %} &middot; <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;">
<span class="badge {% if itv.status == 'corrected' %}bg-info-subtle text-info-emphasis border border-info-subtle{% elif itv.status == 'finished' %}bg-secondary-subtle text-secondary-emphasis border border-secondary-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>
@ -64,7 +64,7 @@
</ul>
<div class="card-footer text-center text-muted small bg-light">
<a href="{% url 'interventions:interventions_index' %}?status=processed&status=corrected"
<a href="{% url 'interventions:interventions_index' %}?status=finished&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>

View file

@ -397,6 +397,14 @@ class ControllerShortcutsWidgetTests(TestCase):
def test_controller_shortcuts_widget_context_and_rendering(self):
from interventions.models import Intervention
itv_finished = Intervention.objects.create(
code='ITV-CTRL-000',
maintain_type='corrective',
thematic=self.thematic,
status='finished',
assigned_control_team=self.team,
assigned_controller=self.member,
)
itv_processed = Intervention.objects.create(
code='ITV-CTRL-001',
maintain_type='corrective',
@ -427,16 +435,19 @@ class ControllerShortcutsWidgetTests(TestCase):
self.assertEqual(response.status_code, 200)
# Context counts
self.assertEqual(response.context['controller_interventions_count'], 2)
self.assertEqual(response.context['controller_interventions_count'], 3)
ctrl_codes = [itv.code for itv in response.context['controller_interventions_list']]
self.assertIn('ITV-CTRL-000', ctrl_codes)
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-000')
self.assertContains(response, 'ITV-CTRL-001')
self.assertContains(response, 'ITV-CTRL-002')
self.assertContains(response, 'status=finished&status=processed&status=corrected')
class StockSummaryWidgetTests(TestCase):

View file

@ -673,15 +673,15 @@ def index(request):
# ========== 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_filter = Q(status__in=['finished', 'processed', 'corrected'])
if not (user_role_names & {'admin', 'top_manager', 'manager'}):
control_team_ids = list(user_teams.values_list('id', flat=True)) if (user_teams and user_teams.exists()) else []
controller_q = Q(assigned_controller__user=request.user)
if control_team_ids:
controller_q |= Q(assigned_control_team_id__in=control_team_ids)
controller_filter &= controller_q
else:
controller_q = None
controller_interventions_qs = interventions.filter(controller_filter)
controller_interventions_count = controller_interventions_qs.count()
@ -692,19 +692,13 @@ def index(request):
)
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)
)
if controller_q is not None:
controller_to_correct_qs = controller_to_correct_qs.filter(controller_q)
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)
)
if controller_q is not None:
controller_validated_qs = controller_validated_qs.filter(controller_q)
controller_validated_count = controller_validated_qs.count()
# ========== Données widget external_interventions ==========