feat: add default contract support to user configurations and auto-assign during intervention creation

This commit is contained in:
kdeterme 2026-08-12 05:26:50 +02:00
parent 6cfd80d61e
commit 718403c295
8 changed files with 141 additions and 2 deletions

View file

@ -617,7 +617,7 @@ class UserConfigAdmin(admin.ModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('user', 'is_intern', 'roles', 'company', 'company_members_link', 'company_teams_link', 'default_thematic', 'generic_email') 'fields': ('user', 'is_intern', 'roles', 'company', 'company_members_link', 'company_teams_link', 'default_thematic', 'default_contract', 'generic_email')
}), }),
('Accès aux vues', { ('Accès aux vues', {
'fields': ('accessible_views',), 'fields': ('accessible_views',),

View file

@ -0,0 +1,20 @@
# Generated by Django 6.0.7 on 2026-08-12 03:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0008_reset_can_inspect_for_non_admins'),
('contracts', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='userconfig',
name='default_contract',
field=models.ForeignKey(blank=True, help_text="Contrat assigné par défaut lors de la création d'une intervention si aucun contrat n'est résolu automatiquement.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_user_accesses', to='contracts.contract', verbose_name='Contrat par défaut'),
),
]

View file

@ -98,6 +98,7 @@ class UserConfig(models.Model):
is_intern = models.BooleanField(default=False) is_intern = models.BooleanField(default=False)
roles = models.ManyToManyField(Role, blank=True) roles = models.ManyToManyField(Role, blank=True)
default_thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True, related_name="default_user_accesses") default_thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True, related_name="default_user_accesses")
default_contract = models.ForeignKey("contracts.Contract", on_delete=models.SET_NULL, null=True, blank=True, related_name="default_user_accesses", verbose_name="Contrat par défaut", help_text="Contrat assigné par défaut lors de la création d'une intervention si aucun contrat n'est résolu automatiquement.")
thematics = models.ManyToManyField("common.Thematic", through="UserThematics", related_name="user_accesses") thematics = models.ManyToManyField("common.Thematic", through="UserThematics", related_name="user_accesses")
contracts = models.ManyToManyField("contracts.Contract", through="UserContractAccess", related_name="user_accesses") contracts = models.ManyToManyField("contracts.Contract", through="UserContractAccess", related_name="user_accesses")
warehouses = models.ManyToManyField("stock.Warehouse", through="UserWarehouseAccess", related_name="user_accesses", blank=True) warehouses = models.ManyToManyField("stock.Warehouse", through="UserWarehouseAccess", related_name="user_accesses", blank=True)
@ -479,6 +480,20 @@ class UserConfig(models.Model):
return self.userthematics.filter(thematic=thematic, can_inspect=True).exists() return self.userthematics.filter(thematic=thematic, can_inspect=True).exists()
def get_default_contract(self):
"""
Retourne le contrat par défaut de l'utilisateur.
Si aucun contrat par défaut n'est spécifié mais que l'utilisateur a limit_interventions_to_contracts=True
et n'a accès qu'à un seul contrat (via UserContractAccess), retourne ce contrat unique.
"""
if self.default_contract_id:
return self.default_contract
if self.limit_interventions_to_contracts:
allowed_contracts = list(self.contracts.all()[:2])
if len(allowed_contracts) == 1:
return allowed_contracts[0]
return None
class UserThematics(models.Model): class UserThematics(models.Model):

View file

@ -407,7 +407,7 @@ def get_allowed_update_fields(user, intervention):
_initial_statuses = ('in_preparation', 'to_be_approved') _initial_statuses = ('in_preparation', 'to_be_approved')
if _thematic_allows_planning_without_contract: if _thematic_allows_planning_without_contract:
_initial_statuses = ('in_preparation', 'to_be_approved', 'to_be_planned') _initial_statuses = ('in_preparation', 'to_be_approved', 'to_be_planned')
if intervention.status in _initial_statuses and user_config.is_intern: if intervention.status in _initial_statuses and (user_config.is_intern or (intervention.contract is None and intervention.created_by_id == user.id)):
allowed_fields.update(['assigned_provider', 'contract', 'maintain_type']) allowed_fields.update(['assigned_provider', 'contract', 'maintain_type'])
# Localisation : modifiable par les internes sur toutes les interventions non validées # Localisation : modifiable par les internes sur toutes les interventions non validées

View file

@ -469,5 +469,67 @@ class CreatorVisibilityTests(TestCase):
self.assertNotIn(itv, filter_viewable_interventions_for_user(self.user2)) self.assertNotIn(itv, filter_viewable_interventions_for_user(self.user2))
class DefaultContractTests(TestCase):
def setUp(self):
from datetime import date
from django.contrib.auth import get_user_model
from common.models import UserConfig, UserContractAccess
from contracts.models import Contract, Company
self.User = get_user_model()
self.user = self.User.objects.create_user(username='default-contract-user', password='pwd')
self.company = Company.objects.create(name='Test Company')
self.contract1 = Contract.objects.create(contract_number='CNT-001', company=self.company, start_date=date(2025, 1, 1), end_date=date(2030, 1, 1))
self.contract2 = Contract.objects.create(contract_number='CNT-002', company=self.company, start_date=date(2025, 1, 1), end_date=date(2030, 1, 1))
self.config = UserConfig.objects.create(
user=self.user,
is_intern=False,
limit_interventions_to_contracts=True,
)
def test_get_default_contract_explicit(self):
self.config.default_contract = self.contract1
self.config.save()
self.assertEqual(self.config.get_default_contract(), self.contract1)
def test_get_default_contract_single_contract_fallback(self):
from common.models import UserContractAccess
UserContractAccess.objects.create(user_config=self.config, contract=self.contract2)
# With only 1 contract in UserContractAccess, get_default_contract returns contract2
self.assertEqual(self.config.get_default_contract(), self.contract2)
# With 2 contracts, get_default_contract returns None if default_contract not set
UserContractAccess.objects.create(user_config=self.config, contract=self.contract1)
self.assertIsNone(self.config.get_default_contract())
def test_observation_conversion_assigns_default_contract(self):
from observations.models import Observation
from interventions.models import Symptom
from common.models import Thematic, UserContractAccess
self.config.default_contract = self.contract1
self.config.save()
UserContractAccess.objects.create(user_config=self.config, contract=self.contract1)
thematic = Thematic.objects.create(code='roads_def', name_fr='Voirie Def')
symptom = Symptom.objects.create(name_fr='Symptom test', thematic=thematic)
obs = Observation.objects.create(
symptom=symptom,
thematic=thematic,
created_by=self.user,
latitude=50.85,
longitude=4.35,
address='123 Test Street'
)
obs.create_auto_intervention()
itv = obs.intervention
self.assertIsNotNone(itv)
self.assertEqual(itv.contract, self.contract1)
self.assertEqual(itv.assigned_provider, self.company)

View file

@ -1798,6 +1798,15 @@ def interventions_add(request, thematic_code):
intervention.assigned_provider = its_contract.contract.company intervention.assigned_provider = its_contract.contract.company
intervention.save(update_fields=['contract', 'assigned_provider']) intervention.save(update_fields=['contract', 'assigned_provider'])
# Fallback contrat par défaut pour les utilisateurs à visibilité limitée aux contrats
if not intervention.contract and user_config and user_config.limit_interventions_to_contracts:
default_contract = user_config.get_default_contract()
if default_contract:
intervention.contract = default_contract
if not intervention.assigned_provider:
intervention.assigned_provider = default_contract.company
intervention.save(update_fields=['contract', 'assigned_provider'])
# -------- Forcer le statut 'à approuver' pour les utilisateurs externes -------- # -------- Forcer le statut 'à approuver' pour les utilisateurs externes --------
# Applicable si limit_interventions_to_contracts est activé et que l'utilisateur # Applicable si limit_interventions_to_contracts est activé et que l'utilisateur
# n'a pas accès au contrat résolu (ou à celui du symptôme). # n'a pas accès au contrat résolu (ou à celui du symptôme).

View file

@ -1548,6 +1548,16 @@ def add_repair_intervention_mobile(request):
break break
with transaction.atomic(): with transaction.atomic():
if not contract:
from common.models import UserConfig
user_config = UserConfig.objects.filter(user=request.user).first()
if user_config and user_config.limit_interventions_to_contracts:
default_contract = user_config.get_default_contract()
if default_contract:
contract = default_contract
if not assigned_provider:
assigned_provider = default_contract.company
new_intervention = Intervention( new_intervention = Intervention(
title=title, title=title,
description=_("Intervention de réparation créée depuis l'application mobile."), description=_("Intervention de réparation créée depuis l'application mobile."),
@ -1775,6 +1785,16 @@ def add_intervention_mobile(request, thematic_code):
return _render_form([_("Catégorie non trouvée.")]) return _render_form([_("Catégorie non trouvée.")])
with transaction.atomic(): with transaction.atomic():
mobile_contract = None
mobile_provider = None
from common.models import UserConfig
user_config = UserConfig.objects.filter(user=request.user).first()
if user_config and user_config.limit_interventions_to_contracts:
default_contract = user_config.get_default_contract()
if default_contract:
mobile_contract = default_contract
mobile_provider = default_contract.company
new_intervention = Intervention( new_intervention = Intervention(
title=title, title=title,
description='', description='',
@ -1783,6 +1803,8 @@ def add_intervention_mobile(request, thematic_code):
priority='3', priority='3',
thematic=thematic, thematic=thematic,
asset_category=category, asset_category=category,
contract=mobile_contract,
assigned_provider=mobile_provider,
lon=lon, lon=lon,
lat=lat, lat=lat,
location_code=location_code, location_code=location_code,

View file

@ -538,6 +538,17 @@ class Observation(models.Model):
assigned_provider=self.symptom.provider, assigned_provider=self.symptom.provider,
) )
if not intervention.contract and self.created_by:
from common.models import UserConfig
user_config = UserConfig.objects.filter(user=self.created_by).first()
if user_config and user_config.limit_interventions_to_contracts:
default_contract = user_config.get_default_contract()
if default_contract:
intervention.contract = default_contract
if not intervention.assigned_provider:
intervention.assigned_provider = default_contract.company
intervention.save(update_fields=['contract', 'assigned_provider'])
# Link to observation and set observation status to 'in_progress' # Link to observation and set observation status to 'in_progress'
self.intervention = intervention self.intervention = intervention
self.status = 'in_progress' self.status = 'in_progress'