diff --git a/AGENTS.md b/AGENTS.md index 21dad6b..2b1b324 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index ed5c9b7..f879915 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/app.py b/app.py index b3519f8..8c5cb68 100644 --- a/app.py +++ b/app.py @@ -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.""" diff --git a/static/script.js b/static/script.js index a48032c..5779b63 100644 --- a/static/script.js +++ b/static/script.js @@ -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'); diff --git a/templates/index.html b/templates/index.html index fede732..c7c1b34 100644 --- a/templates/index.html +++ b/templates/index.html @@ -68,6 +68,10 @@
Noch keine Projekte gespeichert.
+ +