5 Commits
Author SHA1 Message Date
Kerim AtilganandClaude Opus 5.5 2600eea574 Abrechnungen links im Hauptfenster (Klick öffnet), Import fragt nach der Ziel-Abrechnung
Build / linux (push) Successful in 1m50s
Build / windows-exe (push) Successful in 7m49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:35:16 +01:00
Kerim AtilganandClaude Opus 5.5 057cca6378 Export: Knopf „Noch nicht abgerechnet“; Version 1.5.0 (mit Windows-Setup)
Build / linux (push) Successful in 1m52s
Build / windows-exe (push) Successful in 7m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:44:46 +01:00
Kerim AtilganandClaude Opus 5.5 ec99042569 Windows-Setup (Inno Setup): Installation pro Benutzer, Installer-Test im Build
- PyInstaller baut unter Windows zusätzlich die Ordner-Variante dist/Provisionsmanager/
  (schnellerer Start); onefile-.exe und .zip bleiben, Linux unverändert
- build/installer.iss: ohne Admin nach %LOCALAPPDATA%\Programs\Provisionsmanager,
  Startmenü, optional Desktop, deutsch, feste AppId, CloseApplications,
  Benutzerdaten in %APPDATA%\Provisionsmanager95 bleiben bei Deinstallation erhalten
- build_windows.sh: Inno Setup 6.7.3 (SHA256 geprüft) unter Wine installieren, Setup bauen,
  still installieren, Selbsttest der installierten .exe, still deinstallieren und prüfen
- Pipeline: Setup als Artefakt und am Release; README-Abschnitt Installation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:31:16 +01:00
Kerim AtilganandClaude Opus 5.5 7b9a2acec0 Testdaten: erfundene statt echter Kundennummer
Build / linux (push) Successful in 1m58s
Build / windows-exe (push) Successful in 7m19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:15:08 +01:00
Kerim AtilganandClaude Opus 5.5 8c86571034 Modernes Design: neue Symbole und Programm-Icon, ohne „95“ im Namen, Tipp des Tages ohne Büroklammer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:12:05 +01:00
43 changed files with 389 additions and 63 deletions

No files matched your search

+5 -2
View File
@@ -1,6 +1,6 @@
# Baut die Windows-Version (.exe, mit Windows-Python unter Wine) und die Linux-Version. # Baut die Windows-Version (.exe, mit Windows-Python unter Wine) und die Linux-Version.
# - jeder Push / PR: Tests + Programmdateien als Build-Artefakte # - jeder Push / PR: Tests + Programmdateien als Build-Artefakte
# - Tag "v*" (z. B. v1.0.0): zusätzlich Gitea-Release mit .exe, .zip und Linux-.tar.gz # - Tag "v*" (z. B. v1.0.0): zusätzlich Gitea-Release mit .exe, .zip, Setup und Linux-.tar.gz
name: Build name: Build
on: on:
@@ -37,6 +37,7 @@ jobs:
path: | path: |
dist/Provisionsmanager95.exe dist/Provisionsmanager95.exe
dist/Provisionsmanager95-*.zip dist/Provisionsmanager95-*.zip
dist/Provisionsmanager-Setup-*.exe
- name: Release erstellen / ergänzen - name: Release erstellen / ergänzen
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
@@ -44,7 +45,9 @@ jobs:
TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITEA_TOKEN }} TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITEA_TOKEN }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
run: build/release_hochladen.sh dist/Provisionsmanager95.exe dist/Provisionsmanager95-$(tr -d '\r\n' < dist/VERSION).zip run: |
V=$(tr -d '\r\n' < dist/VERSION)
build/release_hochladen.sh dist/Provisionsmanager95.exe dist/Provisionsmanager95-$V.zip dist/Provisionsmanager-Setup-$V.exe
linux: linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1
View File
@@ -16,3 +16,4 @@ build/version_info.txt
selbsttest.txt selbsttest.txt
/*.jpg /*.jpg
/*.png /*.png
.claude/
+14 -2
View File
@@ -5,6 +5,16 @@ von **freenet** (CSV/Excel) und **Vodafone/Euronics** (Excel) ein, ordnet die Po
Unterhändlern zu, erlaubt freie Anpassung der Auszahlung und exportiert je Händler eine Unterhändlern zu, erlaubt freie Anpassung der Auszahlung und exportiert je Händler eine
fertige Excel-Abrechnung. Optik: Windows 95. fertige Excel-Abrechnung. Optik: Windows 95.
## Installation (Windows)
`Provisionsmanager-Setup-<version>.exe` aus dem Release herunterladen und starten – keine
Admin-Rechte nötig. Das Programm landet in `%LOCALAPPDATA%\Programs\Provisionsmanager` mit
Eintrag im Startmenü (Desktop-Verknüpfung optional) und startet schneller als die einzelne `.exe`.
Ein Update installiert man einfach drüber (ein laufendes Programm wird dafür geschlossen).
Deinstallieren über *Einstellungen ▸ Apps*; Datenbank und Einstellungen in
`%APPDATA%\Provisionsmanager95` bleiben dabei erhalten. Ohne Installation geht weiterhin
`Provisionsmanager95.exe` (bzw. das `.zip`) – einfach starten.
## Erster Start ## Erster Start
Beim ersten Start führt ein **Einrichtungs-Assistent** durch Speicherort, Unterhändler Beim ersten Start führt ein **Einrichtungs-Assistent** durch Speicherort, Unterhändler
@@ -70,7 +80,7 @@ Das Hauptfenster ist unter Windows ein echtes Programmfenster mit Win95-Optik: a
ziehen (Andocken), `Win`+Pfeiltasten, Doppelklick auf die Titelleiste und Ziehen am Rand funktionieren ziehen (Andocken), `Win`+Pfeiltasten, Doppelklick auf die Titelleiste und Ziehen am Rand funktionieren
wie gewohnt. wie gewohnt.
**Zeitraum abrechnen:** Im Export-Dialog „Nur von … bis …“ eintragen (oder „Vormonat“), wahlweise nach **Zeitraum abrechnen:** Im Export-Dialog „Nur von … bis …“ eintragen (oder „Vormonat“; „Noch nicht abgerechnet“ = alles Offene), wahlweise nach
Aktivierungsdatum oder Abrechnungszeitraum des Anbieters. Exportierte Positionen werden als *abgerechnet* Aktivierungsdatum oder Abrechnungszeitraum des Anbieters. Exportierte Positionen werden als *abgerechnet*
gekennzeichnet (grau) und nie wieder exportiert – auch nicht, wenn dieselbe Position später in einer gekennzeichnet (grau) und nie wieder exportiert – auch nicht, wenn dieselbe Position später in einer
anderen Datei oder Abrechnung auftaucht. Versehentlich? Rechtsklick ▸ *Kennzeichnung „abgerechnet“ aufheben*. anderen Datei oder Abrechnung auftaucht. Versehentlich? Rechtsklick ▸ *Kennzeichnung „abgerechnet“ aufheben*.
@@ -134,7 +144,9 @@ Gitea Actions (`.gitea/workflows/windows-build.yml`) baut die `.exe` im Containe
- jeder Push: alle Tests unter Windows-Python (Wine) **inklusive Oberflächen-Tests** auf einem - jeder Push: alle Tests unter Windows-Python (Wine) **inklusive Oberflächen-Tests** auf einem
virtuellen Bildschirm, danach `Provisionsmanager95.exe` bauen und mit virtuellen Bildschirm, danach `Provisionsmanager95.exe` bauen und mit
`Provisionsmanager95.exe --selbsttest` die **fertige .exe** prüfen; erst dann gibt es das Artefakt `Provisionsmanager95.exe --selbsttest` die **fertige .exe** prüfen; erst dann gibt es das Artefakt
- Tag `vX.Y.Z` (passend zu `provtool/__init__.py`): zusätzlich ein Gitea-Release mit `.exe` und `.zip` - danach das Setup (`build/installer.iss`, Inno Setup unter Wine) bauen, still installieren,
den Selbsttest in der installierten `.exe` laufen lassen und still deinstallieren
- Tag `vX.Y.Z` (passend zu `provtool/__init__.py`): zusätzlich ein Gitea-Release mit `.exe`, `.zip` und Setup
Die Oberflächen-Tests (`tests/test_gui.py`) bedienen die echte Oberfläche: Dialoge öffnen, Die Oberflächen-Tests (`tests/test_gui.py`) bedienen die echte Oberfläche: Dialoge öffnen,
Programm schließen, während Assistent/Rückfragen/Rundgang offen sind, Alt+F4, Beträge ändern usw. Programm schließen, während Assistent/Rückfragen/Rundgang offen sind, Alt+F4, Beträge ändern usw.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 24 KiB

+40 -4
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Baut die Windows-.exe mit Windows-Python unter Wine (z. B. im Container tobix/pywine:3.12). # Baut die Windows-.exe mit Windows-Python unter Wine (z. B. im Container tobix/pywine:3.12).
# Ergebnis: dist/Provisionsmanager95.exe und dist/Provisionsmanager95-<version>.zip # Ergebnis: dist/Provisionsmanager95.exe, dist/Provisionsmanager95-<version>.zip und
# dist/Provisionsmanager-Setup-<version>.exe (Inno Setup, Installation pro Benutzer)
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
@@ -40,19 +41,24 @@ echo "== PyInstaller =="
$PY -m PyInstaller --noconfirm --clean --distpath dist --workpath build/tmp build/provisionsmanager.spec $PY -m PyInstaller --noconfirm --clean --distpath dist --workpath build/tmp build/provisionsmanager.spec
test -f dist/Provisionsmanager95.exe test -f dist/Provisionsmanager95.exe
test -f dist/Provisionsmanager/Provisionsmanager.exe # Ordner-Variante für das Setup
echo "== Selbsttest der fertigen .exe ==" selbsttest() { # $1 = zu prüfende .exe
rm -f selbsttest.txt rm -f selbsttest.txt
set +e set +e
timeout 300 wine dist/Provisionsmanager95.exe --selbsttest selbsttest.txt timeout 300 wine "$1" --selbsttest selbsttest.txt
CODE=$? CODE=$?
set -e set -e
cat selbsttest.txt 2>/dev/null || echo "(keine Ergebnisdatei – die .exe ist vermutlich gar nicht gestartet)" cat selbsttest.txt 2>/dev/null || echo "(keine Ergebnisdatei – die .exe ist vermutlich gar nicht gestartet)"
if [ "$CODE" -ne 0 ] || ! head -1 selbsttest.txt 2>/dev/null | tr -d '\r' | grep -qx OK; then if [ "$CODE" -ne 0 ] || ! head -1 selbsttest.txt 2>/dev/null | tr -d '\r' | grep -qx OK; then
echo "Selbsttest der .exe FEHLGESCHLAGEN (Exit-Code $CODE)" echo "Selbsttest von $1 FEHLGESCHLAGEN (Exit-Code $CODE)"
exit 1 exit 1
fi fi
echo "Selbsttest bestanden" echo "Selbsttest bestanden"
}
echo "== Selbsttest der fertigen .exe =="
selbsttest dist/Provisionsmanager95.exe
cp README.md dist/LIESMICH.md 2>/dev/null || true cp README.md dist/LIESMICH.md 2>/dev/null || true
( cd dist && rm -f Provisionsmanager95-*.zip && \ ( cd dist && rm -f Provisionsmanager95-*.zip && \
python3 -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); [z.write(f) for f in sys.argv[2:]]; z.close()" \ python3 -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); [z.write(f) for f in sys.argv[2:]]; z.close()" \
@@ -60,4 +66,34 @@ cp README.md dist/LIESMICH.md 2>/dev/null || true
|| $PY -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); [z.write(f) for f in sys.argv[2:]]; z.close()" \ || $PY -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); [z.write(f) for f in sys.argv[2:]]; z.close()" \
"Provisionsmanager95-${VERSION}.zip" Provisionsmanager95.exe LIESMICH.md ) "Provisionsmanager95-${VERSION}.zip" Provisionsmanager95.exe LIESMICH.md )
echo "$VERSION" > dist/VERSION echo "$VERSION" > dist/VERSION
echo "== Setup (Inno Setup) =="
INNO_VERSION=6.7.3
INNO_SHA256=9c73c3bae7ed48d44112a0f48e66742c00090bdb5bef71d9d3c056c66e97b732
INNO_EXE=build/tmp/innosetup-$INNO_VERSION.exe
mkdir -p build/tmp
curl -fsSL -o "$INNO_EXE" "https://github.com/jrsoftware/issrc/releases/download/is-${INNO_VERSION//./_}/innosetup-$INNO_VERSION.exe"
echo "$INNO_SHA256 $INNO_EXE" | sha256sum -c -
timeout 300 wine "$INNO_EXE" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /CURRENTUSER '/DIR=C:\InnoSetup'
rm -f dist/Provisionsmanager-Setup-*.exe
timeout 300 wine 'C:\InnoSetup\ISCC.exe' /Q "/DAppVersion=$VERSION" 'build\installer.iss'
SETUP="dist/Provisionsmanager-Setup-$VERSION.exe"
test -f "$SETUP"
echo "== Installer-Test (still installieren, Selbsttest, still deinstallieren) =="
winordner() { winepath -u "$(wine cmd /c echo "%$1%" | tr -d '\r')"; } # z. B. APPDATA -> /opt/wineprefix/...
DATEN="$(winordner APPDATA)/Provisionsmanager95"
ZIEL="$(winordner LOCALAPPDATA)/Programs/Provisionsmanager"
mkdir -p "$DATEN"
echo "Benutzerdaten" > "$DATEN/installer-test.txt"
timeout 300 wine "$SETUP" /VERYSILENT /SUPPRESSMSGBOXES /CURRENTUSER /NORESTART
test -f "$(winordner APPDATA)/Microsoft/Windows/Start Menu/Programs/Provisions-Manager.lnk"
selbsttest "$ZIEL/Provisionsmanager.exe"
timeout 300 wine "$ZIEL/unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
# Der Deinstaller arbeitet als Kopie im Temp-Ordner weiter – auf das Ende warten
for _ in $(seq 60); do [ -e "$ZIEL" ] || break; sleep 1; done
if [ -e "$ZIEL" ]; then echo "Deinstallation FEHLGESCHLAGEN – Programmordner noch da:"; ls -la "$ZIEL"; exit 1; fi
if [ ! -f "$DATEN/installer-test.txt" ]; then echo "Deinstallation hat Benutzerdaten gelöscht!"; exit 1; fi
rm "$DATEN/installer-test.txt"
echo "Installer-Test bestanden"
ls -la dist ls -la dist
+45
View File
@@ -0,0 +1,45 @@
; Inno-Setup-Skript: Installation pro Benutzer (ohne Admin-Rechte) aus dist/Provisionsmanager/
; Aufruf (siehe build/build_windows.sh): ISCC.exe /DAppVersion=1.2.3 build\installer.iss
; Ergebnis: dist/Provisionsmanager-Setup-<version>.exe
#ifndef AppVersion
#error AppVersion fehlt – ISCC mit /DAppVersion=<version> aufrufen
#endif
[Setup]
; AppId nie ändern: daran erkennt Windows bei Updates die bestehende Installation
AppId={{79CF85D1-16DC-4FD5-924B-B6C9AFB90309}
AppName=Provisions-Manager
AppVersion={#AppVersion}
AppPublisher=Handy-Insel Miesbach
PrivilegesRequired=lowest
DefaultDirName={localappdata}\Programs\Provisionsmanager
DefaultGroupName=Provisions-Manager
DisableProgramGroupPage=yes
UninstallDisplayIcon={app}\Provisionsmanager.exe
SetupIconFile=..\assets\app.ico
OutputDir=..\dist
OutputBaseFilename=Provisionsmanager-Setup-{#AppVersion}
CloseApplications=yes
WizardStyle=modern
; Benutzerdaten liegen in %APPDATA%\Provisionsmanager95 – das Setup fasst sie nicht an,
; der Deinstaller entfernt nur, was er selbst installiert hat.
[Languages]
Name: "de"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[InstallDelete]
; Update: Bibliotheken der alten Version entfernen, damit keine veralteten Dateien liegen bleiben
Type: filesandordirs; Name: "{app}\_internal"
[Files]
Source: "..\dist\Provisionsmanager\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{autoprograms}\Provisions-Manager"; Filename: "{app}\Provisionsmanager.exe"
Name: "{autodesktop}\Provisions-Manager"; Filename: "{app}\Provisionsmanager.exe"; Tasks: desktopicon
[Run]
Filename: "{app}\Provisionsmanager.exe"; Description: "{cm:LaunchProgram,Provisions-Manager}"; Flags: nowait postinstall skipifsilent
-20
View File
@@ -1,20 +0,0 @@
"""Erzeugt assets/app.ico aus der Pixel-Grafik in provtool/gui/win95.py (benötigt Pillow, nur zum Entwickeln)."""
import sys
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from provtool.gui.win95 import PALETTE, _icon_rows # noqa: E402
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
for y, row in enumerate(_icon_rows("app")):
for x, ch in enumerate(row):
if ch != ".":
img.putpixel((x, y), tuple(int(PALETTE[ch][i:i + 2], 16) for i in (1, 3, 5)) + (255,))
groessen = [16, 32, 48, 64, 128, 256]
big = img.resize((256, 256), Image.NEAREST)
big.save(ROOT / "assets" / "app.ico", sizes=[(s, s) for s in groessen])
big.save(ROOT / "assets" / "app.png")
print("assets/app.ico geschrieben")
+117
View File
@@ -0,0 +1,117 @@
"""Zeichnet die modernen Symbole (flach, wie Windows 11) und das Programm-Icon.
Ausgabe: provtool/gui/bilder/modern/<name>.png (20 px) und <name>_gross.png (40 px),
assets/app.ico und assets/app.png. Benötigt Pillow – nur zum Entwickeln, nicht zur Laufzeit.
"""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
ZIEL = ROOT / "provtool" / "gui" / "bilder" / "modern"
N = 160 # Zeichenfläche (8-fach), danach weich verkleinert
LINIE, AKZENT, HELL = (66, 66, 66, 255), (0, 95, 184, 255), (255, 255, 255, 255)
GELB, ROT, ORANGE = (240, 190, 70, 255), (196, 43, 28, 255), (247, 168, 0, 255)
S = 9 # Strichstärke auf der Zeichenfläche
SCHRIFT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
def leinwand():
img = Image.new("RGBA", (N, N), (0, 0, 0, 0))
return img, ImageDraw.Draw(img)
def zeichen(d, text, farbe, groesse=104):
f = ImageFont.truetype(SCHRIFT, groesse)
d.text((N / 2, N / 2 + 4), text, font=f, fill=farbe, anchor="mm")
def prozent(d, farbe, s=S):
d.ellipse((28, 28, 68, 68), outline=farbe, width=s)
d.ellipse((92, 92, 132, 132), outline=farbe, width=s)
d.line((120, 30, 40, 130), fill=farbe, width=s + 2)
def neu(d):
d.polygon([(34, 14), (96, 14), (126, 44), (126, 146), (34, 146)], outline=LINIE, width=S, fill=HELL)
d.line([(96, 14), (96, 44), (126, 44)], fill=LINIE, width=S)
d.ellipse((86, 86, 150, 150), fill=AKZENT)
d.line((118, 100, 118, 136), fill=HELL, width=S)
d.line((100, 118, 136, 118), fill=HELL, width=S)
def oeffnen(d):
d.polygon([(14, 34), (62, 34), (76, 50), (146, 50), (146, 132), (14, 132)], fill=GELB, outline=(193, 140, 30, 255),
width=S - 2)
d.rectangle((14, 66, 146, 132), fill=(250, 210, 100, 255), outline=(193, 140, 30, 255), width=S - 2)
def ablage(d, hoch: bool):
d.line([(18, 96), (18, 140), (142, 140), (142, 96)], fill=LINIE, width=S, joint="curve")
oben, unten = (14, 104) if hoch else (14, 104)
d.line((80, oben, 80, unten), fill=AKZENT, width=S + 2)
if hoch:
d.polygon([(80, 8), (52, 42), (108, 42)], fill=AKZENT)
else:
d.polygon([(80, 118), (52, 82), (108, 82)], fill=AKZENT)
def haendler(d):
d.ellipse((24, 26, 74, 76), outline=LINIE, width=S)
d.arc((8, 82, 90, 170), 180, 360, fill=LINIE, width=S)
d.ellipse((88, 34, 132, 78), fill=AKZENT)
d.pieslice((74, 86, 152, 166), 180, 360, fill=AKZENT)
def rechner(d): # Stift: Betrag ändern
d.polygon([(110, 18), (142, 50), (58, 134), (26, 102)], fill=HELL, outline=LINIE, width=S)
d.polygon([(26, 102), (58, 134), (16, 144)], fill=AKZENT)
d.line((98, 30, 130, 62), fill=LINIE, width=S)
def birne(d):
d.ellipse((34, 10, 126, 102), fill=GELB, outline=(193, 140, 30, 255), width=S - 2)
d.rectangle((56, 96, 104, 122), fill=GELB)
d.rectangle((56, 122, 104, 150), fill=(120, 120, 120, 255))
d.line((60, 134, 100, 134), fill=HELL, width=4)
def kreis(d, farbe, text):
d.ellipse((6, 6, 154, 154), fill=farbe)
zeichen(d, text, HELL)
def warnung(d):
d.polygon([(80, 8), (156, 148), (4, 148)], fill=ORANGE)
zeichen(d, "!", (30, 30, 30, 255), 92)
def app(d):
d.rounded_rectangle((4, 4, 156, 156), radius=34, fill=AKZENT)
prozent(d, HELL, 13)
ICONS = {
"neu": neu, "oeffnen": oeffnen, "import": lambda d: ablage(d, False), "export": lambda d: ablage(d, True),
"haendler": haendler, "prozent": lambda d: prozent(d, AKZENT), "rechner": rechner, "birne": birne,
"info": lambda d: kreis(d, AKZENT, "i"), "frage": lambda d: kreis(d, AKZENT, "?"),
"fehler": lambda d: kreis(d, ROT, "×"), "warnung": warnung, "app": app,
}
def main():
ZIEL.mkdir(parents=True, exist_ok=True)
for name, zeichne in ICONS.items():
img, d = leinwand()
zeichne(d)
img.resize((20, 20), Image.LANCZOS).save(ZIEL / f"{name}.png")
img.resize((40, 40), Image.LANCZOS).save(ZIEL / f"{name}_gross.png")
if name == "app":
gross = img.resize((256, 256), Image.LANCZOS)
gross.save(ROOT / "assets" / "app.png")
gross.save(ROOT / "assets" / "app.ico", sizes=[(s, s) for s in (16, 24, 32, 48, 64, 128, 256)])
print(f"{len(ICONS)} Symbole nach {ZIEL} geschrieben, assets/app.ico + app.png erneuert")
if __name__ == "__main__":
main()
+18 -1
View File
@@ -1,4 +1,5 @@
# PyInstaller-Spezifikation – erzeugt eine einzelne Programmdatei (Windows: .exe, Linux: ELF) # PyInstaller-Spezifikation – erzeugt eine einzelne Programmdatei (Windows: .exe, Linux: ELF),
# unter Windows zusätzlich den Ordner dist/Provisionsmanager/ (schneller Start, Inhalt des Setups)
# Aufruf: pyinstaller --noconfirm build/provisionsmanager.spec # Aufruf: pyinstaller --noconfirm build/provisionsmanager.spec
import os import os
import sys import sys
@@ -31,3 +32,19 @@ exe = EXE(
strip=False, strip=False,
runtime_tmpdir=None, runtime_tmpdir=None,
) )
if WIN:
exe_ordner = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="Provisionsmanager",
icon=os.path.join(ROOT, "assets", "app.ico"),
version=os.path.join(ROOT, "build", "version_info.txt"),
console=False,
upx=False,
debug=False,
strip=False,
)
COLLECT(exe_ordner, a.binaries, a.datas, name="Provisionsmanager", upx=False, strip=False)
+1 -1
View File
@@ -11,7 +11,7 @@ release_id() { curl -sf "$API/releases/tags/$TAG" -H "Authorization: token $TOKE
id=$(release_id || true) id=$(release_id || true)
if [ -z "$id" ]; then if [ -z "$id" ]; then
curl -sf -X POST "$API/releases" -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ curl -sf -X POST "$API/releases" -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"Provisions-Manager 95 $TAG\",\"body\":\"Windows: .exe herunterladen und starten. Linux: tar.gz entpacken, Provisionsmanager95 starten (ab Debian 12 / Ubuntu 24.04).\"}" \ -d "{\"tag_name\":\"$TAG\",\"name\":\"Provisions-Manager 95 $TAG\",\"body\":\"Windows: Setup installieren (oder die .exe direkt starten). Linux: tar.gz entpacken, Provisionsmanager95 starten (ab Debian 12 / Ubuntu 24.04).\"}" \
>/dev/null || sleep 5 # Gleichzeitig angelegt? Dann gibt es das Release jetzt. >/dev/null || sleep 5 # Gleichzeitig angelegt? Dann gibt es das Release jetzt.
id=$(release_id) id=$(release_id)
fi fi
+1 -1
View File
@@ -1,5 +1,5 @@
"""Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach.""" """Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach."""
__version__ = "1.4.0" __version__ = "1.5.0"
APP_NAME = "Provisions-Manager 95" APP_NAME = "Provisions-Manager 95"
APP_ORG = "Handy-Insel Miesbach" APP_ORG = "Handy-Insel Miesbach"
+60 -13
View File
@@ -12,7 +12,7 @@ from datetime import date
from pathlib import Path from pathlib import Path
from tkinter import filedialog from tkinter import filedialog
from provtool import APP_NAME, config from provtool import config
from provtool import export as exp from provtool import export as exp
from provtool import regeln from provtool import regeln
from provtool.db import EIGEN, UNZUGEORDNET, AbrechnungErledigt, Datenbank from provtool.db import EIGEN, UNZUGEORDNET, AbrechnungErledigt, Datenbank
@@ -79,8 +79,8 @@ class App:
root.report_callback_exception = self._fehler_handler root.report_callback_exception = self._fehler_handler
klassisch = w.DESIGN == "win95" # Win95: eigene Titelleiste; modern: normaler Windows-Rahmen klassisch = w.DESIGN == "win95" # Win95: eigene Titelleiste; modern: normaler Windows-Rahmen
w.KLASSISCH = klassisch w.KLASSISCH = klassisch
self.rahmen = rh.Hauptrahmen(root, APP_NAME, self.icons("app"), self.beenden, klassisch=klassisch) self.rahmen = rh.Hauptrahmen(root, w.APP_NAME, self.icons("app"), self.beenden, klassisch=klassisch)
self.rahmen.set_titel(APP_NAME) self.rahmen.set_titel(w.APP_NAME)
self.inhalt = self.rahmen.inhalt self.inhalt = self.rahmen.inhalt
self._menue() self._menue()
@@ -151,7 +151,7 @@ class App:
hi.add_separator() hi.add_separator()
hi.add_command(label="Einrichtungs-Assistent...", command=self.assistent) hi.add_command(label="Einrichtungs-Assistent...", command=self.assistent)
hi.add_separator() hi.add_separator()
hi.add_command(label=f"Info über {APP_NAME}...", command=lambda: dlg.InfoDialog(self.root).zeigen()) hi.add_command(label=f"Info über {w.APP_NAME}...", command=lambda: dlg.InfoDialog(self.root).zeigen())
def _toolbar(self): def _toolbar(self):
tb = tk.Frame(self.inhalt, bg=w.FACE, bd=0 if w.MODERN else 1, relief="raised") tb = tk.Frame(self.inhalt, bg=w.FACE, bd=0 if w.MODERN else 1, relief="raised")
@@ -195,8 +195,23 @@ class App:
bd=0, opaqueresize=False, sashcursor="sb_h_double_arrow") bd=0, opaqueresize=False, sashcursor="sb_h_double_arrow")
pw.pack(fill="both", expand=True, padx=6, pady=6) pw.pack(fill="both", expand=True, padx=6, pady=6)
links = tk.Frame(pw, bg=w.FACE)
# --- Abrechnungen (ein Klick öffnet)
self.f_a = w.Fenster(links, "Abrechnungen", self.icons("oeffnen"))
self.f_a.pack(fill="x", pady=(0, 6))
r, self.atree = w.scroll_tree(self.f_a.inhalt, columns=("status",), show="tree headings", height=5,
selectmode="browse")
self.atree.heading("#0", text="Abrechnung", anchor="w")
self.atree.heading("status", text="Status")
self.atree.column("#0", width=170, minwidth=80, stretch=False)
self.atree.column("status", width=90, anchor="w", stretch=False)
w.auto_breite(self.atree, {"#0": 1})
r.pack(fill="both", expand=True)
self.atree.bind("<<TreeviewSelect>>", self._abrechnung_gewaehlt)
# --- Händler-Fenster # --- Händler-Fenster
self.f_h = w.Fenster(pw, "Händler", self.icons("haendler")) self.f_h = w.Fenster(links, "Händler", self.icons("haendler"))
self.f_h.pack(fill="both", expand=True)
r, self.htree = w.scroll_tree(self.f_h.inhalt, columns=("anzahl", "summe"), show="tree headings", r, self.htree = w.scroll_tree(self.f_h.inhalt, columns=("anzahl", "summe"), show="tree headings",
selectmode="browse") selectmode="browse")
self.htree.heading("#0", text="Händler", anchor="w") self.htree.heading("#0", text="Händler", anchor="w")
@@ -211,7 +226,7 @@ class App:
r.pack(fill="both", expand=True) r.pack(fill="both", expand=True)
self.htree.bind("<<TreeviewSelect>>", self._haendler_gewaehlt) self.htree.bind("<<TreeviewSelect>>", self._haendler_gewaehlt)
self.htree.bind("<Double-1>", lambda e: self._haendler_doppelklick()) self.htree.bind("<Double-1>", lambda e: self._haendler_doppelklick())
pw.add(self.f_h, minsize=240, width=310) pw.add(links, minsize=240, width=310)
# --- Positionen-Fenster # --- Positionen-Fenster
self.f_p = w.Fenster(pw, "Positionen", self.icons("neu")) self.f_p = w.Fenster(pw, "Positionen", self.icons("neu"))
@@ -277,16 +292,33 @@ class App:
if self.aid and self.db.abrechnung(self.aid): if self.aid and self.db.abrechnung(self.aid):
self.positionen = self.db.positionen(self.aid) self.positionen = self.db.positionen(self.aid)
titel = self.db.abrechnung(self.aid)["titel"] + (" – erledigt" if self.db.erledigt(self.aid) else "") titel = self.db.abrechnung(self.aid)["titel"] + (" – erledigt" if self.db.erledigt(self.aid) else "")
self.rahmen.set_titel(f"{APP_NAME} - [{titel}]") self.rahmen.set_titel(f"{w.APP_NAME} - [{titel}]")
self.status.set("info", f" Abrechnung: {titel}") self.status.set("info", f" Abrechnung: {titel}")
else: else:
self.aid = None self.aid = None
self.positionen = [] self.positionen = []
self.rahmen.set_titel(APP_NAME) self.rahmen.set_titel(w.APP_NAME)
self.status.set("info", " Keine Abrechnung geöffnet – Datei > Neue Abrechnung oder Import") self.status.set("info", " Keine Abrechnung geöffnet – Datei > Neue Abrechnung oder Import")
self._abrechnungsliste()
self._haendlerliste(behalte_auswahl) self._haendlerliste(behalte_auswahl)
self._tabelle() self._tabelle()
def _abrechnungsliste(self):
t = self.atree
t.delete(*t.get_children())
for a in self.db.abrechnungen():
status = "erledigt" if a["erledigt"] else (f"{a['abgerechnet']} abgerechnet" if a["abgerechnet"] else "offen")
t.insert("", "end", iid=str(a["id"]), text=a["titel"], values=(status,))
if self.aid and t.exists(str(self.aid)):
t.selection_set(str(self.aid))
t.see(str(self.aid))
def _abrechnung_gewaehlt(self, _e=None):
sel = self.atree.selection()
if sel and int(sel[0]) != self.aid:
self.ansicht = ALLE
self.oeffnen(int(sel[0]))
def _suche_planen(self): def _suche_planen(self):
"""Suche erst kurz nach dem letzten Tastendruck ausführen (flüssiges Tippen).""" """Suche erst kurz nach dem letzten Tastendruck ausführen (flüssiges Tippen)."""
if self._suche_geplant is not None: if self._suche_geplant is not None:
@@ -466,7 +498,7 @@ class App:
else: else:
self._aktualisieren() self._aktualisieren()
def importieren(self, dateien: list[str] | None = None): def importieren(self, dateien: list[str] | None = None, ziel: int | None = None):
if not dateien: if not dateien:
dateien = list(filedialog.askopenfilenames( dateien = list(filedialog.askopenfilenames(
parent=self.root, title="Provisionsabrechnungen importieren", parent=self.root, title="Provisionsabrechnungen importieren",
@@ -477,7 +509,22 @@ class App:
if not dateien: if not dateien:
return return
self.settings["import_ordner"] = str(Path(dateien[0]).parent) self.settings["import_ordner"] = str(Path(dateien[0]).parent)
if not self._brauche_abrechnung() or self._gesperrt(): if ziel is None: # jedes Mal fragen, in welche Abrechnung
if not self.db.abrechnungen():
if not self.neu():
return
else:
res = dlg.AbrechnungenDialog(self.root, self.db, self.aid, fuer_import=True).zeigen()
if not res:
return
if res[0] == "neu":
if not self.neu():
return
else:
self.aid, self.ansicht = res[1], ALLE
else:
self.aid = ziel
if self._gesperrt():
return return
bericht = [] bericht = []
self.root.configure(cursor="watch") self.root.configure(cursor="watch")
@@ -1018,7 +1065,7 @@ class App:
self.ansicht = ALLE self.ansicht = ALLE
self._aktualisieren() self._aktualisieren()
if res["dateien"]: if res["dateien"]:
self.importieren(res["dateien"]) self.importieren(res["dateien"], ziel=self.aid)
if res["rundgang"]: if res["rundgang"]:
self.root.after(200, self.rundgang) self.root.after(200, self.rundgang)
@@ -1061,7 +1108,7 @@ class App:
"Rufnummer, Name oder Tarif eingeben – die Liste filtert sofort. Tastenkürzel: Strg+F."), "Rufnummer, Name oder Tarif eingeben – die Liste filtert sofort. Tastenkürzel: Strg+F."),
(self.menueleiste.knoepfe["?"], "Das war's!", (self.menueleiste.knoepfe["?"], "Das war's!",
"Diesen Rundgang, den Einrichtungs-Assistenten und die Tipps finden Sie jederzeit hier im Menü „?“.\n\n" "Diesen Rundgang, den Einrichtungs-Assistenten und die Tipps finden Sie jederzeit hier im Menü „?“.\n\n"
"Viel Erfolg mit dem Provisions-Manager 95!"), f"Viel Erfolg mit dem {w.APP_NAME}!"),
] ]
self._rundgang = ass.Rundgang(self, schritte) self._rundgang = ass.Rundgang(self, schritte)
@@ -1073,7 +1120,7 @@ class App:
text = "".join(traceback.format_exception(exc, val, tb)) text = "".join(traceback.format_exception(exc, val, tb))
try: try:
with (config.app_dir() / "fehler.log").open("a", encoding="utf-8") as f: with (config.app_dir() / "fehler.log").open("a", encoding="utf-8") as f:
f.write(f"--- {date.today().isoformat()} {APP_NAME}\n{text}\n") f.write(f"--- {date.today().isoformat()} {w.APP_NAME}\n{text}\n")
except OSError: except OSError:
pass pass
# Beim/nach dem Beenden keine Meldung mehr zeigen – das Fenster gibt es dann nicht mehr # Beim/nach dem Beenden keine Meldung mehr zeigen – das Fenster gibt es dann nicht mehr
+10 -10
View File
@@ -7,7 +7,6 @@ from collections import Counter, defaultdict
from pathlib import Path from pathlib import Path
from tkinter import filedialog, ttk from tkinter import filedialog, ttk
from provtool import APP_NAME
from provtool.db import CAPS_ANBIETER, CAPS_WERT, Haendler, ist_grossgeschrieben from provtool.db import CAPS_ANBIETER, CAPS_WERT, Haendler, ist_grossgeschrieben
from provtool.gui import win32 from provtool.gui import win32
from provtool.gui import win95 as w from provtool.gui import win95 as w
@@ -33,7 +32,7 @@ class Einrichtungsassistent(w.Dialog):
BREITE, HOEHE = 470, 340 BREITE, HOEHE = 470, 340
def __init__(self, app): def __init__(self, app):
super().__init__(app.root, "Provisions-Manager 95 – Einrichtung") super().__init__(app.root, f"{w.APP_NAME} – Einrichtung")
self.app = app self.app = app
self.dateien: list[str] = [] self.dateien: list[str] = []
self.vorschlaege: dict[str, list[str]] = {"freenet": [], "vodafone": []} self.vorschlaege: dict[str, list[str]] = {"freenet": [], "vodafone": []}
@@ -77,7 +76,7 @@ class Einrichtungsassistent(w.Dialog):
for i, (x, y) in enumerate(((20, 30), (120, 55), (35, 120), (128, 150), (60, 20))): for i, (x, y) in enumerate(((20, 30), (120, 55), (35, 120), (128, 150), (60, 20))):
c.create_rectangle(x, y, x + 2 + i % 2, y + 2 + i % 2, fill=w.WHITE, outline="") c.create_rectangle(x, y, x + 2 + i % 2, y + 2 + i % 2, fill=w.WHITE, outline="")
c.create_image(77, self.HOEHE // 2 - 10, image=self._img) c.create_image(77, self.HOEHE // 2 - 10, image=self._img)
c.create_text(77, self.HOEHE - 38, text="Provisions-\nManager 95", fill=w.WHITE, font=w.FONT_BOLD, c.create_text(77, self.HOEHE - 38, text=w.APP_NAME.replace("-", "-\n", 1), fill=w.WHITE, font=w.FONT_BOLD,
justify="center") justify="center")
def _leeren(self): def _leeren(self):
@@ -100,7 +99,7 @@ class Einrichtungsassistent(w.Dialog):
self.b_zurueck.configure(state="normal" if nr > 0 else "disabled") self.b_zurueck.configure(state="normal" if nr > 0 else "disabled")
self.b_weiter.configure(text="Fertig stellen" if nr == len(self.seiten) - 1 else "Weiter >") self.b_weiter.configure(text="Fertig stellen" if nr == len(self.seiten) - 1 else "Weiter >")
if self.titelleiste: if self.titelleiste:
self.titelleiste.set_titel(f"Provisions-Manager 95 – Einrichtung (Schritt {nr + 1} von {len(self.seiten)})") self.titelleiste.set_titel(f"{w.APP_NAME} – Einrichtung (Schritt {nr + 1} von {len(self.seiten)})")
def _weiter(self): def _weiter(self):
pruefer = getattr(self, f"_pruefe_{self.nr}", None) pruefer = getattr(self, f"_pruefe_{self.nr}", None)
@@ -128,7 +127,7 @@ class Einrichtungsassistent(w.Dialog):
# ---------------------------------------------------------------- Seite 1 # ---------------------------------------------------------------- Seite 1
def _s_willkommen(self): def _s_willkommen(self):
self._ueberschrift(f"Willkommen beim {APP_NAME}!") self._ueberschrift(f"Willkommen beim {w.APP_NAME}!")
self._text("Dieser Assistent richtet das Programm in wenigen Schritten ein:") self._text("Dieser Assistent richtet das Programm in wenigen Schritten ein:")
for punkt in ("Speicherort der Daten festlegen", for punkt in ("Speicherort der Daten festlegen",
"Unterhändler mit ihren Kennungen anlegen", "Unterhändler mit ihren Kennungen anlegen",
@@ -384,7 +383,7 @@ class Einrichtungsassistent(w.Dialog):
def _s_fertig(self): def _s_fertig(self):
self._ueberschrift("Fertig!") self._ueberschrift("Fertig!")
n = len(self.app.db.haendler_liste()) n = len(self.app.db.haendler_liste())
self._text(f"Der {APP_NAME} ist eingerichtet.") self._text(f"Der {w.APP_NAME} ist eingerichtet.")
self._text(f" - {n} Händler angelegt") self._text(f" - {n} Händler angelegt")
if self.v_abrechnung.get(): if self.v_abrechnung.get():
self._text(f" - Abrechnung „{self.v_titel.get().strip()}“" self._text(f" - Abrechnung „{self.v_titel.get().strip()}“"
@@ -582,13 +581,14 @@ class TippDesTages(w.Dialog):
haupt.pack(padx=12, pady=12) haupt.pack(padx=12, pady=12)
box = tk.Frame(haupt, bg=w.WHITE, bd=2, relief="sunken") box = tk.Frame(haupt, bg=w.WHITE, bd=2, relief="sunken")
box.pack(side="left", fill="both") box.pack(side="left", fill="both")
self._bild = tipp_bild(self) # Büroklammer nur im Win95-Design; modern eine schlichte Glühbirne
self.bild_ok = self._bild is not None self._bild = None if w.MODERN else tipp_bild(self)
if self._bild is None: # Ersatz, falls das Bild fehlt self.bild_ok = self._bild is not None or w.MODERN
if self._bild is None:
self._bild = w.make_icon(self, "birne", 3) self._bild = w.make_icon(self, "birne", 3)
tk.Label(box, image=self._bild, bg=w.WHITE).pack(side="left", anchor="n", padx=(12, 4), pady=10) tk.Label(box, image=self._bild, bg=w.WHITE).pack(side="left", anchor="n", padx=(12, 4), pady=10)
# fester Textbereich: der Tipp bricht darin um und kann nicht mehr abgeschnitten werden # fester Textbereich: der Tipp bricht darin um und kann nicht mehr abgeschnitten werden
rechts = tk.Frame(box, bg=w.WHITE, padx=12, pady=10, width=380, height=self._bild.height() + 20) rechts = tk.Frame(box, bg=w.WHITE, padx=12, pady=10, width=380, height=max(self._bild.height(), 170) + 20)
rechts.pack(side="left", fill="both") rechts.pack(side="left", fill="both")
rechts.pack_propagate(False) rechts.pack_propagate(False)
tk.Label(rechts, text="Wussten Sie schon...", bg=w.WHITE, font=(w.FONT_FAMILY, w.FONT_SIZE + 6, "bold"), tk.Label(rechts, text="Wussten Sie schon...", bg=w.WHITE, font=(w.FONT_FAMILY, w.FONT_SIZE + 6, "bold"),
Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+21 -7
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import tkinter as tk import tkinter as tk
from tkinter import filedialog, ttk from tkinter import filedialog, ttk
from provtool import APP_NAME, APP_ORG, __version__ from provtool import APP_ORG, __version__
from provtool import regeln from provtool import regeln
from provtool import zeitraum as zr from provtool import zeitraum as zr
from provtool.db import CAPS_ANBIETER, CAPS_WERT, AbrechnungErledigt, Datenbank, Haendler from provtool.db import CAPS_ANBIETER, CAPS_WERT, AbrechnungErledigt, Datenbank, Haendler
@@ -558,10 +558,13 @@ class ProvisionsartenDialog(w.Dialog):
# =========================================================================== Abrechnungen # =========================================================================== Abrechnungen
class AbrechnungenDialog(w.Dialog): class AbrechnungenDialog(w.Dialog):
def __init__(self, master, db: Datenbank, aktuelle_id: int | None): def __init__(self, master, db: Datenbank, aktuelle_id: int | None, fuer_import: bool = False):
super().__init__(master, "Abrechnung öffnen", resizable=True) """fuer_import: Ziel eines Imports wählen (Ergebnis ("oeffnen", id) oder ("neu",))."""
super().__init__(master, "Import – in welche Abrechnung?" if fuer_import else "Abrechnung öffnen",
resizable=True)
self.db = db self.db = db
tk.Label(self, text="Gespeicherte Abrechnungen:", anchor="w").pack(fill="x", padx=10, pady=(10, 2)) tk.Label(self, text="In welche Abrechnung sollen die Dateien importiert werden?" if fuer_import
else "Gespeicherte Abrechnungen:", anchor="w").pack(fill="x", padx=10, pady=(10, 2))
r, self.tree = w.scroll_tree(self, columns=("titel", "status", "geaendert", "anzahl", "dateien"), r, self.tree = w.scroll_tree(self, columns=("titel", "status", "geaendert", "anzahl", "dateien"),
show="headings", height=12, selectmode="browse") show="headings", height=12, selectmode="browse")
for c, t, b in (("titel", "Bezeichnung", 180), ("status", "Status", 110), ("geaendert", "Zuletzt geändert", 120), for c, t, b in (("titel", "Bezeichnung", 180), ("status", "Status", 110), ("geaendert", "Zuletzt geändert", 120),
@@ -572,6 +575,10 @@ class AbrechnungenDialog(w.Dialog):
self.tree.bind("<Double-1>", lambda e: self._oeffnen()) self.tree.bind("<Double-1>", lambda e: self._oeffnen())
k = tk.Frame(self, bg=w.FACE) k = tk.Frame(self, bg=w.FACE)
k.pack(fill="x", padx=10, pady=10) k.pack(fill="x", padx=10, pady=10)
if fuer_import:
w.knopf(k, "Hierhin importieren", self._oeffnen, default="active").pack(side="left")
w.knopf(k, "Neue Abrechnung...", self._neu).pack(side="left", padx=6)
else:
w.knopf(k, "Öffnen", self._oeffnen, breite=10, default="active").pack(side="left") w.knopf(k, "Öffnen", self._oeffnen, breite=10, default="active").pack(side="left")
w.knopf(k, "Umbenennen...", self._umbenennen, breite=12).pack(side="left", padx=6) w.knopf(k, "Umbenennen...", self._umbenennen, breite=12).pack(side="left", padx=6)
w.knopf(k, "Löschen", self._loeschen, breite=10).pack(side="left") w.knopf(k, "Löschen", self._loeschen, breite=10).pack(side="left")
@@ -600,6 +607,10 @@ class AbrechnungenDialog(w.Dialog):
self.result = ("oeffnen", self._sel()) self.result = ("oeffnen", self._sel())
self.destroy() self.destroy()
def _neu(self):
self.result = ("neu",)
self.destroy()
def _umbenennen(self): def _umbenennen(self):
aid = self._sel() aid = self._sel()
if not aid: if not aid:
@@ -917,10 +928,13 @@ class ExportDialog(w.Dialog):
tk.Label(zf, text="bis:").pack(side="left") tk.Label(zf, text="bis:").pack(side="left")
tk.Entry(zf, textvariable=self.v_bis, width=11).pack(side="left", padx=4) tk.Entry(zf, textvariable=self.v_bis, width=11).pack(side="left", padx=4)
w.knopf(zf, "Vormonat", self._vormonat).pack(side="left", padx=(4, 0)) w.knopf(zf, "Vormonat", self._vormonat).pack(side="left", padx=(4, 0))
# ohne Zeitraum = alles, was noch nicht abgerechnet ist (abgerechnete Positionen exportiert er nie)
w.knopf(zf, "Noch nicht abgerechnet", lambda: (self.v_von.set(""), self.v_bis.set(""))).pack(
side="left", padx=(4, 0))
self.v_basis = tk.StringVar(value=zr.BASEN[settings.get("export_basis", zr.AKTIVIERUNG)]) self.v_basis = tk.StringVar(value=zr.BASEN[settings.get("export_basis", zr.AKTIVIERUNG)])
ttk.Combobox(z, textvariable=self.v_basis, values=list(zr.BASEN.values()), state="readonly", ttk.Combobox(z, textvariable=self.v_basis, values=list(zr.BASEN.values()), state="readonly",
width=40).pack(anchor="w", pady=(4, 0)) width=40).pack(anchor="w", pady=(4, 0))
tk.Label(z, text="(leer = alle offenen Positionen; Datum als TT.MM.JJJJ)", fg=w.SHADOW, anchor="w").pack( tk.Label(z, text="(leer bzw. „Noch nicht abgerechnet“ = alle offenen Positionen; Datum als TT.MM.JJJJ)", fg=w.SHADOW, anchor="w").pack(
fill="x") fill="x")
self.v_kennz = tk.BooleanVar(value=True) self.v_kennz = tk.BooleanVar(value=True)
tk.Checkbutton(z, text="Exportierte Positionen als abgerechnet kennzeichnen (werden dann nicht noch " tk.Checkbutton(z, text="Exportierte Positionen als abgerechnet kennzeichnen (werden dann nicht noch "
@@ -1022,7 +1036,7 @@ class EinstellungenDialog(w.Dialog):
class InfoDialog(w.Dialog): class InfoDialog(w.Dialog):
def __init__(self, master): def __init__(self, master):
super().__init__(master, f"Info über {APP_NAME}") super().__init__(master, f"Info über {w.APP_NAME}")
top = tk.Frame(self, bg=w.FACE) top = tk.Frame(self, bg=w.FACE)
top.pack(padx=14, pady=14) top.pack(padx=14, pady=14)
c = tk.Canvas(top, width=110, height=110, bg=w.TEAL, bd=2, relief="sunken", highlightthickness=0) c = tk.Canvas(top, width=110, height=110, bg=w.TEAL, bd=2, relief="sunken", highlightthickness=0)
@@ -1031,7 +1045,7 @@ class InfoDialog(w.Dialog):
c.create_image(57, 57, image=self._img) c.create_image(57, 57, image=self._img)
rechts = tk.Frame(top, bg=w.FACE) rechts = tk.Frame(top, bg=w.FACE)
rechts.pack(side="left", padx=(14, 0), anchor="n") rechts.pack(side="left", padx=(14, 0), anchor="n")
tk.Label(rechts, text=APP_NAME, font=w.FONT_BIG).pack(anchor="w") tk.Label(rechts, text=w.APP_NAME, font=w.FONT_BIG).pack(anchor="w")
tk.Label(rechts, text=f"Version {__version__}", anchor="w").pack(anchor="w") tk.Label(rechts, text=f"Version {__version__}", anchor="w").pack(anchor="w")
w.trenner(rechts, vertikal=False).pack(fill="x", pady=8) w.trenner(rechts, vertikal=False).pack(fill="x", pady=8)
tk.Label(rechts, text=f"Lizenziert für:\n{APP_ORG}\n\nProvisionsabrechnung für Unterhändler\n" tk.Label(rechts, text=f"Lizenziert für:\n{APP_ORG}\n\nProvisionsabrechnung für Unterhändler\n"
+10 -1
View File
@@ -7,6 +7,7 @@ from __future__ import annotations
import sys import sys
import tkinter as tk import tkinter as tk
from pathlib import Path
from tkinter import ttk from tkinter import ttk
FACE = "#c0c0c0" FACE = "#c0c0c0"
@@ -22,6 +23,7 @@ SEL_BG, SEL_FG = NAVY, WHITE # Auswahl/Markierung
RAND = SHADOW # dünne Rahmenlinien (nur modern) RAND = SHADOW # dünne Rahmenlinien (nur modern)
DESIGN = "win95" DESIGN = "win95"
MODERN = False MODERN = False
APP_NAME = "Provisions-Manager 95" # modern: ohne „95“
_PALETTEN = { _PALETTEN = {
"win95": dict(FACE="#c0c0c0", HILITE="#ffffff", LIGHT="#dfdfdf", SHADOW="#808080", DARK="#000000", "win95": dict(FACE="#c0c0c0", HILITE="#ffffff", LIGHT="#dfdfdf", SHADOW="#808080", DARK="#000000",
@@ -43,9 +45,10 @@ FONT_MONO = ("Courier New" if IS_WIN else "DejaVu Sans Mono", FONT_SIZE + 1)
def design_setzen(name: str) -> None: def design_setzen(name: str) -> None:
"""„modern“ oder „win95“ – setzt Farben und Schriften (Feinschliff der Schrift in apply()).""" """„modern“ oder „win95“ – setzt Farben und Schriften (Feinschliff der Schrift in apply())."""
global DESIGN, MODERN, FONT_FAMILY, FONT_SIZE global DESIGN, MODERN, FONT_FAMILY, FONT_SIZE, APP_NAME
DESIGN = name if name in _PALETTEN else "modern" DESIGN = name if name in _PALETTEN else "modern"
MODERN = DESIGN == "modern" MODERN = DESIGN == "modern"
APP_NAME = "Provisions-Manager" if MODERN else "Provisions-Manager 95"
globals().update(_PALETTEN[DESIGN]) globals().update(_PALETTEN[DESIGN])
FONT_FAMILY = ("Segoe UI" if MODERN else "MS Sans Serif") if IS_WIN else "DejaVu Sans" FONT_FAMILY = ("Segoe UI" if MODERN else "MS Sans Serif") if IS_WIN else "DejaVu Sans"
FONT_SIZE = 9 if (MODERN or not IS_WIN) else 8 FONT_SIZE = 9 if (MODERN or not IS_WIN) else 8
@@ -295,7 +298,13 @@ def _icon_rows(name: str) -> list[str]:
return [r.ljust(16, ".")[:16] for r in rows[:16]] + ["." * 16] * (16 - len(rows)) return [r.ljust(16, ".")[:16] for r in rows[:16]] + ["." * 16] * (16 - len(rows))
MODERNE_ICONS = Path(__file__).resolve().parent / "bilder" / "modern"
def make_icon(master, name: str, scale: int = 1) -> tk.PhotoImage: def make_icon(master, name: str, scale: int = 1) -> tk.PhotoImage:
datei = MODERNE_ICONS / f"{name}{'_gross' if scale > 1 else ''}.png"
if MODERN and datei.exists(): # glatte Symbole aus build/make_moderne_icons.py
return tk.PhotoImage(master=master, file=str(datei))
img = tk.PhotoImage(master=master, width=16 * scale, height=16 * scale) img = tk.PhotoImage(master=master, width=16 * scale, height=16 * scale)
for y, row in enumerate(_icon_rows(name)): for y, row in enumerate(_icon_rows(name)):
for x, ch in enumerate(row): for x, ch in enumerate(row):
+46 -1
View File
@@ -1098,11 +1098,18 @@ def test_export_zeitraum_kennzeichnen_und_erledigt(lauf_modern, tmp_path, monkey
d.v_auf.set(False) d.v_auf.set(False)
d._ok() d._ok()
def ok_offen():
d = lauf.dialog(ExportDialog)
d.v_von.set("01.10.2026")
d.v_bis.set("02.10.2026")
next(k for k in _alle(d) if isinstance(k, tk.Button) and k.cget("text") == "Noch nicht abgerechnet").invoke()
ok(d.v_von.get(), d.v_bis.get())
lauf.ablauf([ lauf.ablauf([
(IMMER, app.exportieren, "Export Oktober 6."), (IMMER, app.exportieren, "Export Oktober 6."),
(lambda: lauf.dialog(ExportDialog) is not None, lambda: ok("01.10.2026", "05.10.2026"), "nur bis 5.10."), (lambda: lauf.dialog(ExportDialog) is not None, lambda: ok("01.10.2026", "05.10.2026"), "nur bis 5.10."),
(lambda: lauf.dialog(ExportDialog) is None, app.exportieren, "Export Rest"), (lambda: lauf.dialog(ExportDialog) is None, app.exportieren, "Export Rest"),
(lambda: lauf.dialog(ExportDialog) is not None, ok, "alles Offene"), (lambda: lauf.dialog(ExportDialog) is not None, ok_offen, "Noch nicht abgerechnet"),
(lambda: lauf.dialog(ExportDialog) is None, app.exportieren, "nochmal"), (lambda: lauf.dialog(ExportDialog) is None, app.exportieren, "nochmal"),
]) ])
assert not lauf.uebrig, lauf.diagnose() assert not lauf.uebrig, lauf.diagnose()
@@ -1116,3 +1123,41 @@ def test_export_zeitraum_kennzeichnen_und_erledigt(lauf_modern, tmp_path, monkey
app.prozent() app.prozent()
assert "erledigt" in meldungen[-1] assert "erledigt" in meldungen[-1]
lauf.pruefen() lauf.pruefen()
def _alle(widget):
for kind in widget.winfo_children():
yield kind
yield from _alle(kind)
def test_abrechnungen_links_und_import_fragt_nach_ziel(lauf_modern, home, monkeypatch):
from provtool.gui import win95 as w
from provtool.gui.dialogs import AbrechnungenDialog, ImportBericht
monkeypatch.setattr(w, "frage", lambda *a, **k: True) # „bereits importiert – trotzdem?“ (Testdatei ist schon drin)
lauf = lauf_modern
app = lauf.app
alt = app.aid
neu = app.db.abrechnung_neu("November 2026")
app._aktualisieren()
assert set(app.atree.get_children()) == {str(alt), str(neu)}, "Abrechnungen fehlen links"
app.atree.selection_set(str(neu))
lauf.root.update()
assert app.aid == neu and not app.positionen, "Klick links öffnet die Abrechnung nicht"
app.atree.selection_set(str(alt))
lauf.root.update()
def ziel_waehlen():
d = lauf.dialog(AbrechnungenDialog)
d.tree.selection_set(str(neu))
d._oeffnen()
lauf.ablauf([
(IMMER, lambda: app.importieren([str(home / "freenet.csv")]), "Import"),
(lambda: lauf.dialog(AbrechnungenDialog) is not None, ziel_waehlen, "Ziel: November"),
(lambda: lauf.dialog(ImportBericht) is not None, lambda: lauf.dialog(ImportBericht).abbrechen(), "Bericht zu"),
])
assert not lauf.uebrig, lauf.diagnose()
assert app.aid == neu and len(app.positionen) == 3, "Import nicht in der gewählten Abrechnung"
assert app.atree.selection() == (str(neu),)
lauf.pruefen()