JET-Design-Wortmarke im Kopf und auf der Rechnung

Die Logos stammen aus der Shop-Webseite (assets/img/logo). Im Seitenkopf
stehen beide SVG-Varianten im Markup, CSS blendet die zum Theme passende
ein -- der Dark-Mode-Schalter laedt die Seite nicht neu.

Fuer den Rechnungsdruck liegt die Wortmarke zusaetzlich als JPEG bei,
weil fpdf2 kein SVG setzen kann. logo_datei() waehlt aus: hochgeladenes
Logo vor mitgelieferter Marke, Textkopf nur noch als Notnagel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-09-08 14:25:52 +02:00
parent e1142ac904
commit b0e6b1b9f2
10 changed files with 91 additions and 13 deletions

View File

@@ -8,6 +8,8 @@
- `templates/settings.html` — Einstellungsseite (Druckerabschreibung, Arbeitskosten, Marge, MwSt., Firmendaten, Logo) - `templates/settings.html` — Einstellungsseite (Druckerabschreibung, Arbeitskosten, Marge, MwSt., Firmendaten, Logo)
- `static/style.css` — Light-/Dark-Mode-Theming - `static/style.css` — Light-/Dark-Mode-Theming
- `static/script.js` — Live-Berechnung + Auto-Save beim Tippen - `static/script.js` — Live-Berechnung + Auto-Save beim Tippen
- `static/img/logo/` — JET-Design-Wortmarke: `jet-design.svg` (dunkle Schrift),
`jet-design-weiss.svg` (Dark Mode), `jet-design.jpg` (Rechnungs-PDF)
- `druckkalkulation_config.yaml` — auto-created/auto-saved state (in .gitignore) - `druckkalkulation_config.yaml` — auto-created/auto-saved state (in .gitignore)
- `proxmox-setup.sh` — Proxmox LXC-Ersteinrichtung - `proxmox-setup.sh` — Proxmox LXC-Ersteinrichtung
- `proxmox-update.sh` — Proxmox LXC-Update - `proxmox-update.sh` — Proxmox LXC-Update
@@ -100,4 +102,14 @@ Kein Test, Lint, Typecheck oder CI vorhanden. README.md und AGENTS.md enthalten
`_migriere()` in eine einzelne Position und entfernt die Altfelder. `_migriere()` in eine einzelne Position und entfernt die Altfelder.
- **Saeamtliche UI-Texte sind Deutsch** — Labels, Fehlermeldungen, Rechnungsinhalte. - **Saeamtliche UI-Texte sind Deutsch** — Labels, Fehlermeldungen, Rechnungsinhalte.
- Nur `.venv/`, `druckkalkulation_config.yaml` und `static/logo.*` werden ignoriert. - Nur `.venv/`, `druckkalkulation_config.yaml` und `static/logo.*` werden ignoriert.
`static/img/logo/` ist davon nicht betroffen und wird versioniert.
- Das Rechnungslogo liefert `logo_datei()`: hochgeladenes Logo vor
`STANDARD_LOGO`, und erst wenn beide Dateien fehlen, setzt der PDF-Kopf
den Namen als Text. Nie direkt `config['logo']` fuer die Ausgabe lesen.
- Die Wortmarke ist im SVG fest eingefaerbt. Deshalb stehen im Kopf beide
Varianten im Markup und `static/style.css` blendet ueber
`[data-bs-theme="dark"]` die passende ein -- `toggleDarkMode()` laedt die
Seite nicht neu, eine serverseitige Auswahl waere danach falsch. Am
`navbar-brand` darf `navbar-brand-autodark` nicht stehen: Tabler invertiert
damit Bilder im Dark Mode und wuerde die weisse Variante schwarz faerben.
- `3dDruckKalkulator.py` ist die alte Tkinter-Version und wird nicht mehr verwendet (nicht im Tracking). - `3dDruckKalkulator.py` ist die alte Tkinter-Version und wird nicht mehr verwendet (nicht im Tracking).

View File

