402 lines
16 KiB
Python
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),
|
|
},
|
|
)
|