""" Export views for assets. """ import csv import json import re from io import BytesIO from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse, HttpResponseBadRequest from django.utils.dateformat import format as dj_format_date from django.utils.timezone import localtime, now from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST from assets.models import AbstractLocation from assets.permissions import can_view_asset def _get_asset_location(asset): """Return the parent location of an asset by inspecting FK fields.""" for field in asset._meta.get_fields(): related = getattr(field, 'related_model', None) if related and issubclass(related, AbstractLocation): try: loc = getattr(asset, field.name) if loc is not None: return loc except Exception: pass return None def _get_asset_model_display(asset): """Return the display name of the asset's model (subtype), if any.""" try: model_val = getattr(asset, 'model', None) if model_val and hasattr(model_val, 'get_name'): return model_val.get_name() except Exception: pass return '' def _fmt_date(dt): if dt is None: return '' try: return dj_format_date(dt, 'Y-m-d') except Exception: return str(dt) def _get_row(model_name, asset): location = _get_asset_location(asset) loc_code = location.code if location else '' loc_name = location.get_name() if location else '' status_display = ( asset.get_status_display() if hasattr(asset, 'get_status_display') else (getattr(asset, 'status', '') or '') ) obsolescence_display = ( asset.get_obsolescence_display() if hasattr(asset, 'get_obsolescence_display') else (getattr(asset, 'obsolescence', '') or '') ) warranty = getattr(asset, 'warranty_duration', None) return [ model_name, asset.id, asset.code or '', asset.get_name() if hasattr(asset, 'get_name') else '', asset.category.get_name() if getattr(asset, 'category', None) else '', _get_asset_model_display(asset), status_display, obsolescence_display, loc_code, loc_name, _fmt_date(getattr(asset, 'installation_date', None)), _fmt_date(getattr(asset, 'uninstallation_date', None)), getattr(asset, 'serial_number', '') or '', warranty if warranty is not None else '', getattr(asset, 'brand', '') or '', getattr(asset, 'note', '') or '', getattr(asset, 'lon', '') if getattr(asset, 'lon', None) is not None else '', getattr(asset, 'lat', '') if getattr(asset, 'lat', None) is not None else '', ] EXPORT_HEADERS = [ _("Type"), _("ID"), _("Code"), _("Nom"), _("Catégorie"), _("Modèle"), _("Statut"), _("Obsolescence"), _("Code localisation"), _("Nom localisation"), _("Date d'installation"), _("Date de déinstallation"), _("Numéro de série"), _("Garantie (mois)"), _("Marque"), _("Note"), _("Longitude"), _("Latitude"), ] @login_required @require_POST def export_assets(request): """Export selected assets as CSV or XLSX. POST params: - ids: JSON string of asset keys (e.g. ``'["streetlamp:1","pole:2"]'``) - format: ``'csv'`` (default) or ``'xlsx'`` Only assets that the current user is allowed to view are included. """ ids_param = request.POST.get('ids', '[]') fmt = (request.POST.get('format') or 'csv').lower() try: keys = json.loads(ids_param) if not isinstance(keys, list): raise ValueError except (ValueError, TypeError, json.JSONDecodeError): return HttpResponseBadRequest(_("Paramètre 'ids' invalide.")) if not keys: return HttpResponseBadRequest(_("Aucun asset sélectionné.")) # Group keys by model name groups: dict[str, list[int]] = {} for key in keys: try: model_name, asset_id = str(key).rsplit(':', 1) groups.setdefault(model_name.lower(), []).append(int(asset_id)) except (ValueError, TypeError): continue # Fetch assets, filtering by user view permission items: list[tuple[str, object]] = [] for model_name, asset_ids in groups.items(): try: ct = ContentType.objects.get(model=model_name) model_class = ct.model_class() if model_class is None: continue for asset in model_class.objects.filter(pk__in=asset_ids): if can_view_asset(request.user, asset): items.append((model_name, asset)) except ContentType.DoesNotExist: continue timestamp = localtime(now()).strftime('%Y%m%d_%H%M%S') headers = [str(h) for h in EXPORT_HEADERS] if fmt == 'xlsx': try: from openpyxl import Workbook except ImportError: return HttpResponseBadRequest(_("Export XLSX indisponible (dépendance manquante).")) _illegal_re = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]') def _sanitize(value): return _illegal_re.sub('', value) if isinstance(value, str) else value wb = Workbook() ws = wb.active ws.title = 'Assets' ws.append(headers) for model_name, asset in items: ws.append([_sanitize(v) for v in _get_row(model_name, asset)]) output = BytesIO() wb.save(output) output.seek(0) filename = f"assets_{timestamp}.xlsx" resp = HttpResponse( output.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ) resp['Content-Disposition'] = f'attachment; filename="{filename}"' return resp # Default: CSV with semicolon separator (better EU Excel support) filename = f"assets_{timestamp}.csv" resp = HttpResponse(content_type='text/csv; charset=utf-8') resp['Content-Disposition'] = f'attachment; filename="{filename}"' # UTF-8 BOM for Excel auto-detection resp.write('\ufeff') writer = csv.writer(resp, delimiter=';') writer.writerow(headers) for model_name, asset in items: writer.writerow(_get_row(model_name, asset)) return resp