Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
057cca6378 | ||
|
|
ec99042569 | ||
|
|
7b9a2acec0 | ||
|
|
8c86571034 |
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ build/version_info.txt
|
|||||||
selbsttest.txt
|
selbsttest.txt
|
||||||
/*.jpg
|
/*.jpg
|
||||||
/*.png
|
/*.png
|
||||||
|
.claude/
|
||||||
@@ -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.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 24 KiB |
@@ -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
|
||||||
|
|
||||||
|
selbsttest() { # $1 = zu prüfende .exe
|
||||||
|
rm -f selbsttest.txt
|
||||||
|
set +e
|
||||||
|
timeout 300 wine "$1" --selbsttest selbsttest.txt
|
||||||
|
CODE=$?
|
||||||
|
set -e
|
||||||
|
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
|
||||||
|
echo "Selbsttest von $1 FEHLGESCHLAGEN (Exit-Code $CODE)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Selbsttest bestanden"
|
||||||
|
}
|
||||||
|
|
||||||
echo "== Selbsttest der fertigen .exe =="
|
echo "== Selbsttest der fertigen .exe =="
|
||||||
rm -f selbsttest.txt
|
selbsttest dist/Provisionsmanager95.exe
|
||||||
set +e
|
|
||||||
timeout 300 wine dist/Provisionsmanager95.exe --selbsttest selbsttest.txt
|
|
||||||
CODE=$?
|
|
||||||
set -e
|
|
||||||
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
|
|
||||||
echo "Selbsttest der .exe FEHLGESCHLAGEN (Exit-Code $CODE)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Selbsttest bestanden"
|
|
||||||
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
|
||||||
@@ -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
|
||||||
@@ -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")
|
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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,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"
|
||||||
@@ -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")
|
||||||
@@ -277,12 +277,12 @@ 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._haendlerliste(behalte_auswahl)
|
self._haendlerliste(behalte_auswahl)
|
||||||
self._tabelle()
|
self._tabelle()
|
||||||
@@ -1061,7 +1061,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 +1073,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
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
|
After Width: | Height: | Size: 990 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 951 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 964 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 876 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 1007 B |
|
After Width: | Height: | Size: 827 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 539 B |
|
After Width: | Height: | Size: 811 B |
|
After Width: | Height: | Size: 686 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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
|
||||||
@@ -917,10 +917,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 +1025,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 +1034,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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,9 @@ 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)
|
||||||