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:
parent
aa75bcbfeb
commit
b86d805cb5
12 changed files with 186 additions and 36 deletions
Binary file not shown.
|
|
@ -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."
|
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."
|
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"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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."
|
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."
|
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é"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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."
|
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."
|
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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -751,7 +751,11 @@
|
||||||
<th data-field="title">{% translate "Titre" %}</th>
|
<th data-field="title">{% translate "Titre" %}</th>
|
||||||
<th data-field="begin_date">{% translate "Début" %}</th>
|
<th data-field="begin_date">{% translate "Début" %}</th>
|
||||||
<th data-field="statut">{% translate "Statut" %}</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>
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for intervention in interventions %}
|
{% for intervention in interventions %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -759,10 +763,23 @@
|
||||||
<td>{{ intervention.title }}</td>
|
<td>{{ intervention.title }}</td>
|
||||||
<td>{{ intervention.begin_time|date:"d-m-Y H:i" }}</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>
|
<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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</thead>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -344,3 +344,119 @@ class GenerateClaimDeclarationTimelineTests(TestCase):
|
||||||
self.assertEqual(timeline_event_2.event_type, "status_change")
|
self.assertEqual(timeline_event_2.event_type, "status_change")
|
||||||
self.assertEqual(timeline_event_2.event_user, self.user)
|
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é')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ def contracts_detail(request, contract_id):
|
||||||
|
|
||||||
@order_view_required
|
@order_view_required
|
||||||
def contracts_orders_detail(request, order_id):
|
def contracts_orders_detail(request, order_id):
|
||||||
order = ContractOrder.objects.get(id=order_id)
|
order = ContractOrder.objects.select_related('contract').get(id=order_id)
|
||||||
interventions = order.interventions.all()
|
interventions = order.interventions.all().select_related('assigned_controller', 'assigned_control_team')
|
||||||
|
|
||||||
# Normalize numeric values to Decimal to avoid float/Decimal mixing errors
|
# Normalize numeric values to Decimal to avoid float/Decimal mixing errors
|
||||||
total = order.total_amount
|
total = order.total_amount
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{# ========== Widget raccourcis pour les contrôleurs ========== #}
|
{# ========== Widget raccourcis pour les contrôleurs ========== #}
|
||||||
<div class="card shadow-sm h-100" style="border-color: #00897b !important;">
|
<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;">
|
<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;">
|
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>
|
<i class="bi bi-patch-check-fill me-2 fs-5"></i>
|
||||||
<span>{% translate "Interventions à contrôler" %}</span>
|
<span>{% translate "Interventions à contrôler" %}</span>
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
{# --- Compteur principal et sous-raccourcis --- #}
|
{# --- Compteur principal et sous-raccourcis --- #}
|
||||||
<div class="text-center py-2 border-bottom bg-light bg-opacity-25">
|
<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">
|
class="text-decoration-none d-block">
|
||||||
<span class="display-5 fw-bold" style="color: #00897b !important;">{{ controller_interventions_count }}</span>
|
<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>
|
<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.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 %}
|
{% if itv.thematic %} · <span class="badge bg-secondary-subtle text-secondary py-0 px-1">{{ itv.thematic.code }}</span>{% endif %}
|
||||||
</span>
|
</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 }}
|
{{ itv.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="card-footer text-center text-muted small bg-light">
|
<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;">
|
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" %}
|
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir toutes les interventions à contrôler" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,14 @@ class ControllerShortcutsWidgetTests(TestCase):
|
||||||
|
|
||||||
def test_controller_shortcuts_widget_context_and_rendering(self):
|
def test_controller_shortcuts_widget_context_and_rendering(self):
|
||||||
from interventions.models import Intervention
|
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(
|
itv_processed = Intervention.objects.create(
|
||||||
code='ITV-CTRL-001',
|
code='ITV-CTRL-001',
|
||||||
maintain_type='corrective',
|
maintain_type='corrective',
|
||||||
|
|
@ -427,16 +435,19 @@ class ControllerShortcutsWidgetTests(TestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Context counts
|
# 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']]
|
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-001', ctrl_codes)
|
||||||
self.assertIn('ITV-CTRL-002', ctrl_codes)
|
self.assertIn('ITV-CTRL-002', ctrl_codes)
|
||||||
self.assertNotIn('ITV-CTRL-003', ctrl_codes)
|
self.assertNotIn('ITV-CTRL-003', ctrl_codes)
|
||||||
|
|
||||||
# Content rendered in HTML
|
# Content rendered in HTML
|
||||||
self.assertContains(response, 'Interventions à contrôler')
|
self.assertContains(response, 'Interventions à contrôler')
|
||||||
|
self.assertContains(response, 'ITV-CTRL-000')
|
||||||
self.assertContains(response, 'ITV-CTRL-001')
|
self.assertContains(response, 'ITV-CTRL-001')
|
||||||
self.assertContains(response, 'ITV-CTRL-002')
|
self.assertContains(response, 'ITV-CTRL-002')
|
||||||
|
self.assertContains(response, 'status=finished&status=processed&status=corrected')
|
||||||
|
|
||||||
|
|
||||||
class StockSummaryWidgetTests(TestCase):
|
class StockSummaryWidgetTests(TestCase):
|
||||||
|
|
|
||||||
|
|
@ -673,15 +673,15 @@ def index(request):
|
||||||
|
|
||||||
# ========== Données widget controller_shortcuts ==========
|
# ========== Données widget controller_shortcuts ==========
|
||||||
user_role_names = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
user_role_names = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
||||||
controller_filter = Q(status__in=['processed', 'corrected'])
|
controller_filter = Q(status__in=['finished', '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'}):
|
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
|
controller_filter &= controller_q
|
||||||
|
else:
|
||||||
|
controller_q = None
|
||||||
|
|
||||||
controller_interventions_qs = interventions.filter(controller_filter)
|
controller_interventions_qs = interventions.filter(controller_filter)
|
||||||
controller_interventions_count = controller_interventions_qs.count()
|
controller_interventions_count = controller_interventions_qs.count()
|
||||||
|
|
@ -692,19 +692,13 @@ def index(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
controller_to_correct_qs = interventions.filter(status='to_be_corrected')
|
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'}):
|
if controller_q is not None:
|
||||||
controller_to_correct_qs = controller_to_correct_qs.filter(
|
controller_to_correct_qs = controller_to_correct_qs.filter(controller_q)
|
||||||
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_to_correct_count = controller_to_correct_qs.count()
|
||||||
|
|
||||||
controller_validated_qs = interventions.filter(status='validated')
|
controller_validated_qs = interventions.filter(status='validated')
|
||||||
if user_teams and user_teams.exists() and not (user_role_names & {'admin', 'top_manager', 'manager'}):
|
if controller_q is not None:
|
||||||
controller_validated_qs = controller_validated_qs.filter(
|
controller_validated_qs = controller_validated_qs.filter(controller_q)
|
||||||
Q(assigned_control_team_id__in=control_team_ids) |
|
|
||||||
Q(assigned_controller__user=request.user)
|
|
||||||
)
|
|
||||||
controller_validated_count = controller_validated_qs.count()
|
controller_validated_count = controller_validated_qs.count()
|
||||||
|
|
||||||
# ========== Données widget external_interventions ==========
|
# ========== Données widget external_interventions ==========
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue