diff --git a/loko/contracts/models.py b/loko/contracts/models.py
index 1dcf2a7..98b90bd 100644
--- a/loko/contracts/models.py
+++ b/loko/contracts/models.py
@@ -344,6 +344,14 @@ class Contract(models.Model):
def __str__(self):
return f"Contrat {self.contract_number} - {self.company.name}"
+
+ @property
+ def display_name(self):
+ desc = (self.description or '').strip()
+ if desc:
+ truncated = desc[:45] + ('…' if len(desc) > 45 else '')
+ return f"{self.contract_number} ({truncated})"
+ return self.contract_number
class Meta:
ordering = ['contract_number']
diff --git a/loko/contracts/tests.py b/loko/contracts/tests.py
index b49ea74..abbb7e1 100644
--- a/loko/contracts/tests.py
+++ b/loko/contracts/tests.py
@@ -195,3 +195,40 @@ class TeamViewsAjaxTests(TestCase):
member = CompanyMember.objects.get(user=target_user)
self.assertEqual(member.name, 'New Target Member')
self.assertIn(team_b, member.teams.all())
+
+
+class ContractDisplayNameTests(TestCase):
+ def setUp(self):
+ from datetime import date
+ from contracts.models import Contract, Company
+ self.company = Company.objects.create(name="Company Tests")
+ self.contract_no_desc = Contract.objects.create(
+ company=self.company,
+ contract_number="CTR-001",
+ start_date=date(2025, 1, 1),
+ end_date=date(2026, 1, 1)
+ )
+ self.contract_short_desc = Contract.objects.create(
+ company=self.company,
+ contract_number="CTR-002",
+ description="Entretien espaces verts",
+ start_date=date(2025, 1, 1),
+ end_date=date(2026, 1, 1)
+ )
+ self.contract_long_desc = Contract.objects.create(
+ company=self.company,
+ contract_number="CTR-003",
+ description="Travaux et maintenance préventive et corrective de la signalisation lumineuse tricolore",
+ start_date=date(2025, 1, 1),
+ end_date=date(2026, 1, 1)
+ )
+
+ def test_display_name_no_description(self):
+ self.assertEqual(self.contract_no_desc.display_name, "CTR-001")
+
+ def test_display_name_short_description(self):
+ self.assertEqual(self.contract_short_desc.display_name, "CTR-002 (Entretien espaces verts)")
+
+ def test_display_name_long_description_truncated(self):
+ self.assertTrue(self.contract_long_desc.display_name.startswith("CTR-003 (Travaux et maintenance préventive et correct"))
+ self.assertTrue(self.contract_long_desc.display_name.endswith("…)"))
diff --git a/loko/interventions/static/interventions/detail/cms_edit_mode.js b/loko/interventions/static/interventions/detail/cms_edit_mode.js
index 4ae46be..5986e27 100644
--- a/loko/interventions/static/interventions/detail/cms_edit_mode.js
+++ b/loko/interventions/static/interventions/detail/cms_edit_mode.js
@@ -87,9 +87,13 @@
contracts.forEach(contract => {
const option = document.createElement('option');
option.value = contract.id;
- option.textContent = contract.contract_number;
+ option.textContent = contract.label || contract.contract_number;
contractSelect.appendChild(option);
});
+
+ if (contracts.length === 1) {
+ contractSelect.value = String(contracts[0].id);
+ }
} catch (error) {
console.error('Error loading contracts:', error);
diff --git a/loko/interventions/static/interventions/detail/fields.js b/loko/interventions/static/interventions/detail/fields.js
index 15ae475..870fe80 100644
--- a/loko/interventions/static/interventions/detail/fields.js
+++ b/loko/interventions/static/interventions/detail/fields.js
@@ -213,9 +213,12 @@ document.addEventListener("DOMContentLoaded", () => {
contracts.forEach(contract => {
const opt = document.createElement("option");
opt.value = contract.id;
- opt.textContent = contract.contract_number;
+ opt.textContent = contract.label || contract.contract_number;
contractSelect.appendChild(opt);
});
+ if (contracts.length === 1) {
+ contractSelect.value = String(contracts[0].id);
+ }
}
})
.catch(() => {
diff --git a/loko/interventions/static/interventions/detail/layout.js b/loko/interventions/static/interventions/detail/layout.js
index cb07654..4bee3c3 100644
--- a/loko/interventions/static/interventions/detail/layout.js
+++ b/loko/interventions/static/interventions/detail/layout.js
@@ -125,12 +125,14 @@
contracts.forEach((contract) => {
const option = document.createElement('option');
option.value = String(contract.id);
- option.textContent = contract.contract_number;
+ option.textContent = contract.label || contract.contract_number;
contractSelect.appendChild(option);
});
if (selectedContractId) {
contractSelect.value = String(selectedContractId);
+ } else if (contracts.length === 1) {
+ contractSelect.value = String(contracts[0].id);
}
} catch (error) {
console.error('Quick edit contract loading error:', error);
diff --git a/loko/interventions/templates/interventions/detail/card_info.html b/loko/interventions/templates/interventions/detail/card_info.html
index bfed468..594c7dd 100644
--- a/loko/interventions/templates/interventions/detail/card_info.html
+++ b/loko/interventions/templates/interventions/detail/card_info.html
@@ -330,7 +330,7 @@
{% for contract in contracts %}
{% endfor %}
@@ -403,14 +403,14 @@
{% translate "Contrat" %}:
- {{ intervention.contract.contract_number|default:'-' }}
+ {% if intervention.contract %}{{ intervention.contract.contract_number }}{% if intervention.contract.description %} ({{ intervention.contract.description|truncatechars:45 }}){% endif %}{% else %}-{% endif %}
diff --git a/loko/interventions/tests/test_visibility_permissions.py b/loko/interventions/tests/test_visibility_permissions.py
index 8fc6ecc..d3eae01 100644
--- a/loko/interventions/tests/test_visibility_permissions.py
+++ b/loko/interventions/tests/test_visibility_permissions.py
@@ -530,6 +530,77 @@ class DefaultContractTests(TestCase):
self.assertEqual(itv.assigned_provider, self.company)
+class GetContractsByProviderTests(TestCase):
+ def setUp(self):
+ from datetime import date
+ from django.contrib.auth import get_user_model
+ from common.models import UserConfig, UserContractAccess, Role
+ from contracts.models import Contract, Company
+
+ self.User = get_user_model()
+ self.manager_role, _ = Role.objects.get_or_create(name='manager')
+
+ self.company = Company.objects.create(name="Provider A")
+ self.contract1 = Contract.objects.create(
+ contract_number="C-001",
+ description="Short desc",
+ company=self.company,
+ start_date=date(2025, 1, 1),
+ end_date=date(2030, 1, 1)
+ )
+ self.contract2 = Contract.objects.create(
+ contract_number="C-002",
+ description="Another desc",
+ company=self.company,
+ start_date=date(2025, 1, 1),
+ end_date=date(2030, 1, 1)
+ )
+
+ # User limited to contracts
+ self.limited_user = self.User.objects.create_user(username='limited-user', password='pwd')
+ self.limited_config = UserConfig.objects.create(
+ user=self.limited_user,
+ is_intern=True,
+ limit_interventions_to_contracts=True,
+ )
+ self.limited_config.roles.add(self.manager_role)
+ # Give access only to contract1
+ UserContractAccess.objects.create(user_config=self.limited_config, contract=self.contract1, can_view_interventions=True)
+
+ # Unrestricted internal user
+ self.unlimited_user = self.User.objects.create_user(username='unlimited-user', password='pwd')
+ self.unlimited_config = UserConfig.objects.create(
+ user=self.unlimited_user,
+ is_intern=True,
+ limit_interventions_to_contracts=False,
+ )
+ self.unlimited_config.roles.add(self.manager_role)
+
+ def test_limited_user_only_gets_single_accessible_contract(self):
+ self.client.login(username='limited-user', password='pwd')
+ url = reverse('interventions:get_contracts_by_provider')
+ response = self.client.get(f"{url}?provider_id={self.company.id}")
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(len(data), 1)
+ self.assertEqual(data[0]['id'], self.contract1.id)
+ self.assertEqual(data[0]['contract_number'], "C-001")
+ self.assertEqual(data[0]['description'], "Short desc")
+ self.assertEqual(data[0]['label'], "C-001 (Short desc)")
+
+ def test_unlimited_user_gets_all_contracts(self):
+ self.client.login(username='unlimited-user', password='pwd')
+ url = reverse('interventions:get_contracts_by_provider')
+ response = self.client.get(f"{url}?provider_id={self.company.id}")
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(len(data), 2)
+ contract_numbers = [c['contract_number'] for c in data]
+ self.assertIn("C-001", contract_numbers)
+ self.assertIn("C-002", contract_numbers)
+
+
+
diff --git a/loko/interventions/views/detail.py b/loko/interventions/views/detail.py
index ae17e35..a7d3a71 100644
--- a/loko/interventions/views/detail.py
+++ b/loko/interventions/views/detail.py
@@ -230,8 +230,8 @@ def intervention_detail(request, intervention_id):
contract_id = intervention.order.contract.id if intervention.order else None
# Limiter les providers et contracts selon les permissions de l'utilisateur
- if user_config.is_intern:
- # Utilisateurs internes : accès à tous les prestataires et contrats
+ if user_config.is_intern and not user_config.limit_interventions_to_contracts:
+ # Utilisateurs internes non limités aux contrats : accès à tous les prestataires et contrats
providers = Company.objects.all()
if intervention.thematic:
providers = providers.filter(contracts__thematics=intervention.thematic).distinct()
@@ -240,9 +240,9 @@ def intervention_detail(request, intervention_id):
# Filtrer par thématique si l'intervention en a une
if intervention.thematic:
contracts_qs = contracts_qs.filter(thematics=intervention.thematic)
- contracts = contracts_qs.distinct().values("id", "contract_number")
+ contracts = contracts_qs.distinct()
else:
- # Utilisateurs externes (external_managers) : limiter aux contrats accessibles
+ # Utilisateurs limités aux contrats (externes ou avec limit_interventions_to_contracts=True)
accessible_contracts_ids = UserContractAccess.objects.filter(
user_config=user_config,
can_view_interventions=True
@@ -264,7 +264,7 @@ def intervention_detail(request, intervention_id):
else:
contracts_qs = accessible_contracts_qs
- contracts = contracts_qs.distinct().values("id", "contract_number")
+ contracts = contracts_qs.distinct()
projects = Project.objects.filter(thematics=intervention.thematic) if intervention.thematic else None
project_interv = ProjectIntervention.objects.filter(intervention=intervention).first()
@@ -2572,7 +2572,16 @@ def update_intervention(request, intervention_id):
# Handle notes separately (special case)
notes_data = data.pop('notes', None)
- for field, new_value in data.items():
+ # Sort fields to process assigned_provider before contract
+ def _field_priority(item):
+ f_name = item[0]
+ if f_name == 'assigned_provider':
+ return 0
+ if f_name == 'contract':
+ return 1
+ return 2
+
+ for field, new_value in sorted(data.items(), key=_field_priority):
if field not in allowed_update_fields:
errors.append(_('You do not have permission to update field: %(field)s') % {'field': field})
permission_denied = True
@@ -2724,16 +2733,16 @@ def get_contracts_by_provider(request):
return JsonResponse({"error": _("Missing provider_id")}, status=400)
user_config = get_object_or_404(UserConfig, user=request.user)
- if not user_config.roles.filter(name__in=['admin', 'manager', 'operator', 'external_manager', 'controller']).exists():
+ if not user_config.roles.filter(name__in=['admin', 'manager', 'operator', 'external_manager', 'controller', 'top_manager']).exists() and not request.user.is_superuser:
return JsonResponse({"error": "Permission denied"}, status=403)
- if user_config.roles.filter(name__in=['external_manager']).exists():
+ if user_config.limit_interventions_to_contracts or not user_config.is_intern:
accessible_contracts_ids = UserContractAccess.objects.filter(
user_config=user_config,
contract__company_id=provider_id,
can_view_interventions=True
).values_list('contract__id', flat=True)
- contracts = Contract.objects.filter(id__in=accessible_contracts_ids)
+ contracts = Contract.objects.filter(id__in=accessible_contracts_ids, company_id=provider_id)
else:
contracts = Contract.objects.filter(company_id=provider_id)
@@ -2741,9 +2750,24 @@ def get_contracts_by_provider(request):
if thematic_id:
contracts = contracts.filter(thematics__id=thematic_id)
- contracts = contracts.values("id", "contract_number")
+ contracts = contracts.distinct().order_by("contract_number")
- return JsonResponse(list(contracts), safe=False)
+ contract_data = []
+ for c in contracts:
+ desc = (c.description or '').strip()
+ if desc:
+ truncated = desc[:45] + ('…' if len(desc) > 45 else '')
+ label = f"{c.contract_number} ({truncated})"
+ else:
+ label = c.contract_number
+ contract_data.append({
+ "id": c.id,
+ "contract_number": c.contract_number,
+ "description": desc,
+ "label": label,
+ })
+
+ return JsonResponse(contract_data, safe=False)
def add_custom_operation(request, intervention_id):
diff --git a/loko/interventions/views/helpers.py b/loko/interventions/views/helpers.py
index 4755f73..7eac521 100644
--- a/loko/interventions/views/helpers.py
+++ b/loko/interventions/views/helpers.py
@@ -2431,8 +2431,8 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
company = Company.objects.filter(id=new_value).first()
if company and intervention.assigned_provider != company:
- # Pour les external_managers, vérifier qu'ils ont accès à au moins un contrat de ce prestataire
- if user_config and not user_config.is_intern:
+ # Vérifier l'accès aux contrats pour les utilisateurs limités aux contrats ou externes
+ if user_config and (user_config.limit_interventions_to_contracts or not user_config.is_intern):
has_access = UserContractAccess.objects.filter(
user_config=user_config,
contract__company=company,
@@ -2455,8 +2455,8 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
contract = Contract.objects.filter(id=new_value).first()
if contract:
- # Pour les external_managers, vérifier qu'ils ont accès à ce contrat
- if user_config and not user_config.is_intern:
+ # Vérifier l'accès au contrat pour les utilisateurs limités aux contrats ou externes
+ if user_config and (user_config.limit_interventions_to_contracts or not user_config.is_intern):
has_access = UserContractAccess.objects.filter(
user_config=user_config,
contract=contract,