Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2600eea574 | ||
|
|
057cca6378 | ||
|
|
ec99042569 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Baut die Windows-Version (.exe, mit Windows-Python unter Wine) und die Linux-Version.
|
||||
# - 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
|
||||
|
||||
on:
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
path: |
|
||||
dist/Provisionsmanager95.exe
|
||||
dist/Provisionsmanager95-*.zip
|
||||
dist/Provisionsmanager-Setup-*.exe
|
||||
|
||||
- name: Release erstellen / ergänzen
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
@@ -44,7 +45,9 @@ jobs:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITEA_TOKEN }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
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*
|
||||
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*.
|
||||
@@ -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
|
||||
virtuellen Bildschirm, danach `Provisionsmanager95.exe` bauen und mit
|
||||
`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,
|
||||
Programm schließen, während Assistent/Rückfragen/Rundgang offen sind, Alt+F4, Beträge ändern usw.
|
||||
|
||||
+40
-4
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
@@ -40,19 +41,24 @@ echo "== PyInstaller =="
|
||||
$PY -m PyInstaller --noconfirm --clean --distpath dist --workpath build/tmp build/provisionsmanager.spec
|
||||
|
||||
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
|
||||
set +e
|
||||
timeout 300 wine dist/Provisionsmanager95.exe --selbsttest selbsttest.txt
|
||||
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 der .exe FEHLGESCHLAGEN (Exit-Code $CODE)"
|
||||
echo "Selbsttest von $1 FEHLGESCHLAGEN (Exit-Code $CODE)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Selbsttest bestanden"
|
||||
}
|
||||
|
||||
echo "== Selbsttest der fertigen .exe =="
|
||||
selbsttest dist/Provisionsmanager95.exe
|
||||
cp README.md dist/LIESMICH.md 2>/dev/null || true
|
||||
( 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()" \
|
||||
@@ -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()" \
|
||||
"Provisionsmanager95-${VERSION}.zip" Provisionsmanager95.exe LIESMICH.md )
|
||||
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
|
||||
@@ -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,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
|
||||
import os
|
||||
import sys
|
||||
@@ -31,3 +32,19 @@ exe = EXE(
|
||||
strip=False,
|
||||
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)
|
||||
if [ -z "$id" ]; then
|
||||
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.
|
||||
id=$(release_id)
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach."""
|
||||
|
||||
__version__ = "1.4.1"
|
||||
__version__ = "1.5.0"
|
||||
APP_NAME = "Provisions-Manager 95"
|
||||
APP_ORG = "Handy-Insel Miesbach"
|
||||
+52
-5
@@ -195,8 +195,23 @@ class App:
|
||||
bd=0, opaqueresize=False, sashcursor="sb_h_double_arrow")
|
||||
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
|
||||
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",
|
||||
selectmode="browse")
|
||||
self.htree.heading("#0", text="Händler", anchor="w")
|
||||
@@ -211,7 +226,7 @@ class App:
|
||||
r.pack(fill="both", expand=True)
|
||||
self.htree.bind("<<TreeviewSelect>>", self._haendler_gewaehlt)
|
||||
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
|
||||
self.f_p = w.Fenster(pw, "Positionen", self.icons("neu"))
|
||||
@@ -284,9 +299,26 @@ class App:
|
||||
self.positionen = []
|
||||
self.rahmen.set_titel(w.APP_NAME)
|
||||
self.status.set("info", " Keine Abrechnung geöffnet – Datei > Neue Abrechnung oder Import")
|
||||
self._abrechnungsliste()
|
||||
self._haendlerliste(behalte_auswahl)
|
||||
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):
|
||||
"""Suche erst kurz nach dem letzten Tastendruck ausführen (flüssiges Tippen)."""
|
||||
if self._suche_geplant is not None:
|
||||
@@ -466,7 +498,7 @@ class App:
|
||||
else:
|
||||
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:
|
||||
dateien = list(filedialog.askopenfilenames(
|
||||
parent=self.root, title="Provisionsabrechnungen importieren",
|
||||
@@ -477,7 +509,22 @@ class App:
|
||||
if not dateien:
|
||||
return
|
||||
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
|
||||
bericht = []
|
||||
self.root.configure(cursor="watch")
|
||||
@@ -1018,7 +1065,7 @@ class App:
|
||||
self.ansicht = ALLE
|
||||
self._aktualisieren()
|
||||
if res["dateien"]:
|
||||
self.importieren(res["dateien"])
|
||||
self.importieren(res["dateien"], ziel=self.aid)
|
||||
if res["rundgang"]:
|
||||
self.root.after(200, self.rundgang)
|
||||
|
||||
|
||||
+18
-4
@@ -558,10 +558,13 @@ class ProvisionsartenDialog(w.Dialog):
|
||||
# =========================================================================== Abrechnungen
|
||||
|
||||
class AbrechnungenDialog(w.Dialog):
|
||||
def __init__(self, master, db: Datenbank, aktuelle_id: int | None):
|
||||
super().__init__(master, "Abrechnung öffnen", resizable=True)
|
||||
def __init__(self, master, db: Datenbank, aktuelle_id: int | None, fuer_import: bool = False):
|
||||
"""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
|
||||
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"),
|
||||
show="headings", height=12, selectmode="browse")
|
||||
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())
|
||||
k = tk.Frame(self, bg=w.FACE)
|
||||
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, "Umbenennen...", self._umbenennen, breite=12).pack(side="left", padx=6)
|
||||
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.destroy()
|
||||
|
||||
def _neu(self):
|
||||
self.result = ("neu",)
|
||||
self.destroy()
|
||||
|
||||
def _umbenennen(self):
|
||||
aid = self._sel()
|
||||
if not aid:
|
||||
@@ -917,10 +928,13 @@ class ExportDialog(w.Dialog):
|
||||
tk.Label(zf, text="bis:").pack(side="left")
|
||||
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))
|
||||
# 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)])
|
||||
ttk.Combobox(z, textvariable=self.v_basis, values=list(zr.BASEN.values()), state="readonly",
|
||||
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")
|
||||
self.v_kennz = tk.BooleanVar(value=True)
|
||||
tk.Checkbutton(z, text="Exportierte Positionen als abgerechnet kennzeichnen (werden dann nicht noch "
|
||||
|
||||
+46
-1
@@ -1098,11 +1098,18 @@ def test_export_zeitraum_kennzeichnen_und_erledigt(lauf_modern, tmp_path, monkey
|
||||
d.v_auf.set(False)
|
||||
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([
|
||||
(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 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"),
|
||||
])
|
||||
assert not lauf.uebrig, lauf.diagnose()
|
||||
@@ -1116,3 +1123,41 @@ def test_export_zeitraum_kennzeichnen_und_erledigt(lauf_modern, tmp_path, monkey
|
||||
app.prozent()
|
||||
assert "erledigt" in meldungen[-1]
|
||||
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()
|
||||
Reference in new issue
Block a user