Storno: das Journal zaehlte die Uebernachtungen der aufgehobenen Rechnung weiter mit (4 - 0 + 3 = 7 statt 3 gemeldeter Naechte). Die Betraege kuerzten sich, die Naechte nicht. Der Storno traegt jetzt die negative Zahl, und das Journal kennt Art, Bezug und Datum des Belegs (vier neue Spalten, alte Journale bauen sich beim Oeffnen um). Berichtigung: das Blatt wurde nur uebersprungen. Der berichtigte NAME kommt jetzt in der Buchung an - im Amtsbericht stand sonst weiter der falsche. Angewandt wird erst am Ende des Scans, das Blatt kann vor seiner Rechnung im Ordner liegen. Doppelte Nummern: eine stornierte Rechnung ist keine offene mehr und faellt aus der Meldung. Zugeordnet wird ueber Nummer + Datum + Empfaenger, nicht ueber die Nummer allein - bei einer doppelt vergebenen Nummer waere die kein eindeutiger Bezug, und genau den verlangt § 31 Abs. 5 UStDV. Die Texte nennen jetzt den Weg: eine der beiden im Rechnungstool stornieren und mit freier Nummer neu ausstellen (§ 14 Abs. 4 Nr. 4 UStG - jede Nummer nur einmal). Neue Spalte "Art": Storno zu ..., storniert, Neuausstellung. Aufgehobene Zeilen stehen grau - geloescht wird nichts. Gemeinsames Nummernbuch (gemeinsam.py, ordnerwahl.py - in beiden Programmen dieselbe Datei): eine SQLite in einem Ordner, den beide kennen. Das Journal traegt seine Nummern ein, auch die aus der Excel-Mappe ohne PDF - genau die kannte das Rechnungstool nicht und vergab sie ein zweites Mal. Beim ersten Start wird nach dem Ordner gefragt (auswaehlen, OK, Sicherheitsfrage, bei Nein zurueck ins Feld); wer ihn nicht hat, kommt mit "Spaeter einrichten" weiter. Kein Doppelklick zum Bearbeiten mehr (gui_dialoge.py geloescht): wer hier Betrag oder Nummer verstellt, meldet dem Amt etwas anderes, als auf dem Beleg steht. Loeschen bleibt. --test / CAMPINGHOF_TEST=1 unterdrueckt den Erststart-Dialog (Pruefstaende). Pruefstaende: pruef_storno_journal.py (neu, ueber BEIDE Programme: Storno, Berichtigung, doppelte Nummer aufloesen), pruef_doppelte, pruef_berichtigung, pruef_einzelinstanz - alle gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
183 lines
7.7 KiB
Python
183 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Den gemeinsamen Ordner abfragen - in beiden Programmen gleich.
|
||
|
||
Rechnungstool und Steuerjournal fuehren ein gemeinsames Nummernbuch
|
||
(siehe gemeinsam.py). Damit sie sich finden, muss BEIDEN derselbe Ordner
|
||
genannt werden - deshalb fragt jedes Programm beim ersten Start danach und
|
||
sagt ausdruecklich dazu, dass es derselbe sein muss.
|
||
|
||
Der Weg ist bewusst umstaendlich: auswaehlen, dann OK, dann noch einmal
|
||
bestaetigen. Ein falscher Ordner faellt sonst erst auf, wenn eine
|
||
Rechnungsnummer zum zweiten Mal vergeben wurde.
|
||
|
||
Wer jetzt keinen Ordner hat, kommt mit "Spaeter einrichten" weiter - das
|
||
Programm laeuft dann ohne das gemeinsame Buch und sagt das in der Fussleiste.
|
||
|
||
⚠ Das Fenster bekommt seine Groesse ausdruecklich gesetzt. Ohne das war es zu
|
||
schmal, der Knopf zum Auswaehlen lag ausserhalb - und der Dialog war nicht zu
|
||
bedienen (gefunden am 03.09.2026, bevor es jemand benutzen musste).
|
||
|
||
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
|
||
hinueber.
|
||
"""
|
||
|
||
import os
|
||
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox
|
||
|
||
import gemeinsam
|
||
|
||
BREITE = 780
|
||
|
||
|
||
class OrdnerWahl(tk.Toplevel):
|
||
"""Modaler Dialog. Nach dem Schliessen steht das Ergebnis in `.ergebnis`
|
||
(gewaehlter Ordner) oder es ist None (spaeter/abgebrochen)."""
|
||
|
||
def __init__(self, master, programm: str, vorgabe: str = ""):
|
||
super().__init__(master)
|
||
self.ergebnis = None
|
||
self.title("Gemeinsamer Ordner für das Nummernbuch")
|
||
self.transient(master)
|
||
self.pfad = tk.StringVar(value=vorgabe or "")
|
||
|
||
rahmen = ttk.Frame(self, padding=18)
|
||
rahmen.pack(fill="both", expand=True)
|
||
rahmen.columnconfigure(0, weight=1)
|
||
breite = BREITE - 60
|
||
|
||
ttk.Label(rahmen, text="Bitte den gemeinsamen Ordner auswählen",
|
||
font=("Segoe UI", 13, "bold")).grid(row=0, column=0, sticky="w")
|
||
ttk.Label(rahmen, wraplength=breite, justify="left", text=(
|
||
"Rechnungstool und Steuerjournal führen zusammen ein Nummernbuch. Darin steht "
|
||
"jede vergebene Rechnungsnummer – auch die aus der alten Excel-Tabelle, zu der "
|
||
"es gar keine PDF gibt.\n\n"
|
||
"Ohne dieses Buch kennt jedes Programm nur seine eigenen Nummern und vergibt "
|
||
"eine Nummer ein zweites Mal. Jede Nummer darf es aber nur einmal geben "
|
||
"(§ 14 Abs. 4 Nr. 4 UStG).")).grid(row=1, column=0, sticky="w", pady=(8, 12))
|
||
|
||
tk.Label(rahmen, wraplength=breite, justify="left", fg="#a00000",
|
||
font=("Segoe UI", 10, "bold"),
|
||
text="WICHTIG: In BEIDEN Programmen muss derselbe Ordner stehen – am besten "
|
||
"der Ordner, in dem die Programme liegen.").grid(
|
||
row=2, column=0, sticky="w", pady=(0, 14))
|
||
|
||
wahl = ttk.Frame(rahmen)
|
||
wahl.grid(row=3, column=0, sticky="ew")
|
||
wahl.columnconfigure(0, weight=1)
|
||
self.feld = ttk.Entry(wahl, textvariable=self.pfad, state="readonly")
|
||
self.feld.grid(row=0, column=0, sticky="ew")
|
||
ttk.Button(wahl, text="Install-Ordner auswählen…", command=self.waehlen,
|
||
width=26).grid(row=0, column=1, sticky="w", padx=(10, 0))
|
||
|
||
self.stand = tk.Label(rahmen, text="", wraplength=breite, justify="left",
|
||
font=("Segoe UI", 9))
|
||
self.stand.grid(row=4, column=0, sticky="w", pady=(10, 0))
|
||
|
||
knoepfe = ttk.Frame(rahmen)
|
||
knoepfe.grid(row=5, column=0, sticky="ew", pady=(18, 0))
|
||
self.ok = ttk.Button(knoepfe, text="OK", command=self.uebernehmen, width=14)
|
||
self.ok.pack(side="right")
|
||
ttk.Button(knoepfe, text="Später einrichten", command=self.spaeter,
|
||
width=18).pack(side="left")
|
||
|
||
self._stand_zeigen()
|
||
self.protocol("WM_DELETE_WINDOW", self.spaeter)
|
||
self.bind("<Escape>", lambda _e: self.spaeter())
|
||
|
||
# Groesse ausdruecklich setzen: sonst schneidet Tk das Fenster an der
|
||
# zweiten Spalte ab und der Auswahl-Knopf ist nicht erreichbar.
|
||
self.update_idletasks()
|
||
hoehe = max(self.winfo_reqheight(), 300)
|
||
x = master.winfo_rootx() + max(0, (master.winfo_width() - BREITE) // 2)
|
||
y = master.winfo_rooty() + 90
|
||
self.geometry(f"{BREITE}x{hoehe}+{max(0, x)}+{max(0, y)}")
|
||
self.minsize(BREITE, hoehe)
|
||
self.grab_set()
|
||
self.lift()
|
||
self.focus_force()
|
||
|
||
# ------------------------------------------------------------------ Weg
|
||
def waehlen(self):
|
||
# Der Griff (grab_set) muss waehrend des Windows-Ordnerdialogs los sein:
|
||
# sonst kann der native Dialog hinter dem gesperrten Fenster landen und
|
||
# nimmt keine Eingabe an - man klickt und es passiert nichts.
|
||
try:
|
||
self.grab_release()
|
||
except tk.TclError:
|
||
pass
|
||
try:
|
||
ordner = filedialog.askdirectory(
|
||
title="Gemeinsamen Ordner wählen (in beiden Programmen derselbe!)",
|
||
initialdir=self.pfad.get() or os.path.expanduser("~"), parent=self)
|
||
finally:
|
||
try:
|
||
self.grab_set()
|
||
except tk.TclError:
|
||
pass
|
||
if ordner:
|
||
self.pfad.set(os.path.abspath(ordner))
|
||
self._stand_zeigen()
|
||
|
||
def _stand_zeigen(self):
|
||
"""Sagt, was in dem gewaehlten Ordner schon liegt.
|
||
|
||
Ein bereits vorhandenes Nummernbuch ist das beste Zeichen, dass es der
|
||
richtige Ordner ist - dann hat das andere Programm ihn schon.
|
||
"""
|
||
ordner = self.pfad.get().strip()
|
||
if not ordner:
|
||
self.stand.configure(text="Noch kein Ordner gewählt.", fg="#a00000")
|
||
self.ok.state(["disabled"])
|
||
return
|
||
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
||
if not taugt:
|
||
self.stand.configure(text=grund, fg="#a00000")
|
||
self.ok.state(["disabled"])
|
||
return
|
||
self.ok.state(["!disabled"])
|
||
if os.path.exists(gemeinsam.pfad_zur_datei(ordner)):
|
||
try:
|
||
anzahl = len(gemeinsam.Nummernbuch(ordner).nummern())
|
||
except Exception: # noqa: BLE001
|
||
anzahl = "?"
|
||
self.stand.configure(
|
||
text=f"Hier liegt schon ein Nummernbuch mit {anzahl} Rechnungsnummern – "
|
||
"das ist der Ordner des anderen Programms. Genau richtig.",
|
||
fg="#1f5c3d")
|
||
else:
|
||
self.stand.configure(
|
||
text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im "
|
||
"anderen Programm denselben Ordner.", fg="#444444")
|
||
|
||
def uebernehmen(self):
|
||
ordner = self.pfad.get().strip()
|
||
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
||
if not taugt:
|
||
messagebox.showwarning("Ordner geht nicht", grund or "Bitte einen Ordner wählen.",
|
||
parent=self)
|
||
return
|
||
if not messagebox.askyesno(
|
||
"Sind Sie sicher?",
|
||
f"Gemeinsamer Ordner:\n\n{ordner}\n\n"
|
||
"Steht im anderen Programm derselbe Ordner? Nur dann kennen beide "
|
||
"dieselben Rechnungsnummern.\n\n"
|
||
"Ja = Ordner übernehmen\n"
|
||
"Nein = zurück zur Auswahl",
|
||
icon="question", default="yes", parent=self):
|
||
# Zurueck ins Auswahlfeld - der Dialog bleibt stehen, nichts ist gesetzt.
|
||
self.feld.focus_set()
|
||
return
|
||
self.ergebnis = ordner
|
||
self.destroy()
|
||
|
||
def spaeter(self):
|
||
self.ergebnis = None
|
||
self.destroy()
|
||
|
||
|
||
def ordner_abfragen(fenster, programm, vorgabe=""):
|
||
"""Dialog zeigen und den gewaehlten Ordner zurueckgeben (oder None)."""
|
||
d = OrdnerWahl(fenster, programm, vorgabe)
|
||
fenster.wait_window(d)
|
||
return d.ergebnis
|