Storno und Berichtigung kommen an, gemeinsames Nummernbuch, kein Bearbeiten mehr
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
This commit is contained in:
parent
9e564466c7
commit
59d2db75af
12 changed files with 906 additions and 212 deletions
86
db.py
86
db.py
|
|
@ -32,6 +32,10 @@ CREATE TABLE IF NOT EXISTS buchungen (
|
|||
gezahlt REAL NOT NULL DEFAULT 0,
|
||||
satz REAL NOT NULL DEFAULT 5,
|
||||
quelle TEXT NOT NULL DEFAULT 'pdf',
|
||||
art TEXT NOT NULL DEFAULT 'rechnung',
|
||||
storno_zu TEXT NOT NULL DEFAULT '',
|
||||
storno_datum TEXT NOT NULL DEFAULT '',
|
||||
vorgang TEXT NOT NULL DEFAULT '',
|
||||
pdf_pfad TEXT NOT NULL DEFAULT '',
|
||||
pdf_mtime REAL NOT NULL DEFAULT 0,
|
||||
pdf_groesse INTEGER NOT NULL DEFAULT 0
|
||||
|
|
@ -59,6 +63,7 @@ class Journal:
|
|||
self.con.row_factory = sqlite3.Row
|
||||
self._alte_nummern_sperre_loesen()
|
||||
self.con.executescript(SCHEMA)
|
||||
self._spalten_ergaenzen()
|
||||
self.con.commit()
|
||||
log.info("Journal %s (%s)", pfad, "neu angelegt" if neu else "geöffnet")
|
||||
|
||||
|
|
@ -114,6 +119,21 @@ class Journal:
|
|||
log.info("Umbau fertig (%d Buchungen) - doppelte Rechnungsnummern bleiben "
|
||||
"ab jetzt sichtbar", genommen)
|
||||
|
||||
def _spalten_ergaenzen(self):
|
||||
"""Spalten nachziehen, die es in aelteren Journalen noch nicht gab.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS aendert eine bestehende Tabelle nicht - ohne
|
||||
das hier faenden die neuen Felder in einer alten Datei kein Zuhause.
|
||||
"""
|
||||
da = {z["name"] for z in self.con.execute("PRAGMA table_info('buchungen')")}
|
||||
for name, vorgabe in (("art", "'rechnung'"), ("storno_zu", "''"),
|
||||
("storno_datum", "''"), ("vorgang", "''")):
|
||||
if name not in da:
|
||||
self.con.execute(
|
||||
f"ALTER TABLE buchungen ADD COLUMN {name} TEXT NOT NULL DEFAULT {vorgabe}")
|
||||
log.info("Journal um Spalte %s ergänzt", name)
|
||||
self.con.commit()
|
||||
|
||||
def schliessen(self):
|
||||
self.con.close()
|
||||
|
||||
|
|
@ -156,6 +176,10 @@ class Journal:
|
|||
datum=datetime.strptime(r["datum"], "%Y-%m-%d").date(),
|
||||
nachname=r["nachname"], naechte=r["naechte"], gezahlt=r["gezahlt"],
|
||||
satz=r["satz"], quelle=r["quelle"], pdf_pfad=r["pdf_pfad"],
|
||||
art=(r["art"] if "art" in r.keys() else "rechnung"),
|
||||
storno_zu=(r["storno_zu"] if "storno_zu" in r.keys() else ""),
|
||||
storno_datum=(r["storno_datum"] if "storno_datum" in r.keys() else ""),
|
||||
vorgang=(r["vorgang"] if "vorgang" in r.keys() else ""),
|
||||
)
|
||||
|
||||
def _pdf_bekannt(self, pfad: str) -> bool:
|
||||
|
|
@ -179,16 +203,20 @@ class Journal:
|
|||
cur = self.con.execute(
|
||||
"INSERT INTO buchungen "
|
||||
" (jahr, rechnungsnummer, datum, nachname, naechte, gezahlt, satz, "
|
||||
" quelle, pdf_pfad, pdf_mtime, pdf_groesse) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?) "
|
||||
" quelle, pdf_pfad, pdf_mtime, pdf_groesse, art, storno_zu, "
|
||||
" storno_datum, vorgang) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
|
||||
"ON CONFLICT(pdf_pfad) WHERE pdf_pfad <> '' DO UPDATE SET "
|
||||
" jahr=excluded.jahr, rechnungsnummer=excluded.rechnungsnummer, "
|
||||
" datum=excluded.datum, nachname=excluded.nachname, naechte=excluded.naechte, "
|
||||
" gezahlt=excluded.gezahlt, satz=excluded.satz, quelle=excluded.quelle, "
|
||||
" pdf_pfad=excluded.pdf_pfad, pdf_mtime=excluded.pdf_mtime, "
|
||||
" pdf_groesse=excluded.pdf_groesse",
|
||||
" pdf_groesse=excluded.pdf_groesse, art=excluded.art, "
|
||||
" storno_zu=excluded.storno_zu, storno_datum=excluded.storno_datum, "
|
||||
" vorgang=excluded.vorgang",
|
||||
(b.jahr, b.rechnungsnummer, b.datum.isoformat(), b.nachname, int(b.naechte or 0),
|
||||
float(b.gezahlt or 0), float(b.satz or 0), b.quelle, b.pdf_pfad, mtime, groesse))
|
||||
float(b.gezahlt or 0), float(b.satz or 0), b.quelle, b.pdf_pfad, mtime, groesse,
|
||||
b.art or "rechnung", b.storno_zu or "", b.storno_datum or "", b.vorgang or ""))
|
||||
self.con.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
|
@ -214,10 +242,15 @@ class Journal:
|
|||
|
||||
Gibt (neu, aktualisiert, uebersprungen, fehler) zurück.
|
||||
"""
|
||||
from pdf_parser import parse_pdf, keine_buchung, ParserFehler # lokal: hält modell/db leichtgewichtig
|
||||
from pdf_parser import (parse_pdf, berichtigung_daten,
|
||||
ParserFehler) # lokal: hält modell/db leichtgewichtig
|
||||
|
||||
neu = akt = uebersprungen = 0
|
||||
fehler = []
|
||||
# Berichtigungsblaetter werden gesammelt und ERST AM ENDE angewandt: das
|
||||
# Blatt kann vor "seiner" Rechnung im Ordner liegen, und dann gaebe es
|
||||
# noch nichts zu berichtigen.
|
||||
berichtigungen = []
|
||||
if not os.path.isdir(ordner):
|
||||
return 0, 0, 0, [(ordner, "Ordner existiert nicht")]
|
||||
|
||||
|
|
@ -230,10 +263,14 @@ class Journal:
|
|||
uebersprungen += 1
|
||||
continue
|
||||
# Berichtigungsblatt: traegt die Nummer der Rechnung und Nullen.
|
||||
# Frueher haette es die echte Buchung ueberschrieben.
|
||||
if keine_buchung(pfad):
|
||||
# Frueher haette es die echte Buchung ueberschrieben. Es bucht
|
||||
# nichts - aber die berichtigten Angaben zum Gast gehoeren
|
||||
# uebernommen, sonst steht im Amtsbericht weiter der falsche Name.
|
||||
berichtigt = berichtigung_daten(pfad)
|
||||
if berichtigt is not None:
|
||||
self.con.execute("DELETE FROM buchungen WHERE pdf_pfad=?",
|
||||
(os.path.abspath(pfad),))
|
||||
berichtigungen.append(berichtigt)
|
||||
continue
|
||||
try:
|
||||
b = parse_pdf(pfad, standard_satz)
|
||||
|
|
@ -255,6 +292,37 @@ class Journal:
|
|||
else:
|
||||
neu += 1
|
||||
|
||||
log.info("Scan fertig: %d neu, %d aktualisiert, %d unverändert, %d Fehler",
|
||||
neu, akt, uebersprungen, len(fehler))
|
||||
berichtigt_anzahl = self._berichtigungen_anwenden(berichtigungen)
|
||||
log.info("Scan fertig: %d neu, %d aktualisiert, %d unverändert, %d berichtigt, "
|
||||
"%d Fehler", neu, akt, uebersprungen, berichtigt_anzahl, len(fehler))
|
||||
return neu, akt, uebersprungen, fehler
|
||||
|
||||
def _berichtigungen_anwenden(self, berichtigungen):
|
||||
"""Berichtigte Empfaengerangaben in die zugehoerige Buchung uebernehmen.
|
||||
|
||||
Nach dem Datum der Berichtigung sortiert - gibt es zu einer Rechnung
|
||||
mehrere Blaetter, gilt das juengste.
|
||||
"""
|
||||
from modell import zerlege_nummer
|
||||
|
||||
def wann(k):
|
||||
teile = str(k.get("berichtigt_am") or "").split(".")
|
||||
return tuple(reversed(teile)) if len(teile) == 3 else ("",)
|
||||
|
||||
geaendert = 0
|
||||
for kopf in sorted(berichtigungen, key=wann):
|
||||
nachname = str(kopf.get("nachname") or "").strip()
|
||||
bezug = str(kopf.get("berichtigt_zu") or kopf.get("rechnungsnummer") or "")
|
||||
if not nachname or not bezug:
|
||||
continue
|
||||
jahr, nummer = zerlege_nummer(bezug, date.today().year)
|
||||
cur = self.con.execute(
|
||||
"UPDATE buchungen SET nachname=? "
|
||||
"WHERE jahr=? AND rechnungsnummer=? AND nachname<>?",
|
||||
(nachname, jahr, nummer, nachname))
|
||||
if cur.rowcount:
|
||||
geaendert += cur.rowcount
|
||||
log.info("Berichtigung %s-%s: Name jetzt %r", jahr, nummer, nachname)
|
||||
if geaendert:
|
||||
self.con.commit()
|
||||
return geaendert
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue