import io from django import forms from django.contrib import admin, messages from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import path, reverse from .models import ( Company, Contract, ContractPost, ContractOrder, ContractOrderDocument, CompanyTeam, CompanyMember, CompanyMemberHistory, ContractOrderQuote, ContractOrderQuoteItem, ContractOrderProgressState, ContractOrderProgressStateItem, MaintainType, ClaimDeclarationAttachment, UserCompanyTeamAccess, DirectOrderPost, ) # --------------------------------------------------------------------------- # Formulaire d'import de postes # --------------------------------------------------------------------------- class ImportContractPostsForm(forms.Form): contract = forms.ModelChoiceField( queryset=None, # initialisé dans __init__ label="Contrat", empty_label="-- Sélectionner un contrat --", ) excel_file = forms.FileField( label="Fichier Excel (.xlsx / .xlsm)", help_text="Fichier inventaire des postes (format SPRB-GOB Fonteinen).", ) update_existing = forms.BooleanField( required=False, label="Mettre à jour les postes existants", help_text=( "Si coché, les postes déjà présents (même numéro de poste) seront" " mis à jour avec les valeurs du fichier Excel." " Sinon, ils sont ignorés." ), ) dry_run = forms.BooleanField( required=False, label="Mode simulation (dry-run)", help_text="Affiche le résultat sans écrire quoi que ce soit en base de données.", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from .models import Contract self.fields['contract'].queryset = Contract.objects.order_by('contract_number') # --------------------------------------------------------------------------- # Admins # --------------------------------------------------------------------------- def _get_pause_reason_inline(): from interventions.models import ContractPauseReasonAssignment class ContractPauseReasonAssignmentInline(admin.TabularInline): model = ContractPauseReasonAssignment extra = 0 fields = ('pause_reason', 'assigned_member') verbose_name = "Réassignation automatique sur pause" verbose_name_plural = "Réassignations automatiques sur pause" return ContractPauseReasonAssignmentInline def _get_symptom_autoclose_inline(): from interventions.models import SymptomContractAutoClose class SymptomAutoCloseInline(admin.TabularInline): model = SymptomContractAutoClose extra = 0 fields = ('symptom', 'delay_days') verbose_name = "Règle de clôture automatique" verbose_name_plural = "Règles de clôture automatique (par symptôme)" return SymptomAutoCloseInline @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): list_display = ('name', 'contact_email', 'phone_number') @admin.register(Contract) class ContractAdmin(admin.ModelAdmin): list_display = ('contract_number', 'description', 'company', 'start_date', 'end_date', 'is_active', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'thematics_list') search_fields = ('contract_number', 'company__name', 'description', 'contact_phone') list_filter = ('is_active', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'send_daily_summary') filter_horizontal = ('thematics',) def get_inlines(self, request, obj=None): return [_get_pause_reason_inline(), _get_symptom_autoclose_inline()] + list(super().get_inlines(request, obj)) fieldsets = ( (None, { 'fields': ('company', 'contract_number', 'description', 'start_date', 'end_date', 'is_active', 'committed_amount', 'internal_link', 'executive_officer', 'contact_phone', 'signatory_name', 'signatory_role', 'thematics'), }), ('Notifications email', { 'fields': ('send_email_notifications', 'notification_emails', 'cc_notification_emails', 'send_daily_summary', 'daily_summary_hour', 'email_only_if_urgent'), 'description': ( "Si \"Envoi groupé journalier\" est activé, les fiches d'intervention ne sont PAS envoyées " "individuellement. Elles sont regroupées et envoyées une fois par jour via la commande " "\"send_daily_intervention_summary\" (à planifier via les tâches planifiées)." ), }), ('Options de workflow', { 'fields': ('needs_approval', 'needs_checking', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'warn_if_no_prestation_on_processed'), }), ) def thematics_list(self, obj): return ", ".join([thematic.name for thematic in obj.thematics.all()]) thematics_list.short_description = 'Thématiques' @admin.register(ContractPost) class ContractPostAdmin(admin.ModelAdmin): list_display = ('contract', 'order_number', 'code', 'description_fr', 'unit_price', 'post_type', 'is_coefficient_multipliable') list_filter = ('contract',) search_fields = ('code', 'description_fr', 'contract__contract_number') change_list_template = 'admin/contracts/contractpost/change_list.html' def get_urls(self): urls = super().get_urls() custom = [ path( 'import-posts/', self.admin_site.admin_view(self.import_posts_view), name='contracts_contractpost_import_posts', ), ] return custom + urls def import_posts_view(self, request): from .utils_import_posts import import_contract_posts context = { **self.admin_site.each_context(request), 'title': 'Importer / mettre à jour des postes (Excel)', 'form': ImportContractPostsForm(), 'opts': self.model._meta, 'stats': None, 'log_lines': None, 'dry_run': False, } if request.method == 'POST': form = ImportContractPostsForm(request.POST, request.FILES) context['form'] = form if form.is_valid(): contract = form.cleaned_data['contract'] excel_file = form.cleaned_data['excel_file'] update_existing = form.cleaned_data['update_existing'] dry_run = form.cleaned_data['dry_run'] log_lines = [] # openpyxl accepte un objet bytes-like ; on wrappe le fichier file_bytes = io.BytesIO(excel_file.read()) stats = import_contract_posts( contract=contract, excel_source=file_bytes, dry_run=dry_run, update_existing=update_existing, log=log_lines.append, ) context['stats'] = stats context['log_lines'] = log_lines context['dry_run'] = dry_run if stats['errors']: messages.error(request, f"{stats['errors']} erreur(s) lors de l'import.") elif not dry_run: messages.success( request, f"{stats['created']} poste(s) créé(s), " f"{stats['updated']} mis à jour pour le contrat " f"« {contract.contract_number} »." ) else: messages.info( request, f"[DRY-RUN] {stats['created']} poste(s) seraient créés, " f"{stats['updated']} mis à jour." ) return render(request, 'admin/contracts/contractpost/import_posts.html', context) @admin.register(ContractOrder) class ContractOrderAdmin(admin.ModelAdmin): list_display = ('contract', 'order_code', 'description', 'order_date') search_fields = ('order_code', 'contract__contract_number') list_filter = ('contract',) @admin.register(MaintainType) class MaintainTypeAdmin(admin.ModelAdmin): list_display = ('code', 'name_fr', 'name_nl') search_fields = ('code', 'name_fr', 'name_nl') ordering = ('code',) class CompanyTeamMemberInline(admin.TabularInline): model = CompanyMember.teams.through extra = 0 verbose_name = "Membre" verbose_name_plural = "Membres" @admin.register(CompanyTeam) class CompanyTeamAdmin(admin.ModelAdmin): list_display = ('name', 'company', 'thematics_list', 'members_count', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'visibility_start_hour', 'restrict_to_assigned_interventions') search_fields = ('name', 'company__name') list_filter = ('company', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions') filter_horizontal = ('thematics',) inlines = [CompanyTeamMemberInline] def thematics_list(self, obj): return ", ".join([thematic.name for thematic in obj.thematics.all()]) thematics_list.short_description = 'Thématiques' def members_count(self, obj): return obj.members.count() members_count.short_description = 'Nb membres' @admin.register(CompanyMember) class CompanyMemberAdmin(admin.ModelAdmin): list_display = ('name', 'user', 'display_teams', 'visible_maintain_types_list') search_fields = ('name', 'user__username', 'teams__name') list_filter = ('teams', 'visible_maintain_types') filter_horizontal = ('teams', 'visible_maintain_types') def display_teams(self, obj): return ", ".join([t.name for t in obj.teams.all()]) or "—" display_teams.short_description = 'Équipes' def visible_maintain_types_list(self, obj): types = obj.visible_maintain_types.all() if not types: return "Tous" return ", ".join([t.name_fr for t in types]) visible_maintain_types_list.short_description = 'Types visibles' class ContractOrderQuoteItemInline(admin.TabularInline): model = ContractOrderQuoteItem extra = 0 autocomplete_fields = ('contract_post',) def get_queryset(self, request): return super().get_queryset(request).select_related('contract_post__contract') @admin.register(ContractOrderQuote) class ContractOrderQuoteAdmin(admin.ModelAdmin): list_display = ('order', 'created_by', 'created_at') search_fields = ('order__order_code', 'order__contract__contract_number') autocomplete_fields = ('order', 'created_by') inlines = [ContractOrderQuoteItemInline] class ContractOrderProgressStateItemInline(admin.TabularInline): model = ContractOrderProgressStateItem extra = 0 readonly_fields = ('contract_post', 'quantity') @admin.register(ContractOrderProgressState) class ContractOrderProgressStateAdmin(admin.ModelAdmin): list_display = ('order', 'created_at', 'created_by') search_fields = ('order__order_code', 'order__contract__contract_number') list_filter = ('created_at',) readonly_fields = ('created_at', 'created_by', 'order') inlines = [ContractOrderProgressStateItemInline] class ClaimDeclarationAttachmentInline(admin.TabularInline): model = ClaimDeclarationAttachment extra = 0 readonly_fields = ('uploaded_at', 'uploaded_by') @admin.register(ContractOrderDocument) class ContractOrderDocumentAdmin(admin.ModelAdmin): list_display = ('title', 'order', 'document_type', 'uploaded_at') search_fields = ('title', 'order__order_code') list_filter = ('document_type',) inlines = [ClaimDeclarationAttachmentInline] @admin.register(ClaimDeclarationAttachment) class ClaimDeclarationAttachmentAdmin(admin.ModelAdmin): list_display = ('title', 'claim_document', 'uploaded_at', 'uploaded_by') search_fields = ('title', 'claim_document__title', 'claim_document__order__order_code') list_filter = ('uploaded_at',) readonly_fields = ('uploaded_at',) # --------------------------------------------------------------------------- # Accès utilisateur aux équipes # --------------------------------------------------------------------------- @admin.register(UserCompanyTeamAccess) class UserCompanyTeamAccessAdmin(admin.ModelAdmin): list_display = ('user_config_user', 'team', 'can_view', 'can_edit') list_filter = ('can_view', 'can_edit', 'team__company') search_fields = ('user_config__user__username', 'user_config__user__first_name', 'user_config__user__last_name', 'team__name') autocomplete_fields = ('team',) def user_config_user(self, obj): return obj.user_config.user.username user_config_user.short_description = 'Utilisateur' user_config_user.admin_order_field = 'user_config__user__username' # --------------------------------------------------------------------------- # Historique des appartenances aux équipes # --------------------------------------------------------------------------- @admin.register(CompanyMemberHistory) class CompanyMemberHistoryAdmin(admin.ModelAdmin): list_display = ('member', 'team', 'join_date', 'leave_date', 'changed_by') list_filter = ('team__company', 'team') search_fields = ('member__name', 'team__name') readonly_fields = ('member', 'team', 'join_date', 'leave_date', 'changed_by') date_hierarchy = 'join_date' def has_add_permission(self, request): return False def has_delete_permission(self, request, obj=None): return False # --------------------------------------------------------------------------- # Commandes directes # --------------------------------------------------------------------------- @admin.register(DirectOrderPost) class DirectOrderPostAdmin(admin.ModelAdmin): list_display = ('order', 'contract_post', 'quantity', 'control_status', 'created_by', 'created_at') list_filter = ('control_status', 'order__contract') search_fields = ('order__order_code', 'contract_post__code') readonly_fields = ('created_at', 'created_by', 'validated_at', 'validated_by') autocomplete_fields = ('contract_post',)