feat: improve contract selection with descriptions, enhanced visibility filtering, and user access controls
This commit is contained in:
parent
8b4c854416
commit
016ce202b4
9 changed files with 170 additions and 21 deletions
|
|
@ -345,6 +345,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']
|
||||
|
||||
|
|
|
|||
|
|
@ -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("…)"))
|
||||
|
|
|
|||
|
|
@ -87,10 +87,14 @@
|
|||
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);
|
||||
contractSelect.innerHTML = '<option value="">-</option>';
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@
|
|||
<option value="">-</option>
|
||||
{% for contract in contracts %}
|
||||
<option value="{{ contract.id }}" {% if contract.id == intervention.contract.id %}selected{% endif %}>
|
||||
{{ contract.contract_number }}
|
||||
{{ contract.contract_number }}{% if contract.description %} ({{ contract.description|truncatechars:45 }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
@ -403,14 +403,14 @@
|
|||
<button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-file-earmark-text"></i></button>
|
||||
<span class="label mx-1">{% translate "Contrat" %}:</span>
|
||||
<div class="field-display">
|
||||
<span class="field-value" id="contractDisplay">{{ intervention.contract.contract_number|default:'-' }}</span>
|
||||
<span class="field-value" id="contractDisplay">{% if intervention.contract %}{{ intervention.contract.contract_number }}{% if intervention.contract.description %} ({{ intervention.contract.description|truncatechars:45 }}){% endif %}{% else %}-{% endif %}</span>
|
||||
</div>
|
||||
<div class="field-edit d-none" style="flex: 1;">
|
||||
<select class="form-select form-select-sm">
|
||||
<option value="">-</option>
|
||||
{% for contract in contracts %}
|
||||
<option value="{{ contract.id }}" {% if contract.id == intervention.contract.id %}selected{% endif %}>
|
||||
{{ contract.contract_number }}
|
||||
{{ contract.contract_number }}{% if contract.description %} ({{ contract.description|truncatechars:45 }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue