Oberflaeche im Pcore-Stil: Tuerkis, Kacheln, Glas-Knoepfe, Roboto
Seine Ansage: "schau dir unsern fivem server an und unsere webseiten, dann
weisst du wie mein design ist" - also die Palette aus _ui_www und der
Adminpage: Tuerkis #06C6A4 auf gestuften dunklen Flaechen (#0d0e10 Grund,
#15171b Kachel), Raender #25282e, Text #e8e8ea / #8a8e96, Rot #e85a5a.
Das PROGRAMM traegt diese Handschrift, die RECHNUNG die Farbe der jeweiligen
Firma - das Blatt gehoert dem Kunden, nicht uns.
- Roboto wird mitgeliefert und nur fuer den Prozess geladen
(AddFontResourceEx mit FR_PRIVATE): keine Installation, keine Adminrechte,
nach dem Schliessen ist der Rechner wie vorher. Fehlt sie, bleibt es bei
Segoe UI. Die Schriftdateien gehen mit in die exe (spec).
- Abschnitte sind Kacheln mit Ueberschrift statt grauer Rahmen.
- Knoepfe im Glas-Stil wie in seinen Oberflaechen: getoente Flaeche, Rand in
Akzent-Transparenz. Tk kann kein backdrop-filter - die Toenung wird
ausgerechnet (theme.mischen), das Ergebnis ist auf einfarbigem Grund
identisch. "PDF erstellen" fuehrt, "Storno" ist rot.
- Zebrastreifen in der Leistungsliste: bei 17 Zeilen verrutscht man sonst.
- Kein expand am offenen Reiter mehr - dadurch sprang beim Umschalten die
ganze Leiste ("das taben macht den aktiven kleiner").
- Das Fenster fuer den gemeinsamen Ordner stand als einziges noch in Weiss da:
es kam nie durch _style_dialog.
Dazu ein Fund, der nichts mit dem Design zu tun hat: die Pruefstaende erbten
"nummer_eingerichtet" aus der config DIESES Rechners. Sobald hier jemand die
Nummer einmal einrichtet, liefen sie in einen anderen Zweig und wurden rot -
ohne dass sich am Programm etwas geaendert haette. Sie legen den Zustand jetzt
selbst fest. Alle 14 gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
parent
0ad03fd1a4
commit
5ebb02b267
17 changed files with 330 additions and 87 deletions
110
app.py
110
app.py
|
|
@ -39,6 +39,7 @@ from bestand import (hoechster_zaehler_im_ordner, naechste_freie_nummer,
|
|||
vorhandene_rechnungen)
|
||||
from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn,
|
||||
laufende_pid_lesen)
|
||||
import theme
|
||||
from theme import ThemeMixin
|
||||
from korrektur import KorrekturMixin
|
||||
from einstellungen import EinstellungenMixin
|
||||
|
|
@ -202,12 +203,13 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
|
||||
# ---------------------------------------------------------------- Kopf
|
||||
def _build_kopf(self):
|
||||
top = ttk.Frame(self, padding=(12, 10, 12, 4))
|
||||
top = ttk.Frame(self, padding=(16, 14, 16, 6))
|
||||
top.pack(fill="x")
|
||||
|
||||
# Kunde (links)
|
||||
kunde = ttk.LabelFrame(top, text="Rechnung an (Kunde)", padding=8)
|
||||
kunde.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
|
||||
kunde = ttk.LabelFrame(top, text="RECHNUNG AN (KUNDE)", padding=14,
|
||||
style="Karte.TLabelframe")
|
||||
kunde.grid(row=0, column=0, sticky="nsew", padx=(0, 14))
|
||||
self.k_anrede = tk.StringVar()
|
||||
self.k_vorname = tk.StringVar()
|
||||
self.k_name = tk.StringVar() # Nachname oder Firma – die Zuordnung im Steuerjournal
|
||||
|
|
@ -223,7 +225,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
("PLZ / Ort *", self.k_plzort),
|
||||
]
|
||||
for i, (lbl, var) in enumerate(felder):
|
||||
ttk.Label(kunde, text=lbl).grid(row=i, column=0, sticky="w", pady=2)
|
||||
ttk.Label(kunde, text=lbl, style="Karte.TLabel").grid(
|
||||
row=i, column=0, sticky="w", pady=4)
|
||||
if var is self.k_anrede:
|
||||
# Auswahlliste, aber nicht gesperrt - Sonderfaelle kann man tippen.
|
||||
w = ttk.Combobox(kunde, textvariable=var, width=36, values=ANREDEN)
|
||||
|
|
@ -232,12 +235,13 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
w.grid(row=i, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||||
ttk.Label(kunde, text="* Pflicht – Name und Anschrift des Kunden gehören "
|
||||
"auf die Rechnung (§ 14 Abs. 4 Nr. 1 UStG).",
|
||||
foreground=self.col_muted, wraplength=330, justify="left").grid(
|
||||
style="KarteMuted.TLabel", wraplength=330, justify="left").grid(
|
||||
row=len(felder), column=0, columnspan=2, sticky="w", pady=(6, 0))
|
||||
kunde.columnconfigure(1, weight=1)
|
||||
|
||||
# Rechnungsinfos (rechts)
|
||||
info = ttk.LabelFrame(top, text="Rechnungsdaten", padding=8)
|
||||
info = ttk.LabelFrame(top, text="RECHNUNGSDATEN", padding=14,
|
||||
style="Karte.TLabelframe")
|
||||
info.grid(row=0, column=1, sticky="nsew")
|
||||
self.r_nummer = tk.StringVar()
|
||||
self.r_datum = tk.StringVar(value=datetime.date.today().strftime("%d.%m.%Y"))
|
||||
|
|
@ -253,8 +257,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
date_vars = (self.r_datum, self.r_anreise, self.r_abreise)
|
||||
self.datum_labels = {}
|
||||
for i, (lbl, var) in enumerate(rows):
|
||||
beschriftung = ttk.Label(info, text=lbl)
|
||||
beschriftung.grid(row=i, column=0, sticky="w", pady=2)
|
||||
beschriftung = ttk.Label(info, text=lbl, style="Karte.TLabel")
|
||||
beschriftung.grid(row=i, column=0, sticky="w", pady=4)
|
||||
if var is self.r_anreise:
|
||||
self.datum_labels["von"] = beschriftung
|
||||
elif var is self.r_abreise:
|
||||
|
|
@ -267,14 +271,16 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
self.nummer_entry = ent
|
||||
if var in date_vars: # Datum: Punkte automatisch, Cursor ans Ende
|
||||
ent.bind("<KeyRelease>", self._auto_datum_evt)
|
||||
self.naechte_label = ttk.Label(info, text="Nächte")
|
||||
self.naechte_label.grid(row=4, column=0, sticky="w", pady=2)
|
||||
ttk.Label(info, textvariable=self.r_naechte, font=("Segoe UI", 10, "bold")).grid(
|
||||
self.naechte_label = ttk.Label(info, text="Nächte", style="Karte.TLabel")
|
||||
self.naechte_label.grid(row=4, column=0, sticky="w", pady=4)
|
||||
ttk.Label(info, textvariable=self.r_naechte, style="Karte.TLabel",
|
||||
font=(theme.SCHRIFT_NAME, 11, "bold")).grid(
|
||||
row=4, column=1, sticky="w", padx=(8, 0))
|
||||
# Storno-Hinweis: steht nur da, solange ein Storno im Zwischenspeicher liegt.
|
||||
self.storno_hinweis = tk.StringVar(value="")
|
||||
ttk.Label(info, textvariable=self.storno_hinweis, foreground="#b00020",
|
||||
font=("Segoe UI", 9, "bold"), wraplength=200, justify="left").grid(
|
||||
ttk.Label(info, textvariable=self.storno_hinweis, style="Karte.TLabel",
|
||||
foreground=self.col_gefahr,
|
||||
font=(theme.SCHRIFT_NAME, 9, "bold"), wraplength=200, justify="left").grid(
|
||||
row=5, column=0, columnspan=2, sticky="w", pady=(6, 0))
|
||||
info.columnconfigure(1, weight=1)
|
||||
self.r_anreise.trace_add("write", lambda *a: self._update_naechte())
|
||||
|
|
@ -369,10 +375,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
self.blaetter.add(montage_tab, text=" Montage ")
|
||||
self._build_montage(montage_tab)
|
||||
|
||||
wrap = ttk.LabelFrame(camping, text="Leistungen – Anzahl (und ggf. Personen) eintragen", padding=(6, 4))
|
||||
wrap = ttk.LabelFrame(camping, text="LEISTUNGEN – ANZAHL (UND GGF. PERSONEN) EINTRAGEN",
|
||||
padding=(12, 8), style="Karte.TLabelframe")
|
||||
wrap.pack(side="top", fill="both", expand=True)
|
||||
|
||||
canvas = tk.Canvas(wrap, highlightthickness=0, bg=self.col_bg)
|
||||
canvas = tk.Canvas(wrap, highlightthickness=0, bg=self.col_card)
|
||||
sb = ttk.Scrollbar(wrap, orient="vertical", command=canvas.yview)
|
||||
self.pos_frame = ttk.Frame(canvas)
|
||||
self._pos_win = canvas.create_window((0, 0), window=self.pos_frame, anchor="nw")
|
||||
|
|
@ -399,10 +406,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
|
||||
# Kopfzeile (gleiches Grid wie die Daten)
|
||||
for i, (title, anchor, _) in enumerate(self.COLS):
|
||||
ttk.Label(self.pos_frame, text=title, anchor=anchor,
|
||||
font=("Segoe UI", 9, "bold")).grid(row=0, column=i, padx=3, pady=(0, 3), sticky="ew")
|
||||
ttk.Label(self.pos_frame, text=title.upper(), anchor=anchor,
|
||||
style="Abschnitt.TLabel").grid(
|
||||
row=0, column=i, padx=3, pady=(0, 5), sticky="ew")
|
||||
ttk.Separator(self.pos_frame, orient="horizontal").grid(
|
||||
row=1, column=0, columnspan=len(self.COLS), sticky="ew", pady=(0, 2))
|
||||
row=1, column=0, columnspan=len(self.COLS), sticky="ew", pady=(0, 4))
|
||||
|
||||
for idx, pos in enumerate(self.cfg["katalog"]):
|
||||
r = idx + 2
|
||||
|
|
@ -414,17 +422,26 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
av.trace_add("write", lambda *a: self.recalc())
|
||||
pv.trace_add("write", lambda *a: self.recalc())
|
||||
|
||||
ttk.Label(self.pos_frame, text=str(nr), anchor="center").grid(row=r, column=0, padx=3, pady=2, sticky="ew")
|
||||
# Jede zweite Zeile bekommt einen anderen Grund: bei 17 Positionen
|
||||
# verrutscht man beim Eintragen sonst leicht eine Zeile.
|
||||
zeilenstil = "Zebra.TLabel" if idx % 2 else "Karte.TLabel"
|
||||
ttk.Label(self.pos_frame, text=str(nr), anchor="center", style=zeilenstil).grid(
|
||||
row=r, column=0, padx=0, pady=3, sticky="ew", ipady=2)
|
||||
ttk.Label(self.pos_frame, text=pos["leistung"], anchor="w", wraplength=320,
|
||||
justify="left").grid(row=r, column=1, padx=3, pady=2, sticky="ew")
|
||||
ttk.Label(self.pos_frame, text=str(pos["satz"]), anchor="center").grid(row=r, column=2, padx=3, pady=2, sticky="ew")
|
||||
ttk.Entry(self.pos_frame, textvariable=av, width=8, justify="center").grid(row=r, column=3, padx=3, pady=2)
|
||||
justify="left", style=zeilenstil).grid(
|
||||
row=r, column=1, padx=0, pady=3, sticky="ew", ipady=2)
|
||||
ttk.Label(self.pos_frame, text=str(pos["satz"]), anchor="center",
|
||||
style=zeilenstil).grid(row=r, column=2, padx=0, pady=3, sticky="ew", ipady=2)
|
||||
ttk.Entry(self.pos_frame, textvariable=av, width=8, justify="center").grid(
|
||||
row=r, column=3, padx=4, pady=3)
|
||||
# Personen-Feld NUR bei "pro Person"-Positionen anzeigen
|
||||
if pos["pro_person"]:
|
||||
ttk.Entry(self.pos_frame, textvariable=pv, width=6, justify="center").grid(row=r, column=4, padx=3, pady=2)
|
||||
ttk.Label(self.pos_frame, text=berechnung.eur_kurz(pos["preis"]), anchor="e").grid(row=r, column=5, padx=3, pady=2, sticky="ew")
|
||||
g = ttk.Label(self.pos_frame, text="0,00", anchor="e")
|
||||
g.grid(row=r, column=6, padx=3, pady=2, sticky="ew")
|
||||
ttk.Entry(self.pos_frame, textvariable=pv, width=6, justify="center").grid(
|
||||
row=r, column=4, padx=4, pady=3)
|
||||
ttk.Label(self.pos_frame, text=berechnung.eur_kurz(pos["preis"]), anchor="e",
|
||||
style=zeilenstil).grid(row=r, column=5, padx=0, pady=3, sticky="ew", ipady=2)
|
||||
g = ttk.Label(self.pos_frame, text="0,00", anchor="e", style=zeilenstil)
|
||||
g.grid(row=r, column=6, padx=0, pady=3, sticky="ew", ipady=2)
|
||||
self.gesamt_lbls[nr] = g
|
||||
|
||||
# ------------------------------------------------------------ Montage
|
||||
|
|
@ -440,7 +457,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
self.m_monate.trace_add("write", lambda *a: self.recalc())
|
||||
|
||||
ttk.Label(eltern, text="Monatsmiete für einen Montagearbeiter",
|
||||
font=("Segoe UI", 11, "bold")).grid(row=0, column=0, columnspan=4, sticky="w")
|
||||
font=(theme.SCHRIFT_NAME, 11, "bold")).grid(row=0, column=0, columnspan=4, sticky="w")
|
||||
ttk.Label(eltern, wraplength=760, justify="left", foreground=self.col_muted,
|
||||
text="Den Zeitraum trägst du oben rechts ein (dieselben Felder wie Anreise/Abreise). "
|
||||
"Bezeichnung, Steuersatz und Monatspreis stehen unter "
|
||||
|
|
@ -451,17 +468,17 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
ttk.Entry(eltern, textvariable=self.m_monate, width=8, justify="center").grid(
|
||||
row=2, column=1, sticky="w", padx=(8, 24), pady=3)
|
||||
ttk.Label(eltern, text="Monatspreis (brutto):").grid(row=2, column=2, sticky="w", pady=3)
|
||||
self.m_preis = ttk.Label(eltern, text="", font=("Segoe UI", 10, "bold"))
|
||||
self.m_preis = ttk.Label(eltern, text="", font=(theme.SCHRIFT_NAME, 10, "bold"))
|
||||
self.m_preis.grid(row=2, column=3, sticky="w", padx=(8, 0), pady=3)
|
||||
|
||||
ttk.Separator(eltern, orient="horizontal").grid(
|
||||
row=3, column=0, columnspan=4, sticky="ew", pady=12)
|
||||
|
||||
ttk.Label(eltern, text="So steht es auf der Rechnung:",
|
||||
font=("Segoe UI", 9, "bold")).grid(row=4, column=0, columnspan=4, sticky="w")
|
||||
font=(theme.SCHRIFT_NAME, 9, "bold")).grid(row=4, column=0, columnspan=4, sticky="w")
|
||||
self.m_text = ttk.Label(eltern, text="", wraplength=760, justify="left")
|
||||
self.m_text.grid(row=5, column=0, columnspan=4, sticky="w", pady=(4, 10))
|
||||
self.m_zahlen = ttk.Label(eltern, text="", font=("Segoe UI", 10))
|
||||
self.m_zahlen = ttk.Label(eltern, text="", font=(theme.SCHRIFT_NAME, 10))
|
||||
self.m_zahlen.grid(row=6, column=0, columnspan=4, sticky="w")
|
||||
eltern.columnconfigure(3, weight=1)
|
||||
|
||||
|
|
@ -522,14 +539,14 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
status = ttk.Frame(self, padding=(12, 2))
|
||||
status.pack(side="bottom", fill="x")
|
||||
ttk.Label(status, text="powered by ravokk", foreground=self.col_muted,
|
||||
font=("Segoe UI", 8)).pack(side="right")
|
||||
font=(theme.SCHRIFT_NAME, 8)).pack(side="right")
|
||||
# Steht der gemeinsame Ordner? Das gehoert sichtbar hin - ohne ihn kennt
|
||||
# das Programm die Nummern des Steuerjournals nicht.
|
||||
self.buch_lbl = ttk.Label(status, text="", foreground=self.col_muted,
|
||||
font=("Segoe UI", 8))
|
||||
font=(theme.SCHRIFT_NAME, 8))
|
||||
self.buch_lbl.pack(side="left")
|
||||
|
||||
f = ttk.Frame(self, padding=(12, 6, 12, 10))
|
||||
f = ttk.Frame(self, padding=(16, 12, 16, 14))
|
||||
f.pack(side="bottom", fill="x")
|
||||
ttk.Separator(self, orient="horizontal").pack(side="bottom", fill="x")
|
||||
|
||||
|
|
@ -539,13 +556,17 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
|
||||
links = ttk.Frame(f)
|
||||
links.pack(side="left")
|
||||
ttk.Button(links, text="Vorschau", command=self.vorschau_zeigen, width=12).pack(side="left", padx=4)
|
||||
ttk.Button(links, text="PDF erstellen", command=self.pdf_erstellen, width=18).pack(side="left", padx=4)
|
||||
# "PDF erstellen" ist der Knopf, um den es geht - der traegt die
|
||||
# Akzentfarbe. Storno greift in eine ausgestellte Rechnung ein und ist
|
||||
# deshalb rot markiert, nicht weil er gefaehrlich waere, sondern damit
|
||||
# niemand ihn versehentlich statt "PDF erstellen" trifft.
|
||||
ttk.Button(links, text="PDF erstellen", command=self.pdf_erstellen,
|
||||
style="Haupt.TButton").pack(side="left", padx=(0, 6))
|
||||
ttk.Button(links, text="Vorschau", command=self.vorschau_zeigen).pack(side="left", padx=6)
|
||||
ttk.Button(links, text="Storno", command=self.dlg_storno,
|
||||
width=12).pack(side="left", padx=4)
|
||||
ttk.Button(links, text="Berichtigen", command=self.dlg_berichtigen,
|
||||
width=14).pack(side="left", padx=4)
|
||||
ttk.Button(links, text="Zurücksetzen", command=self.reset_formular, width=14).pack(side="left", padx=4)
|
||||
style="Gefahr.TButton").pack(side="left", padx=6)
|
||||
ttk.Button(links, text="Berichtigen", command=self.dlg_berichtigen).pack(side="left", padx=6)
|
||||
ttk.Button(links, text="Zurücksetzen", command=self.reset_formular).pack(side="left", padx=6)
|
||||
|
||||
# --------------------------------------------------- Gemeinsames Buch
|
||||
def _buch_oeffnen(self):
|
||||
|
|
@ -638,7 +659,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
rahmen = ttk.Frame(d, padding=18)
|
||||
rahmen.pack(fill="both", expand=True)
|
||||
ttk.Label(rahmen, text="Wo soll die Rechnungsnummer weiterlaufen?",
|
||||
font=("Segoe UI", 13, "bold")).grid(row=0, column=0, columnspan=2, sticky="w")
|
||||
font=(theme.SCHRIFT_NAME, 13, "bold")).grid(row=0, column=0, columnspan=2, sticky="w")
|
||||
ttk.Label(rahmen, wraplength=620, justify="left", text=(
|
||||
"Zwei Wege - such dir einen aus." + chr(10) + chr(10) +
|
||||
"Das wird EINMAL eingerichtet. Danach steht der Zähler fest: eine "
|
||||
|
|
@ -649,7 +670,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
|
||||
if gefunden:
|
||||
tk.Label(rahmen, wraplength=620, justify="left", fg="#1f5c3d",
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
font=(theme.SCHRIFT_NAME, 9, "bold"),
|
||||
text=f"Im Rechnungsordner und im Nummernbuch steht schon "
|
||||
f"{jahr}-{gefunden:03d} – niedriger geht es nicht.").grid(
|
||||
row=2, column=0, columnspan=2, sticky="w", pady=(0, 10))
|
||||
|
|
@ -677,7 +698,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
eingabe.pack(side="left", padx=(8, 0))
|
||||
weg_gewaehlt()
|
||||
hinweis = tk.Label(rahmen, text="", wraplength=620, justify="left", fg="#a00000",
|
||||
font=("Segoe UI", 9))
|
||||
font=(theme.SCHRIFT_NAME, 9))
|
||||
hinweis.grid(row=5, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
|
||||
def uebernehmen():
|
||||
|
|
@ -957,7 +978,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
rows.append(("Gesamtbetrag:", berechnung.eur(r.gesamt), True))
|
||||
|
||||
for i, (lbl, val, fett) in enumerate(rows):
|
||||
font = ("Segoe UI", 12, "bold") if fett else ("Segoe UI", 10)
|
||||
font = ((theme.SCHRIFT_NAME, 15, "bold") if fett
|
||||
else (theme.SCHRIFT_NAME, 10))
|
||||
pady = (5, 0) if fett else (1, 0)
|
||||
ttk.Label(self.summen_box, text=lbl, font=font).grid(row=i, column=0, sticky="e", padx=6, pady=pady)
|
||||
vkw = {"font": font, "width": 14, "anchor": "e"}
|
||||
|
|
@ -1040,7 +1062,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
rahmen = ttk.Frame(d, padding=18)
|
||||
rahmen.pack(fill="both", expand=True)
|
||||
ttk.Label(rahmen, text=f"{wer or 'ohne Namen'} – {betrag}",
|
||||
font=("Segoe UI", 12, "bold")).grid(row=0, column=0, sticky="w")
|
||||
font=(theme.SCHRIFT_NAME, 12, "bold")).grid(row=0, column=0, sticky="w")
|
||||
|
||||
ttk.Label(rahmen, wraplength=560, justify="left", text=(
|
||||
f"RECHNUNG {nummer} – der Beleg. Danach lässt sie sich nicht mehr "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue