204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
"""
|
|
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
|