""" 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), }, )