Knopf "Neues Projekt" zum Leeren des Arbeitsstands

Bisher liess sich ein neuer Auftrag nur von Hand beginnen: Namen
ueberschreiben, jede Position neu ausfuellen und auf der Rechnungsseite
Kundendaten und Rechnungsnummer leeren. Wurde der letzte Schritt
vergessen, stand der alte Kunde auf der neuen Rechnung.

- POST /projekte/neu setzt nur PROJEKT_FELDER zurueck (Name, Positionen,
  Kundendaten, Rechnungsnummer, Datum auf heute). Betriebswerte wie
  Marge, Stundenlohn, Drucker, Steuer und Firmendaten bleiben.
- GET /projekte liefert zusaetzlich 'ungespeichert'. Der Knopf fragt nur
  nach, wenn der offene Stand wirklich nicht als Projekt gesichert ist.
- Der Vergleich ignoriert 'datum' und 'geaendert', sonst waere ein
  gespeichertes Projekt am naechsten Tag faelschlich "ungespeichert".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-08-22 11:59:20 +02:00
parent ce3ba9df7e
commit 1b3ef82db4
5 changed files with 106 additions and 1 deletions

View File

@@ -92,6 +92,10 @@ Kein Test, Lint, Typecheck oder CI vorhanden. README.md und AGENTS.md enthalten
Die Preise in der Projektliste werden mit den AKTUELLEN Betriebswerten
gerechnet, nicht mit denen von damals.
- `druckkalkulation_config.yaml` ist der Arbeitsstand, nicht das Archiv.
- `/projekte/neu` setzt nur `PROJEKT_FELDER` zurueck. `GET /projekte`
liefert zusaetzlich `ungespeichert` -- der Vergleich laeuft ueber
`_vergleichbar()` und ignoriert bewusst `datum` und `geaendert`, sonst
waere ein Projekt am naechsten Tag faelschlich "ungespeichert".
- Alte Configs mit den Flachfeldern (`materialGewicht` etc.) migriert
`_migriere()` in eine einzelne Position und entfernt die Altfelder.
- **Saeamtliche UI-Texte sind Deutsch** — Labels, Fehlermeldungen, Rechnungsinhalte.

View File

@@ -33,6 +33,8 @@ sich ueber `HOST` und `PORT` ueberschreiben.
Positionsliste kalkuliert und als eigene Rechnungszeilen ausgewiesen.
Material, Preis/kg, Druckzeit, Arbeitszeit und Stückzahl gelten je Position;
Drucker, Strom, Stundenlohn, Marge und Steuer gelten für den ganzen Auftrag.
- **Neues Projekt** — leert Projektname, Positionen, Kundendaten und
Rechnungsnummer; warnt vorher, wenn der offene Stand nicht gesichert ist
- **Projekte speichern und laden** — benannte Kalkulationen bleiben erhalten
und lassen sich jederzeit wieder aufrufen (`druckkalkulation_projekte.yaml`).
Betriebswerte (Drucker, Strom, Stundenlohn, Marge, Steuer) gehören *nicht*
@@ -139,6 +141,7 @@ druckkalkulation_projekte.yaml # Gespeicherte Projekte (ignoriert)
| POST | `/projekte/speichern` | Aktuellen Stand als Projekt sichern |
| POST | `/projekte/laden` | Projekt in den Arbeitsstand laden |
| POST | `/projekte/loeschen` | Projekt entfernen |
| POST | `/projekte/neu` | Arbeitsstand auf leeren Auftrag zurücksetzen |
| POST | `/upload_logo` | Logo hochladen |
| POST | `/delete_logo` | Logo loschen |

65
app.py
View File

@@ -359,6 +359,50 @@ def projekt_aus_config(config):
for feld in PROJEKT_FELDER}
def leerer_projektstand():
"""Projektfelder im Ausgangszustand. Datum auf heute."""
stand = {feld: DEFAULT_VALUES.get(feld) for feld in PROJEKT_FELDER}
stand['projektName'] = ''
stand['kundenName'] = ''
stand['kundenAdresse'] = ''
stand['rechnungsnummer'] = ''
stand['leistungsdatum'] = ''
stand['datum'] = datetime.now().strftime('%Y-%m-%d')
stand['positionen'] = [dict(DEFAULT_POSITION)]
return stand
def _vergleichbar(stand):
"""Projektfelder in eine vergleichbare Form bringen.
'geaendert' und das Datum bleiben aussen vor -- ein neuer Tag macht
aus einem gespeicherten Projekt keinen ungespeicherten Stand.
"""
return {
'projektName': projekt_name(stand.get('projektName')),
'kundenName': str(stand.get('kundenName') or ''),
'kundenAdresse': str(stand.get('kundenAdresse') or ''),
'rechnungsnummer': str(stand.get('rechnungsnummer') or ''),
'leistungsdatum': str(stand.get('leistungsdatum') or ''),
'positionen': saubere_positionen(stand.get('positionen')),
}
def ungespeicherte_aenderungen(config, projekte):
"""True, wenn der Arbeitsstand nicht so in den Projekten liegt.
Ein voellig leerer Stand zaehlt nicht als ungespeichert -- da ist
nichts zu verlieren.
"""
aktuell = _vergleichbar(config)
if aktuell == _vergleichbar(leerer_projektstand()):
return False
gespeichert = projekte.get(aktuell['projektName'])
if not isinstance(gespeichert, dict):
return True
return aktuell != _vergleichbar(gespeichert)
def steuer_kontext(data):
"""Liest Regime und MwSt.-Satz robust aus den Formularwerten."""
regime = str(data.get('steuerregime', 'regel')).lower()
@@ -582,7 +626,11 @@ def projekte_liste():
'gesamt_endpreis': calc['gesamt_endpreis'],
})
eintraege.sort(key=lambda e: e['geaendert'], reverse=True)
return jsonify({'projekte': eintraege, 'aktuell': config.get('projektName', '')})
return jsonify({
'projekte': eintraege,
'aktuell': config.get('projektName', ''),
'ungespeichert': ungespeicherte_aenderungen(config, projekte),
})
@app.route('/projekte/speichern', methods=['POST'])
@@ -635,6 +683,21 @@ def projekt_laden():
return jsonify({'status': 'ok', 'name': name})
@app.route('/projekte/neu', methods=['POST'])
def projekt_neu():
"""Setzt den Arbeitsstand auf einen leeren Auftrag zurueck.
Betriebswerte (Drucker, Strom, Stundenlohn, Marge, Steuer,
Firmendaten) bleiben unangetastet -- zurueckgesetzt wird nur, was
zu einem einzelnen Auftrag gehoert.
"""
with _config_lock:
config = load_config()
config.update(leerer_projektstand())
save_config(_migriere(config))
return jsonify({'status': 'ok'})
@app.route('/projekte/loeschen', methods=['POST'])
def projekt_loeschen():
"""Entfernt ein gespeichertes Projekt. Der Arbeitsstand bleibt."""

View File

@@ -345,6 +345,37 @@ async function projektLaden() {
}
}
/** Beginnt einen leeren Auftrag. Betriebswerte bleiben erhalten. */
async function projektNeu() {
// Offenen Autosave durchdruecken, damit der Server den wirklich
// aktuellen Stand kennt, bevor wir nach Ungespeichertem fragen.
await sofortSpeichern();
let offen = false;
try {
const daten = await (await fetch('/projekte')).json();
offen = !!daten.ungespeichert;
} catch (e) {
offen = true; // Im Zweifel lieber nachfragen.
}
if (offen && !confirm(
'Der aktuelle Stand ist nicht als Projekt gespeichert.\n\n' +
'Projektname, Positionen, Kundendaten und Rechnungsnummer werden ' +
'geleert. Betriebswerte (Marge, Drucker, Steuer) bleiben erhalten.\n\n' +
'Trotzdem neu anfangen?')) {
return;
}
try {
const res = await fetch('/projekte/neu', { method: 'POST' });
if (!res.ok) {
showNotification('Zuruecksetzen fehlgeschlagen');
return;
}
location.reload();
} catch (e) {
showNotification('Zuruecksetzen fehlgeschlagen');
}
}
/** Loescht das ausgewaehlte Projekt (der Arbeitsstand bleibt bestehen). */
async function projektLoeschen() {
const auswahl = document.getElementById('projekt-auswahl');

View File

@@ -68,6 +68,10 @@
</button>
</div>
<div class="form-text" id="projekt-info">Noch keine Projekte gespeichert.</div>
<button type="button" class="btn btn-outline-secondary w-100 mt-3" onclick="projektNeu()" title="Alle Auftragsdaten leeren">
<i class="ti ti-file-plus me-1"></i>Neues Projekt
</button>
</div>
</div>