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:
@@ -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.
|
||||
|
||||
@@ -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
65
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."""
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user