@@ -53,7 +53,8 @@ sich ueber `HOST` und `PORT` ueberschreiben.
- *Regelbesteuert* — MwSt. mit 19 % oder 7 % - *Regelbesteuert* — MwSt. mit 19 % oder 7 %
- **Rechnungspflichtangaben** nach §14 UStG (Steuernummer/USt-IdNr., - **Rechnungspflichtangaben** nach §14 UStG (Steuernummer/USt-IdNr.,
Leistungsdatum) — nur in den beiden Unternehmer-Modi Leistungsdatum) — nur in den beiden Unternehmer-Modi
- **Logo-Upload** (PNG/JPEG/WebP) für die Rechnung - **JET-Design-Wortmarke** im Seitenkopf (SVG, hell/dunkel) und auf der
Rechnung (JPEG). Ein eigenes **Logo-Upload** (PNG/JPEG/WebP) ersetzt sie.
## Installation auf Proxmox (LXC-Container) ## Installation auf Proxmox (LXC-Container)
@@ -151,3 +152,5 @@ druckkalkulation_projekte.yaml # Gespeicherte Projekte (ignoriert)
- Alle UI-Texte sind Deutsch - Alle UI-Texte sind Deutsch
- Die YAML-Config wird automatisch erstellt und bei jedem Tastendruck gespeichert - Die YAML-Config wird automatisch erstellt und bei jedem Tastendruck gespeichert
- `druckkalkulation_config.yaml` und `static/logo.*` sind in `.gitignore` ausgeschlossen - `druckkalkulation_config.yaml` und `static/logo.*` sind in `.gitignore` ausgeschlossen
- `static/img/logo/` gehört dagegen ins Repo: die beiden SVG-Varianten für den
Seitenkopf und `jet-design.jpg` für den Rechnungsdruck (fpdf2 setzt nur Pixelbilder)

26
app.py
View File

@@ -118,6 +118,20 @@ def as_stueckzahl(value):
return max(1, zahl) return max(1, zahl)
def logo_datei(config):
"""Pfad des Rechnungslogos relativ zu static/.
Ein hochgeladenes Logo hat Vorrang, sonst greift die mitgelieferte
Wortmarke. Fehlen beide Dateien, kommt '' zurueck und der PDF-Kopf
setzt den Firmennamen als Text.
"""
for name in (config.get('logo', ''), STANDARD_LOGO):
if name and os.path.exists(os.path.join('static', name)):
return name
return ''
def pdf_text(value): def pdf_text(value):
"""Macht beliebigen Text fuer die latin-1-Kernfonts von fpdf sicher. """Macht beliebigen Text fuer die latin-1-Kernfonts von fpdf sicher.
@@ -147,6 +161,11 @@ MAX_PROJEKTE = 500
LOGO_EXTS = ('png', 'jpg', 'webp') LOGO_EXTS = ('png', 'jpg', 'webp')
# Mitgelieferte JET-Design-Wortmarke fuer den Rechnungskopf. Aus der
# SVG-Vorlage des Shops gerastert, weil fpdf2 nur Pixelbilder setzt.
# Greift, solange kein eigenes Logo hochgeladen wurde.
STANDARD_LOGO = 'img/logo/jet-design.jpg'
# Obergrenze, damit ein fehlerhafter Client die Config nicht aufblaeht. # Obergrenze, damit ein fehlerhafter Client die Config nicht aufblaeht.
MAX_POSITIONEN = 50 MAX_POSITIONEN = 50
@@ -589,7 +608,8 @@ def index():
def settings(): def settings():
"""Einstellungsseite fuer Druckerabschreibung, Arbeitskosten und Marge.""" """Einstellungsseite fuer Druckerabschreibung, Arbeitskosten und Marge."""
config = load_config() config = load_config()
return render_template('settings.html', config=config) return render_template('settings.html', config=config,
logo_anzeige=logo_datei(config))
@app.route('/calculate', methods=['POST']) @app.route('/calculate', methods=['POST'])
@@ -843,9 +863,9 @@ def invoice_pdf():
col_w = 45 col_w = 45
# Logo rechts oben # Logo rechts oben
logo_name = config.get('logo', '') logo_name = logo_datei(config)
logo_path = os.path.join('static', logo_name) if logo_name else '' logo_path = os.path.join('static', logo_name) if logo_name else ''
if logo_name and os.path.exists(logo_path): if logo_name:
pdf.image(logo_path, x=175, y=12, w=20) pdf.image(logo_path, x=175, y=12, w=20)
img_w, img_h = image_dimensions(logo_path) img_w, img_h = image_dimensions(logo_path)
if img_w and img_h: if img_w and img_h:

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -11,3 +11,25 @@
white-space: pre; white-space: pre;
overflow-x: auto; overflow-x: auto;
} }
/* Wortmarke im Kopf. Die Schrift ist im SVG fest eingefaerbt, deshalb
liegen beide Varianten im Markup und die zum Theme passende wird
eingeblendet -- der Dark-Mode-Schalter tauscht nur das Attribut am
<html>, ohne die Seite neu zu laden. */
.marke {
height: 40px;
width: auto;
display: block;
}
.marke-dunkel {
display: none;
}
[data-bs-theme="dark"] .marke-hell {
display: none;
}
[data-bs-theme="dark"] .marke-dunkel {
display: block;
}

View File

@@ -16,8 +16,9 @@
<header class="navbar navbar-expand-md"> <header class="navbar navbar-expand-md">
<div class="container-xl"> <div class="container-xl">
<div class="navbar-brand navbar-brand-autodark pe-0 pe-md-3 py-2 d-flex align-items-center gap-2"> <div class="navbar-brand pe-0 pe-md-3 py-2 d-flex align-items-center gap-2">
<span class="fw-bold">JET Design</span> <img src="{{ url_for('static', filename='img/logo/jet-design.svg') }}" alt="JET Design" class="marke marke-hell">
<img src="{{ url_for('static', filename='img/logo/jet-design-weiss.svg') }}" alt="JET Design" class="marke marke-dunkel">
<span class="text-secondary" style="opacity:0.5;">|</span> <span class="text-secondary" style="opacity:0.5;">|</span>
<span class="fw-semibold">3D Druck Kostenkalkulator</span> <span class="fw-semibold">3D Druck Kostenkalkulator</span>
</div> </div>

View File

@@ -16,8 +16,9 @@
<header class="navbar navbar-expand-md"> <header class="navbar navbar-expand-md">
<div class="container-xl"> <div class="container-xl">
<div class="navbar-brand navbar-brand-autodark pe-0 pe-md-3 py-2 d-flex align-items-center gap-2"> <div class="navbar-brand pe-0 pe-md-3 py-2 d-flex align-items-center gap-2">
<span class="fw-bold">JET Design</span> <img src="{{ url_for('static', filename='img/logo/jet-design.svg') }}" alt="JET Design" class="marke marke-hell">
<img src="{{ url_for('static', filename='img/logo/jet-design-weiss.svg') }}" alt="JET Design" class="marke marke-dunkel">
<span class="text-secondary" style="opacity:0.5;">|</span> <span class="text-secondary" style="opacity:0.5;">|</span>
<span class="fw-semibold"><i class="ti ti-file-invoice me-1"></i>Rechnung erstellen</span> <span class="fw-semibold"><i class="ti ti-file-invoice me-1"></i>Rechnung erstellen</span>
</div> </div>

View File

@@ -16,8 +16,9 @@
<header class="navbar navbar-expand-md"> <header class="navbar navbar-expand-md">
<div class="container-xl"> <div class="container-xl">
<div class="navbar-brand navbar-brand-autodark pe-0 pe-md-3 py-2 d-flex align-items-center gap-2"> <div class="navbar-brand pe-0 pe-md-3 py-2 d-flex align-items-center gap-2">
<span class="fw-bold">JET Design</span> <img src="{{ url_for('static', filename='img/logo/jet-design.svg') }}" alt="JET Design" class="marke marke-hell">
<img src="{{ url_for('static', filename='img/logo/jet-design-weiss.svg') }}" alt="JET Design" class="marke marke-dunkel">
<span class="text-secondary" style="opacity:0.5;">|</span> <span class="text-secondary" style="opacity:0.5;">|</span>
<span class="fw-semibold">Einstellungen</span> <span class="fw-semibold">Einstellungen</span>
</div> </div>
@@ -82,13 +83,17 @@
<div class="card-header"><i class="ti ti-photo me-2"></i>Logo</div> <div class="card-header"><i class="ti ti-photo me-2"></i>Logo</div>
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
{% if config.logo %} {% if logo_anzeige %}
<div class="mb-2"> <div class="mb-2">
<img src="{{ url_for('static', filename=config.logo) }}" style="max-height:80px;max-width:240px" class="border rounded p-1"> <img src="{{ url_for('static', filename=logo_anzeige) }}" style="max-height:80px;max-width:240px" class="border rounded p-1">
</div> </div>
{% endif %}
{% if config.logo %}
<button type="button" class="btn btn-outline-danger btn-sm mb-2" onclick="deleteLogo()"> <button type="button" class="btn btn-outline-danger btn-sm mb-2" onclick="deleteLogo()">
<i class="ti ti-trash me-1"></i>Logo entfernen <i class="ti ti-trash me-1"></i>Eigenes Logo entfernen
</button> </button>
{% else %}
<div class="form-text mb-2">Mitgelieferte JET-Design-Wortmarke. Ein hochgeladenes Logo ersetzt sie.</div>
{% endif %} {% endif %}
<input type="file" class="form-control" id="logo-upload" accept="image/png,image/jpeg,image/webp"> <input type="file" class="form-control" id="logo-upload" accept="image/png,image/jpeg,image/webp">
<button type="button" class="btn btn-primary btn-sm mt-2" onclick="uploadLogo()"> <button type="button" class="btn btn-primary btn-sm mt-2" onclick="uploadLogo()">