Mini-API zwischen beiden Programmen, Proforma-Rechnung
MINI-API (seine Ansage: "die muessen ja sehen das die beiden auf sind"): Jedes Programm macht beim Start einen winzigen Server auf 127.0.0.1 mit einem Port, den das Betriebssystem aussucht, und traegt Port und PID in die gemeinsame SQLite ein - die ist das Telefonbuch. Zwei Nachrichten reichen: "ping" wird mit "hallo, bin noch da" beantwortet, "schau_nach" ist der Push. Der Push traegt KEINE Daten. Er sagt nur "schau in die sqlite" - dort steht die Meldung ohnehin. Eine Wahrheit, und wenn der Anruf nicht durchgeht, findet das andere Programm die Meldung beim naechsten Nachsehen. Gebunden wird nur auf Loopback: von aussen ist nichts erreichbar, Windows fragt bei 127.0.0.1 nicht nach der Firewall, und die API fuehrt keine Befehle aus. Kein Netzwerk-Server mit MySQL - dafuer braeuchte er Infrastruktur, die es nicht gibt; auf verschiedenen Rechnern waere ohnehin ein anderer Bau noetig. PROFORMA-RECHNUNG: ein Knopf, gewaehlt wird im Popup - die fruehere Sicherheitsfrage IST diese Auswahl geworden (eine Frage statt zwei). Seine Bedingung: "musst nur auf passen das der rechner dann die rechnungs nummer nicht aendert". Die Proforma laeuft in einer EIGENEN Reihe (2026-P001); Nummernfeld, Rechnungszaehler, Nummernbuch und die Meldung ans Journal bleiben unberuehrt. Genau das prueft pruef_proforma.py zuerst. An den Quellen geprueft, nicht an Ratgeberseiten: * § 14 Abs. 1 S. 1 UStG / UStAE 14.1 - Rechnung ist jedes Dokument, mit dem abgerechnet wird, "gleichgueltig, wie dieses Dokument im Geschaeftsverkehr bezeichnet wird". Das Wort "Proforma" allein schuetzt also nicht. * § 14c Abs. 2 UStG - wer Steuer gesondert ausweist, ohne berechtigt zu sein, schuldet den Betrag; Korrektur nur mit Antrag und Zustimmung des Finanzamts. * § 14 Abs. 4 Nr. 4 UStG - die fortlaufende, einmalig vergebene Nummer gilt fuer Rechnungen. Deshalb: Ueberschrift "Proforma-Rechnung", der Satz "keine Rechnung im Sinne des § 14 UStG ... nicht zu bezahlen", "Proforma an:" statt "Rechnung an:", die Nummer ausdruecklich als KEINE Rechnungsnummer, KEIN Steuersatz und KEIN Steuerbetrag (auch die Beherbergungssteuer nicht - sonst waere das Blatt halb ausgewiesen, halb nicht), dafuer "Voraussichtlicher Gesamtbetrag" mit dem Hinweis, dass alles enthalten ist. Kein Blatt fuers Amt, kein Zahlungstext. TESTLAUF: _testwahl sagt, was im Auswahlfenster geklickt worden waere; pruef_proforma setzt _dialog_zeigen und drueckt die echten Knoepfe. Pruefstaende: pruef_proforma.py (neu, mit dem echten Popup), pruef_montage, pruef_storno, pruef_kette, pruef_kernregeln, pruef_nummernbuch, pruef_nummern, pruef_dialoge - 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
8163213960
commit
94c2c7881a
7 changed files with 703 additions and 46 deletions
|
|
@ -188,13 +188,17 @@ def _kopf_fuss(canvas, doc, cfg, amt=False):
|
|||
|
||||
|
||||
# ---- Hauptfunktion ----------------------------------------------------------
|
||||
def _positionstabelle(story, st, rechnung):
|
||||
"""Die gewohnte Positionstabelle der Campingrechnung."""
|
||||
def _positionstabelle(story, st, rechnung, ohne_ust=False):
|
||||
"""Die gewohnte Positionstabelle der Campingrechnung.
|
||||
|
||||
ohne_ust=True laesst die Spalte mit dem Steuersatz weg - auf einer
|
||||
Proforma steht bewusst kein Steuersatz und kein Steuerbetrag
|
||||
(§ 14c UStG: wer Steuer ausweist, schuldet sie)."""
|
||||
col_w = [10 * mm, 70 * mm, 12 * mm, 21 * mm, 13 * mm, 22 * mm, 26 * mm]
|
||||
data = [[
|
||||
Paragraph("Pos.", st["th"]),
|
||||
Paragraph("Leistung", st["th_l"]),
|
||||
Paragraph("USt %", st["th"]),
|
||||
Paragraph("" if ohne_ust else "USt %", st["th"]),
|
||||
Paragraph("Anzahl<br/>Tage/Menge", st["th"]),
|
||||
Paragraph("Pers.", st["th"]),
|
||||
Paragraph("E.-Preis", st["th"]),
|
||||
|
|
@ -205,7 +209,7 @@ def _positionstabelle(story, st, rechnung):
|
|||
data.append([
|
||||
Paragraph(str(z.nr), st["cell_c"]),
|
||||
Paragraph(z.leistung, st["leistung"]),
|
||||
Paragraph(str(z.satz), st["cell_c"]),
|
||||
Paragraph("" if ohne_ust else str(z.satz), st["cell_c"]),
|
||||
Paragraph(eur_kurz(z.anzahl).replace(",00", ""), st["cell_c"]),
|
||||
Paragraph(pers, st["cell_c"]),
|
||||
Paragraph(eur_kurz(z.preis), st["cell_r"]),
|
||||
|
|
@ -274,7 +278,8 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
story.append(Spacer(1, 5 * mm))
|
||||
|
||||
# Kundenadresse (links) | Rechnungsinfos (rechts)
|
||||
kunde_flow = [Paragraph("Rechnung an:", st["addr_label"])]
|
||||
kunde_flow = [Paragraph("Proforma an:" if kopf.get("art") == "proforma"
|
||||
else "Rechnung an:", st["addr_label"])]
|
||||
for z in kopf.get("kunde_zeilen", []):
|
||||
if str(z).strip():
|
||||
kunde_flow.append(Paragraph(str(z), st["addr"]))
|
||||
|
|
@ -286,8 +291,10 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
# Keine Positionstabelle, keine Naechte im Kopf - der Zeitraum steht in der
|
||||
# einen Textzeile, so wie Bartl das Blatt bisher von Hand geschrieben hat.
|
||||
montage = kopf.get("vorlage") == "montage"
|
||||
proforma = art == "proforma"
|
||||
info_rows = [
|
||||
("Stornonummer:" if art == "storno" else "Rechnungsnummer:",
|
||||
("Stornonummer:" if art == "storno" else
|
||||
"Proforma-Nr.:" if proforma else "Rechnungsnummer:",
|
||||
kopf.get("rechnungsnummer", "")),
|
||||
("Rechnungsdatum:", kopf.get("datum", "")),
|
||||
]
|
||||
|
|
@ -352,23 +359,40 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
# Das Wort "Stornorechnung" muss auf dem Blatt stehen, damit der Beleg
|
||||
# eindeutig als Aufhebung erkennbar ist.
|
||||
titel = {"storno": "Stornorechnung",
|
||||
"berichtigung": "Berichtigte Rechnung"}.get(art, "Rechnungsübersicht")
|
||||
"berichtigung": "Berichtigte Rechnung",
|
||||
"proforma": "Proforma-Rechnung"}.get(art, "Rechnungsübersicht")
|
||||
titel += " (für das Amt)" if gesamtrechnung_betont else ""
|
||||
story.append(Paragraph(titel, st["h"]))
|
||||
if proforma:
|
||||
# Ohne diesen Satz sieht das Blatt aus wie eine Rechnung. Es ist keine -
|
||||
# und ohne die deutliche Kennzeichnung kann das Finanzamt es als eine
|
||||
# werten. Ein Steuersatz oder Steuerbetrag steht bewusst NIRGENDS auf
|
||||
# dem Blatt: wer Steuer ausweist, schuldet sie (§ 14c UStG).
|
||||
story.append(Paragraph(
|
||||
"<b>Dies ist eine Proforma-Rechnung</b> – eine unverbindliche Vorab-"
|
||||
"Aufstellung der Kosten. Sie ist <b>keine Rechnung im Sinne des § 14 "
|
||||
"UStG</b>, berechtigt nicht zum Vorsteuerabzug und ist nicht zu bezahlen. "
|
||||
"Die Nummer oben ist <b>keine Rechnungsnummer</b>. Alle Beträge in Euro "
|
||||
"inkl. gesetzlicher Umsatzsteuer; ausgewiesen wird sie erst in der "
|
||||
"Rechnung nach dem Aufenthalt.", st["dank"]))
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
if montage:
|
||||
_montage_zeile(story, st, rechnung, kopf)
|
||||
else:
|
||||
_positionstabelle(story, st, rechnung)
|
||||
_positionstabelle(story, st, rechnung, ohne_ust=proforma)
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
# ---- Summenblock: Netto -> USt je Satz -> Zwischensumme (brutto)
|
||||
# -> zzgl. Beherbergungssteuer -> Gesamtbetrag ----
|
||||
summen_rows = [[
|
||||
# Auf der Proforma steht weder Netto noch Umsatzsteuer: ausgewiesen wird
|
||||
# sie erst mit der Rechnung. Die Beherbergungssteuer ist keine Umsatzsteuer
|
||||
# und darf bleiben - der Gast soll sehen, was er zahlen wird.
|
||||
summen_rows = [] if proforma else [[
|
||||
Paragraph("Nettobetrag:", st["info_l"]),
|
||||
Paragraph(eur(rechnung.netto_gesamt), st["cell_r"]),
|
||||
]]
|
||||
for b in rechnung.bloecke:
|
||||
for b in ([] if proforma else rechnung.bloecke):
|
||||
name = b.bezeichnung or "USt"
|
||||
summen_rows.append([
|
||||
Paragraph(f"{name} {b.satz} %:", st["info_l"]),
|
||||
|
|
@ -384,7 +408,10 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
#
|
||||
# Auf der Kundenseite bleibt alles wie gehabt - dort MUSS die Steuer ausgewiesen
|
||||
# sein und im Gesamtbetrag stecken, das ist der Betrag, den der Gast zahlt.
|
||||
if rechnung.aufschlaege and not gesamtrechnung_betont:
|
||||
# Auf der Proforma wird gar keine Steuer ausgewiesen - weder Umsatzsteuer
|
||||
# noch Beherbergungssteuer. Es steht nur, was der Gast voraussichtlich
|
||||
# zahlt; aufgeschluesselt wird erst in der Rechnung.
|
||||
if rechnung.aufschlaege and not gesamtrechnung_betont and not proforma:
|
||||
summen_rows.append([
|
||||
Paragraph("Zwischensumme:", st["info_l"]),
|
||||
Paragraph(eur(rechnung.brutto), st["cell_r"]),
|
||||
|
|
@ -398,7 +425,9 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
|
||||
endsumme = rechnung.brutto if gesamtrechnung_betont else rechnung.gesamt
|
||||
summen_rows.append([
|
||||
Paragraph("Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:", st["total_l"]),
|
||||
Paragraph("Voraussichtlicher Gesamtbetrag:" if proforma else
|
||||
"Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:",
|
||||
st["total_l"]),
|
||||
Paragraph(eur(endsumme), st["total_v"]),
|
||||
])
|
||||
style_cmds = [
|
||||
|
|
@ -415,7 +444,12 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
|
||||
# Hinweis zur Beherbergungssteuer - nur auf der Kundenseite. Auf dem Amt-Blatt
|
||||
# wird die Steuer gar nicht mehr ausgewiesen, dann waere der Satz sinnlos.
|
||||
if not gesamtrechnung_betont:
|
||||
if proforma:
|
||||
story.append(Spacer(1, 1.5 * mm))
|
||||
story.append(Paragraph(
|
||||
"In diesem Betrag sind alle Steuern und Abgaben enthalten. Aufgeschlüsselt "
|
||||
"werden sie erst in der Rechnung nach dem Aufenthalt.", st["dank"]))
|
||||
elif not gesamtrechnung_betont:
|
||||
for a in rechnung.aufschlaege:
|
||||
if not a.bezeichnung:
|
||||
continue
|
||||
|
|
@ -467,6 +501,15 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
"ihre Nummer.", st["dank"]))
|
||||
if texte.get("dank1"):
|
||||
block.append(Paragraph(texte["dank1"], st["dank"]))
|
||||
elif proforma:
|
||||
# Dank- und Zahlungstexte waeren hier falsch: der Aufenthalt liegt
|
||||
# noch vor uns, und bezahlt wird nichts. Der Hinweis "bitte die
|
||||
# Rechnungsnummer angeben" wuerde dem Blatt sogar widersprechen.
|
||||
block.append(Paragraph(
|
||||
"Diese Aufstellung ist unverbindlich und dient nur zur Information. "
|
||||
"Sie ist keine Zahlungsaufforderung – die Rechnung erhalten Sie nach "
|
||||
"dem Aufenthalt. Für Rückfragen stehen wir gern zur Verfügung.",
|
||||
st["dank"]))
|
||||
else:
|
||||
if art == "korrektur" and kopf.get("ersetzt"):
|
||||
hinweis = f"Diese Rechnung ersetzt die Rechnung {kopf['ersetzt']}"
|
||||
|
|
@ -507,6 +550,22 @@ def _kenndaten(rechnung, kopf):
|
|||
Steuerrechner summieren dadurch von allein richtig (+100 -100 +120 = 120).
|
||||
"""
|
||||
aufschlag = rechnung.aufschlaege[0] if rechnung.aufschlaege else None
|
||||
if kopf.get("art") == "proforma":
|
||||
# Eine Proforma ist kein Umsatz - sie traegt Nullen. Ein Programm, das
|
||||
# "art" nicht kennt, zaehlt dann 0,00 statt einer Uebernachtung, die nie
|
||||
# abgerechnet wurde.
|
||||
return json.dumps({
|
||||
"quelle": "rechnungstool", "version": 3, "art": "proforma",
|
||||
"rechnungsnummer": kopf.get("rechnungsnummer", ""),
|
||||
"datum": kopf.get("datum", ""),
|
||||
"nachname": kopf.get("nachname", ""), "vorname": kopf.get("vorname", ""),
|
||||
"naechte": kopf.get("naechte", ""),
|
||||
"zwischensumme": 0.0, "gesamt": 0.0,
|
||||
"steuer_satz": 0.0, "steuer_betrag": 0.0,
|
||||
"netto_gesamt": 0.0, "ust_gesamt": 0.0, "ust_bloecke": [],
|
||||
"kunde": kopf.get("kunde", {}),
|
||||
"anreise": kopf.get("anreise", ""), "abreise": kopf.get("abreise", ""),
|
||||
}, ensure_ascii=False)
|
||||
if kopf.get("art") == "berichtigung":
|
||||
# Das berichtigte Blatt zeigt dieselben Betraege wie die Rechnung, ist aber
|
||||
# KEINE zweite Einnahme. Es traegt deshalb Nullen: ein Programm, das "art"
|
||||
|
|
@ -592,14 +651,17 @@ def render_rechnung(pfad, cfg, rechnung, kopf):
|
|||
ziel, pagesize=A4,
|
||||
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
|
||||
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
|
||||
title=("Stornorechnung" if kopf.get("art") == "storno" else "Rechnung")
|
||||
title=({"storno": "Stornorechnung",
|
||||
"proforma": "Proforma-Rechnung"}.get(kopf.get("art"), "Rechnung"))
|
||||
+ f" {kopf.get('rechnungsnummer','')}",
|
||||
author=cfg["firma"].get("name", ""),
|
||||
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
|
||||
)
|
||||
story = []
|
||||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=False) # Kunde
|
||||
if rechnung.aufschlaege:
|
||||
# Eine Proforma bekommt KEIN Blatt fuers Amt: gemeldet wird eine
|
||||
# Uebernachtung erst, wenn sie abgerechnet ist.
|
||||
if rechnung.aufschlaege and kopf.get("art") != "proforma":
|
||||
story.append(PageBreak())
|
||||
story.append(_AmtMarke(marke))
|
||||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue