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 Die Preise in der Projektliste werden mit den AKTUELLEN Betriebswerten
gerechnet, nicht mit denen von damals. gerechnet, nicht mit denen von damals.
- `druckkalkulation_config.yaml` ist der Arbeitsstand, nicht das Archiv. - `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 - Alte Configs mit den Flachfeldern (`materialGewicht` etc.) migriert
`_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.

View File

@@ -33,6 +33,8 @@ sich ueber `HOST` und `PORT` ueberschreiben.
Positionsliste kalkuliert und als eigene Rechnungszeilen ausgewiesen. Positionsliste kalkuliert und als eigene Rechnungszeilen ausgewiesen.
Material, Preis/kg, Druckzeit, Arbeitszeit und Stückzahl gelten je Position; Material, Preis/kg, Druckzeit, Arbeitszeit und Stückzahl gelten je Position;
Drucker, Strom, Stundenlohn, Marge und Steuer gelten für den ganzen Auftrag. 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 - **Projekte speichern und laden** — benannte Kalkulationen bleiben erhalten
und lassen sich jederzeit wieder aufrufen (`druckkalkulation_projekte.yaml`). und lassen sich jederzeit wieder aufrufen (`druckkalkulation_projekte.yaml`).
Betriebswerte (Drucker, Strom, Stundenlohn, Marge, Steuer) gehören *nicht* 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/speichern` | Aktuellen Stand als Projekt sichern |
| POST | `/projekte/laden` | Projekt in den Arbeitsstand laden | | POST | `/projekte/laden` | Projekt in den Arbeitsstand laden |
| POST | `/projekte/loeschen` | Projekt entfernen | | POST | `/projekte/loeschen` | Projekt entfernen |
| POST | `/projekte/neu` | Arbeitsstand auf leeren Auftrag zurücksetzen |
| POST | `/upload_logo` | Logo hochladen | | POST | `/upload_logo` | Logo hochladen |
| POST | `/delete_logo` | Logo loschen | | POST | `/delete_logo` | Logo loschen |

65
app.py
View File

@@ -359,6 +359,50 @@ def projekt_aus_config(config):
for feld in PROJEKT_FELDER} 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): def steuer_kontext(data):
"""Liest Regime und MwSt.-Satz robust aus den Formularwerten.""" """Liest Regime und MwSt.-Satz robust aus den Formularwerten."""
regime = str(data.get('steuerregime', 'regel')).lower() regime = str(data.get('steuerregime', 'regel')).lower()
@@ -582,7 +626,11 @@ def projekte_liste():
'gesamt_endpreis': calc['gesamt_endpreis'], 'gesamt_endpreis': calc['gesamt_endpreis'],
}) })
eintraege.sort(key=lambda e: e['geaendert'], reverse=True) 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']) @app.route('/projekte/speichern', methods=['POST'])
@@ -635,6 +683,21 @@ def projekt_laden():
return jsonify({'status': 'ok', 'name': name}) 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']) @app.route('/projekte/loeschen', methods=['POST'])
def projekt_loeschen(): def projekt_loeschen():
"""Entfernt ein gespeichertes Projekt. Der Arbeitsstand bleibt.""" """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). */ /** Loescht das ausgewaehlte Projekt (der Arbeitsstand bleibt bestehen). */
async function projektLoeschen() { async function projektLoeschen() {
const auswahl = document.getElementById('projekt-auswahl'); const auswahl = document.getElementById('projekt-auswahl');

View File

@@ -68,6 +68,10 @@
</button> </button>
</div> </div>
<div class="form-text" id="projekt-info">Noch keine Projekte gespeichert.</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>
</div> </div>