NetBox Customer Export plugin with configurable branding

Plugin for NetBox 4.6.8–4.6.99 that exports customer documentation
(PDF, Word, HTML preview) for a site, location, tenant or tenant group.
Technicians pick sections, device roles and columns before exporting.

Branding profiles (colors, font, logo, footer, Word template incl. .dotx,
table style, extra CSS) are configurable via PLUGINS_CONFIG and selectable
per export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 12:43:19 +02:00
co-authored by Claude Opus 5.5
commit 8f45bffa99
21 changed files with 1444 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
"""Neutrales Dokumentmodell, das sowohl der HTML/PDF- als auch der Word-Renderer versteht."""
import base64
from dataclasses import dataclass, field
from django.utils.text import slugify
EMPTY = "—"
@dataclass
class Block:
kind: str # heading | paragraph | table | kv | images | pagebreak
level: int = 0
text: str = ""
headers: list = field(default_factory=list)
rows: list = field(default_factory=list)
images: list = field(default_factory=list)
anchor: str = ""
@dataclass
class Document:
title: str
subtitle: str
blocks: list
meta: dict
branding: dict = field(default_factory=dict)
@property
def toc(self):
return [b for b in self.blocks if b.kind == "heading" and b.level <= 2]
_anchor_counter = {"n": 0}
def H(level, text):
_anchor_counter["n"] += 1
return Block("heading", level=level, text=str(text), anchor=f"s{_anchor_counter['n']}-{slugify(str(text))[:40]}")
def P(text):
return Block("paragraph", text=str(text))
def T(headers, rows):
return Block("table", headers=list(headers), rows=[[fmt(c) for c in r] for r in rows])
def KV(pairs, skip_empty=True):
rows = [[k, fmt(v)] for k, v in pairs if not (skip_empty and v in (None, "", [], EMPTY))]
return Block("kv", rows=rows)
def IMAGES(images):
"""images: Liste von (svg_string, caption)."""
out = []
for svg, caption in images:
data = base64.b64encode(svg.encode("utf-8")).decode("ascii")
out.append({"svg": svg, "caption": caption, "data_uri": f"data:image/svg+xml;base64,{data}"})
return Block("images", images=out)
def PAGEBREAK():
return Block("pagebreak")
def fmt(value):
if value is None or value == "":
return EMPTY
if isinstance(value, bool):
return "Ja" if value else "Nein"
if isinstance(value, (list, tuple, set)) or hasattr(value, "all"):
items = value.all() if hasattr(value, "all") else value
return ", ".join(str(i) for i in items) or EMPTY
return str(value)
def get(obj, path):
"""Dotted-getattr, bevorzugt get_<feld>_display() für Choice-Felder."""
for part in path.split("."):
if obj is None:
return None
display = getattr(obj, f"get_{part}_display", None)
obj = display() if callable(display) else getattr(obj, part, None)
return obj