loko/streetup/common/management/commands/import_user_configs.py
2026-07-22 14:48:40 +02:00

402 lines
16 KiB
Python

"""
Importe les configurations utilisateur depuis un fichier JSON produit par
export_user_configs.
Modes :
create (défaut) — crée uniquement les configs absentes ; ignore les existantes
update — crée les absentes ET met à jour les existantes (thématiques /
contrats / entrepôts / équipes et leurs permissions par statut)
reset — supprime les relations (thématiques, contrats, etc.) des configs
existantes avant de les recréer depuis le JSON ; ne supprime pas
le UserConfig lui-même ni le compte User.
Dans tous les modes, un User inexistant est IGNORÉ (l'import ne crée pas de
comptes utilisateurs pour ne pas contourner la gestion des mots de passe).
Usage :
python manage.py import_user_configs exports/user_configs.json
python manage.py import_user_configs exports/user_configs.json --mode update
python manage.py import_user_configs exports/user_configs.json --mode reset --no-input
python manage.py import_user_configs exports/user_configs.json --dry-run
"""
import json
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
from django.db import transaction
from common.models import (
UserConfig, Role, UserThematics, UserContractAccess,
UserWarehouseAccess, UserWarehouseLocationAccess, UserThematicStatusPermission,
UserContractStatusPermission, AppView,
)
from contracts.models import UserCompanyTeamAccess, CompanyTeam, Company
from contracts.models import Contract # contract_number lookup
User = get_user_model()
SUPPORTED_VERSIONS = {1, 2}
class Command(BaseCommand):
help = 'Importe les configurations utilisateur depuis un fichier JSON.'
def add_arguments(self, parser):
parser.add_argument(
'file',
metavar='FICHIER',
help='Chemin vers le fichier JSON exporté par export_user_configs.',
)
parser.add_argument(
'--mode',
choices=['create', 'update', 'reset'],
default='create',
help=(
'create : crée uniquement les lignes manquantes (défaut). '
'update : crée + met à jour les existantes. '
'reset : vide les relations existantes avant de recréer.'
),
)
parser.add_argument(
'--dry-run',
action='store_true',
default=False,
dest='dry_run',
help='Affiche ce qui serait fait sans rien écrire.',
)
parser.add_argument(
'--no-input', '--noinput',
action='store_true',
default=False,
dest='no_input',
)
# ------------------------------------------------------------------
def handle(self, *args, **options):
mode = options['mode']
dry_run = options['dry_run']
no_input = options['no_input']
if dry_run:
self.stdout.write(self.style.WARNING('Mode DRY-RUN : aucune modification.\n'))
# Charger le JSON
try:
with open(options['file'], encoding='utf-8') as fh:
payload = json.load(fh)
except FileNotFoundError:
raise CommandError(f"Fichier introuvable : {options['file']}")
except json.JSONDecodeError as exc:
raise CommandError(f"JSON invalide : {exc}")
version = payload.get('version', 1)
if version not in SUPPORTED_VERSIONS:
raise CommandError(f"Version {version} non supportée (supportées : {SUPPORTED_VERSIONS}).")
users_data = payload.get('users', [])
self.stdout.write(f"{len(users_data)} configuration(s) à traiter (mode={mode}).\n")
if mode == 'reset' and not dry_run:
if not no_input:
confirm = input(
'ATTENTION : --mode reset effacera toutes les relations '
'(thématiques, contrats, entrepôts, équipes, rôles, vues) '
'des configs présentes dans le fichier avant de les recréer.\n'
'Continuer ? [o/N] '
)
if confirm.strip().lower() not in ('o', 'oui', 'y', 'yes'):
self.stdout.write(self.style.WARNING('Opération annulée.'))
return
stats = {'created': 0, 'updated': 0, 'skipped': 0, 'missing_user': 0, 'errors': 0}
with transaction.atomic():
for entry in users_data:
try:
self._process_entry(entry, mode, dry_run, stats)
except Exception as exc:
self.stderr.write(self.style.ERROR(
f'Erreur pour {entry.get("username", "?")} : {exc}'
))
stats['errors'] += 1
if not dry_run:
raise # rollback
self.stdout.write('')
self.stdout.write(self.style.SUCCESS(
f'Résultat — créées: {stats["created"]} | mises à jour: {stats["updated"]} '
f'| ignorées: {stats["skipped"]} | utilisateur manquant: {stats["missing_user"]} '
f'| erreurs: {stats["errors"]}'
))
# ------------------------------------------------------------------
def _process_entry(self, entry, mode, dry_run, stats):
username = entry.get('username') or ''
email = entry.get('email') or ''
# Résoudre l'utilisateur (ne crée pas de User)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
if email:
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
self.stdout.write(
self.style.WARNING(f' [IGNORÉ] utilisateur introuvable : {username} / {email}')
)
stats['missing_user'] += 1
return
else:
self.stdout.write(
self.style.WARNING(f' [IGNORÉ] utilisateur introuvable : {username}')
)
stats['missing_user'] += 1
return
# Chercher ou créer le UserConfig
try:
cfg = UserConfig.objects.get(user=user)
exists = True
except UserConfig.DoesNotExist:
exists = False
cfg = None
if exists and mode == 'create':
self.stdout.write(f' [IGNORÉ] config existante pour {username}')
stats['skipped'] += 1
return
label = 'MAJ' if exists else 'CRÉE'
self.stdout.write(f' [{label}] {username}')
if dry_run:
stats['updated' if exists else 'created'] += 1
return
# Résoudre les FK optionnelles
default_thematic = None
if entry.get('default_thematic'):
from common.models import Thematic
default_thematic = Thematic.objects.filter(code=entry['default_thematic']).first()
company_obj = None
if entry.get('company'):
company_obj, created = Company.objects.get_or_create(name=entry['company'])
if created:
self.stdout.write(self.style.WARNING(
f' [+] société créée : {entry["company"]}'
))
scalar_fields = {
'is_intern': entry.get('is_intern', False),
'generic_email': entry.get('generic_email'),
'limit_assets_to_contracts': entry.get('limit_assets_to_contracts', True),
'limit_interventions_to_contracts': entry.get('limit_interventions_to_contracts', True),
'limit_shortcuts_to_contracts': entry.get('limit_shortcuts_to_contracts', False),
'hide_intervention_map_by_default': entry.get('hide_intervention_map_by_default', False),
'auto_logout_hours': entry.get('auto_logout_hours'),
'can_add_unique_tag_documents': entry.get('can_add_unique_tag_documents', False),
'default_thematic': default_thematic,
'company': company_obj,
}
if not exists:
cfg = UserConfig.objects.create(user=user, **scalar_fields)
else:
for field, value in scalar_fields.items():
setattr(cfg, field, value)
cfg.save()
# Rôles
roles = Role.objects.filter(name__in=entry.get('roles', []))
cfg.roles.set(roles)
# Vues accessibles
views = AppView.objects.filter(code__in=entry.get('accessible_views', []))
cfg.accessible_views.set(views)
# Linked configs (best-effort — peut rater si l'user n'existe pas encore)
linked_usernames = entry.get('linked_configs', [])
linked_cfgs = UserConfig.objects.filter(user__username__in=linked_usernames)
cfg.linked_configs.set(linked_cfgs)
# Relations avec reset possible
if mode == 'reset' and exists:
cfg.userthematics.all().delete()
cfg.usercontractaccess_set.all().delete()
cfg.userwarehouses.all().delete()
cfg.userlocations.all().delete()
cfg.team_accesses.all().delete()
# Thématiques
for ut_data in entry.get('thematics', []):
self._apply_thematic(cfg, ut_data, mode)
# Contrats
for uc_data in entry.get('contracts', []):
self._apply_contract(cfg, uc_data, mode)
# Entrepôts
for wa_data in entry.get('warehouses', []):
self._apply_warehouse(cfg, wa_data, mode)
# Emplacements
for la_data in entry.get('locations', []):
self._apply_location(cfg, la_data, mode)
# Équipes
for ta_data in entry.get('team_accesses', []):
self._apply_team(cfg, ta_data, mode)
stats['updated' if exists else 'created'] += 1
# ------------------------------------------------------------------
def _apply_thematic(self, cfg, data, mode):
from common.models import Thematic
thematic = Thematic.objects.filter(code=data['thematic']).first()
if not thematic:
self.stdout.write(self.style.WARNING(
f' [⚠] thématique introuvable : {data["thematic"]}'
))
return
ut, created = UserThematics.objects.update_or_create(
user_config=cfg,
thematic=thematic,
defaults={
'can_view_assets': data.get('can_view_assets', True),
'can_edit_assets': data.get('can_edit_assets', False),
'can_view_interventions': data.get('can_view_interventions', True),
'can_edit_interventions': data.get('can_edit_interventions', False),
'can_edit_locations': data.get('can_edit_locations', False),
'can_process_observations': data.get('can_process_observations', False),
},
)
# Permissions par statut
if mode in ('update', 'reset') or created:
self._apply_status_permissions_thematic(ut, data.get('status_permissions', []), mode)
def _apply_status_permissions_thematic(self, ut, sp_list, mode):
if mode == 'reset':
ut.status_permissions.all().delete()
for sp in sp_list:
UserThematicStatusPermission.objects.update_or_create(
user_thematic=ut,
status=sp['status'],
defaults={
'can_view': sp.get('can_view', False),
'can_edit': sp.get('can_edit', False),
'can_change_status_to': sp.get('can_change_status_to', False),
},
)
# ------------------------------------------------------------------
def _apply_contract(self, cfg, data, mode):
try:
contract = Contract.objects.get(contract_number=data['contract'])
except Contract.DoesNotExist:
self.stdout.write(self.style.WARNING(
f' [⚠] contrat introuvable : {data["contract"]}'
))
return
uc, created = UserContractAccess.objects.update_or_create(
user_config=cfg,
contract=contract,
defaults={
'can_view_posts': data.get('can_view_posts', False),
'can_view_prices': data.get('can_view_prices', False),
'can_view_interventions': data.get('can_view_interventions', True),
'can_view_assets': data.get('can_view_assets', True),
'can_create_interventions': data.get('can_create_interventions', False),
'can_check_interventions': data.get('can_check_interventions', False),
'can_approve': data.get('can_approve', False),
},
)
if mode in ('update', 'reset') or created:
self._apply_status_permissions_contract(uc, data.get('status_permissions', []), mode)
def _apply_status_permissions_contract(self, uc, sp_list, mode):
if mode == 'reset':
uc.status_permissions.all().delete()
for sp in sp_list:
UserContractStatusPermission.objects.update_or_create(
user_contract=uc,
status=sp['status'],
defaults={
'can_view': sp.get('can_view', False),
'can_edit': sp.get('can_edit', False),
'can_change_status_to': sp.get('can_change_status_to', False),
},
)
# ------------------------------------------------------------------
def _apply_warehouse(self, cfg, data, mode):
from stock.models import Warehouse
warehouse = Warehouse.objects.filter(code=data['warehouse']).first()
if not warehouse:
self.stdout.write(self.style.WARNING(
f' [⚠] entrepôt introuvable : {data["warehouse"]}'
))
return
UserWarehouseAccess.objects.update_or_create(
user_config=cfg,
warehouse=warehouse,
defaults={
'can_view': data.get('can_view', True),
'can_manage': data.get('can_manage', False),
'can_administer': data.get('can_administer', False),
'can_manage_preparation_orders': data.get('can_manage_preparation_orders', False),
'can_manage_purchase_orders': data.get('can_manage_purchase_orders', False),
'can_validate_inventory': data.get('can_validate_inventory', False),
'is_default': data.get('is_default', False),
},
)
# ------------------------------------------------------------------
def _apply_location(self, cfg, data, mode):
from stock.models import WarehouseLocation
location = WarehouseLocation.objects.filter(code=data['location_code']).first()
if not location:
self.stdout.write(self.style.WARNING(
f' [⚠] emplacement introuvable : {data["location_code"]}'
))
return
UserWarehouseLocationAccess.objects.update_or_create(
user_config=cfg,
warehouse_location=location,
defaults={
'can_perform_inventory': data.get('can_perform_inventory', False),
'can_validate_inventory': data.get('can_validate_inventory', False),
},
)
# ------------------------------------------------------------------
def _apply_team(self, cfg, data, mode):
team = CompanyTeam.objects.filter(name=data['team']).first()
if not team:
self.stdout.write(self.style.WARNING(
f' [⚠] équipe introuvable : {data["team"]}'
))
return
UserCompanyTeamAccess.objects.update_or_create(
user_config=cfg,
team=team,
defaults={
'can_view': data.get('can_view', True),
'can_edit': data.get('can_edit', False),
},
)