from collections import defaultdict from datetime import datetime import re from openpyxl import Workbook from django.core.management.base import BaseCommand from assets.models import NatureGreenSurface ILLEGAL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") def sanitize(value): if isinstance(value, str): return ILLEGAL_CHAR_RE.sub("", value) return value def autosize_columns(ws): for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: value = "" if cell.value is None else str(cell.value) if len(value) > max_length: max_length = len(value) ws.column_dimensions[column_letter].width = min(max(12, max_length + 2), 60) class Command(BaseCommand): help = ( "Exporte les assets NatureGreenSurface dans un fichier XLSX avec " "une feuille de donnees brutes et une feuille de synthese par type de surface." ) def add_arguments(self, parser): parser.add_argument( "--output", type=str, default=None, help="Chemin du fichier XLSX de sortie.", ) parser.add_argument( "--include-removed", action="store_true", default=False, help="Inclut les surfaces avec le statut 'removed'.", ) def handle(self, *args, **options): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = options["output"] or f"greensurfaces_export_{timestamp}.xlsx" queryset = NatureGreenSurface.objects.select_related("model", "location").order_by("code") if not options["include_removed"]: queryset = queryset.exclude(status="removed") wb = Workbook() # Feuille 1: donnees brutes ws_raw = wb.active ws_raw.title = "Donnees" raw_headers = [ "ID", "Code", "Nom FR", "Nom NL", "Statut", "Type surface (code)", "Type surface (nom FR)", "Type surface (nom NL)", "Categorie (code)", "Categorie (nom FR)", "Categorie (nom NL)", "Localisation (code)", "Localisation (nom FR)", "Localisation (nom NL)", "Superficie", "Pente", "Densite", "Largeur", "Longueur", "Longitude", "Latitude", ] ws_raw.append(raw_headers) summary = defaultdict(lambda: {"count": 0, "surface_sum": 0.0}) for surface in queryset: model = surface.model category = model.category if model and model.category else None location = surface.location model_key = ( model.code if model and model.code else "SANS_TYPE", model.name_fr if model and model.name_fr else "Sans type", model.name_nl if model and model.name_nl else "", ) surface_area = float(surface.surface_area or 0) summary[model_key]["count"] += 1 summary[model_key]["surface_sum"] += surface_area ws_raw.append( [ surface.id, sanitize(surface.code), sanitize(surface.name_fr), sanitize(surface.name_nl), sanitize(surface.status), sanitize(model.code if model else None), sanitize(model.name_fr if model else None), sanitize(model.name_nl if model else None), sanitize(category.code if category else None), sanitize(category.name_fr if category else None), sanitize(category.name_nl if category else None), sanitize(location.code if location else None), sanitize(location.name_fr if location else None), sanitize(location.name_nl if location else None), surface.surface_area, surface.slope, surface.density, surface.width, surface.length, surface.lon, surface.lat, ] ) autosize_columns(ws_raw) # Feuille 2: synthese ws_summary = wb.create_sheet(title="Synthese") summary_headers = [ "Type surface (code)", "Type surface (nom FR)", "Type surface (nom NL)", "Nombre", "Superficie totale", ] ws_summary.append(summary_headers) for (model_code, model_name_fr, model_name_nl), values in sorted( summary.items(), key=lambda item: (item[0][1], item[0][0]) ): ws_summary.append( [ sanitize(model_code), sanitize(model_name_fr), sanitize(model_name_nl), values["count"], round(values["surface_sum"], 2), ] ) ws_summary.append(["TOTAL", "", "", f"=SUM(D2:D{ws_summary.max_row})", f"=SUM(E2:E{ws_summary.max_row})"]) autosize_columns(ws_summary) wb.save(output_path) self.stdout.write(self.style.SUCCESS(f"Export termine: {output_path}")) self.stdout.write(self.style.SUCCESS(f"Nombre de surfaces exportees: {queryset.count()}"))