sherpa-fibu-konto-suchbox/FiBu-Konto.ahk

571 lines
18 KiB
AutoHotkey
Raw Normal View History

/*
Dieses Script vereinfacht das Buchen mit der Sherpa, indem FiBu-Konten
schnell gesucht und ausgewählt werden können. Das Suchfenster wird dafür
mit der Tastenkombination STRG+ALT+k (K wie Konto) geöffnet.
Es basiert auf AutoHotKey v2 und ist deshalb nur für Windows verfügbar.
Installation:
1. AutoHotKey v2 herunterladen & installieren: https://www.autohotkey.com/
2. Dieses Script (FiBu-Konto.ahk) z. B. in den AutoHotKey-Ordner ablegen
(Dokumente -> AutoHotKey)
3. FiBu-Konto.ahk doppelklicken (ein neues System-Icon öffnet sich)
Einrichtung (Kontenplan aus Summen- und Saldenliste importieren):
- In den Sherpa-Einstellungen sicherstellen, dass der Export mit UTF-8-
Zeichensatz geschieht (Allgemein -> Finanzbuchhaltung -> Exportoptionen).
Diese Einstellung muss für jeden Kontext/Gliederung gesetzt werden.
- In Sherpa: Finanzbuchhaltung -> Summen- und Saldenliste -> Anzeigen
-> "Nur bebuchte Konten" ausschalten (alle anzeigen)
-> CSV-Datei exportieren
- System-Icon rechtsklicken, "Summen- und Saldenliste importieren",
Datei auswählen
- Die SuSa-Datei kann jetzt gelöscht werden
Benutzung:
- Cursor in der Sherpa in das Soll-/Haben-Eingabefeld setzen
- STRG+ALT+k öffnet das Fenster
- Enter-Taste schreibt das 4-stellige FiBu-Konto in das Soll-/Haben-Feld
- Escape-Taste schließt das Fenster
Tipps:
- Die Suche findet alle Konten, die die Zeichen in der richtigen Reihenfolge
enthalten, diese müssen aber nicht direkt aufeinander folgen. Ein Beispiel:
"vblkr" findet "Verbindlichkeiten an Kreisverbände"
- Das Script kann automatisch bei Systemstart ausgeführt werden, sodass das
Suchfenster immer verfügbar ist: Rechtsklick auf das System-Icon
-> "Bei Systemstart starten"
- Das Script kann nach der Eingabe des FiBu-Kontos Strg+S drücken, z. B. um
den Buchungsstapel regelmäßig zu speichern
- Außerdem kann das FiBu-Konto vorgelesen werden, um eine akustische Rück-
meldung zu erhalten und dadurch Fehler zu vermeiden
Lizenz: GPL v3
Ansprechperson: Florian Kürsch <florian.kuersch@hamburg.gruene.de>
Version: 0.1
Stand: 2024-10-23
*/
#Requires AutoHotkey v2.0
FileEncoding("UTF-8")
autostartShortcutPath := A_Startup . "\FiBu-Konto.lnk"
configDir := A_AppData . "\AHK-FiBu-Konto"
ktoListPath := configDir . "\fibu-konten.txt"
autosaveConfigPath := configDir . "\autosave_enabled"
voiceConfigPath := configDir . "\voice_enabled"
DirCreate(configDir)
defaultKtoList := [
"0000 Importiere die FiBu-Konten über die SuSa-Liste"
]
isActive := false
voice := ComObject("SAPI.SpVoice")
voice.Volume := 100 ; Volume from 0 to 100
voice.Rate := 0 ; Rate from -10 (slow) to 10 (fast)
A_TrayMenu.Delete()
A_TrayMenu.Insert("1&", "Konten-Suche öffnen (Strg+Alt+k)", (*) => open())
A_TrayMenu.Insert("2&") ; separator
A_TrayMenu.Insert("3&", "Summen- && Saldenliste importieren...", (*) => import_susa_file())
A_TrayMenu.Insert("4&", "Beim Systemstart starten", toggle_autostart)
if (is_autostart_enabled()) {
A_TrayMenu.Check("4&")
}
A_TrayMenu.Insert("5&", "Nach Eingabe des Kontos Strg+S drücken", toggle_autosave)
if (is_autosave_enabled()) {
A_TrayMenu.Check("5&")
}
A_TrayMenu.Insert("6&", "Konto vorlesen", toggle_voice)
if (is_voice_enabled()) {
A_TrayMenu.Check("6&")
}
A_TrayMenu.Insert("7&") ; separator
A_TrayMenu.Insert("8&", "Beenden", (*) => ExitApp())
myGui := Gui("+AlwaysOnTop -Caption +ToolWindow -MinimizeBox", "FiBu-Konto auswählen")
myGui.SetFont("s10")
myGui.OnEvent("Escape", on_escape_pressed)
guiHwnd := myGui.Hwnd
OnMessage(0x0006, handle_activation)
myEdit := myGui.Add("Edit", "w500 vMyEdit")
myEdit.OnEvent("Change", on_text_changed)
myListBox := myGui.Add("ListBox", "r15 w500 vmyListBox")
okButton := myGui.Add("Button", "Default w80", "OK")
okButton.OnEvent("Click", on_okBtn_clicked)
ktoList := read_array_from_file(ktoListPath, defaultKtoList)
apply_list(ktoList)
if (not is_kto_list_imported()) {
import_susa_file()
}
^!k:: ; Ctrl+Alt+k
{
open()
}
on_escape_pressed(*) {
close()
}
handle_activation(wParam, lParam, msg, hwnd) {
if (hwnd = guiHwnd && !wParam) {
on_focus_lost()
}
}
on_focus_lost() {
close()
}
on_okBtn_clicked(*) { ; auch wenn Enter gedrückt wird
if (myListBox.Value = 0) {
return
}
kto := SubStr(myListBox.Text, 1, 4) ; FiBu-Konten sind 4-stellig
close()
if(is_voice_enabled()) {
textToSpeak := RegExReplace(myListBox.Text, "^\d{4}\s+")
textToSpeak := RegExReplace(textToSpeak, "U)\s*\(.*\)$") ; U) ungreedy
textToSpeak := textToSpeak . ", " . RegExReplace(kto, "(.)", "$1 ")
SetTimer () => say(textToSpeak), -1 ; async
}
SendText(kto)
if(is_autosave_enabled()) {
SendInput "^s" ; ctrl + s
}
}
open() {
global isActive
isActive := true
show_gui_on_active_monitor()
myEdit.Focus()
}
close() {
global isActive
myGui.Hide()
isActive := false
}
; Wir wollen aus dem Textfeld heraus Elemente aus der Liste auswählen.
; Deshalb registrieren wir einen globalen Hotkey, der nur ausgeführt
; wird, wenn das Fenster aktiv ist (es gibt kein KeyPress-Event o. Ä.
; für das Textfeld).
#HotIf isActive
Up:: {
up_by(1)
}
PgUp:: {
up_by(10)
}
up_by(offset) {
val := myListBox.Value - offset
if (val < 1) {
val := 1
}
myListBox.Value := val
}
Down:: {
down_by(1)
}
PgDn:: {
down_by(10)
}
down_by(offset) {
val := myListBox.Value + offset
len := ControlGetItems(myListBox).Length
if (val > len) {
val := len
}
myListBox.Value := val
}
#HotIf
on_text_changed(*) {
do_search_delayed()
}
searchUpdateTimer := do_search
do_search_delayed() {
global searchUpdateTimer
if (searchUpdateTimer != 0) {
SetTimer searchUpdateTimer, 0 ; clear
}
searchUpdateTimer := do_search
SetTimer(searchUpdateTimer, -50) ; 50 ms delay / negative: exec once
}
do_search() {
searchText := myEdit.Text
filteredList := filter_list(ktoList, searchText)
apply_list(filteredList)
select_best_match(filteredList, searchText)
}
filter_list(originalList, searchText) {
filteredList := []
Loop originalList.Length {
if is_subsequence(originalList[A_Index], searchText) {
filteredList.Push(originalList[A_Index])
}
}
return filteredList
}
apply_list(newList) {
myListBox.Opt("-Redraw")
myListBox.Delete()
if(newList.Length = 0) {
okButton.Enabled := false
myListBox.Opt("+Redraw")
return
}
okButton.Enabled := true
myListBox.Add(newList)
myListBox.Opt("+Redraw")
}
select_best_match(filteredList, searchText) {
if (filteredList.Length = 0) {
return
}
if (searchText = "") {
myListBox.Choose(1)
return
}
exact_match := find_best_exact_match(filteredList, searchText)
if (exact_match > 0) {
myListBox.Choose(exact_match)
return
}
grouped_match := find_best_grouped_match(filteredList, searchText)
if (grouped_match > 0) {
myListBox.Choose(grouped_match)
return
}
subsequence_match := find_best_subsequence_match(filteredList, searchText)
if (subsequence_match > 0) {
myListBox.Choose(subsequence_match)
return
}
;myListBox.Choose(1) ; fallback
}
find_best_exact_match(list, searchText) {
/*
Beispiel: Suche nach "43"
- "4243 ..."
- "4300 ..." <= Selektion, weil die 43 am Anfang steht
- "4301 ..."
*/
return find_best_match(list, searchText, match_substring)
}
match_substring(sourceText, searchText) {
startPos := InStr(sourceText, searchText, false) ; false = case-insensitive
length := startPos = 0 ? 0 : StrLen(searchText)
return {startPos: startPos, length: length}
}
find_best_grouped_match(list, searchText) {
/*
Beispiel: Suche nach "pa ra"
- "1803 Forderungen Pfand (Ford Pfand)"
- "4301 PA Allg Raummiete & Nebenkosten (PA Allg MietRaum)" <= Selektion, weil "ra" in einem neuen Wort beginnt
- "4321 PA PT Raummiete & Nebenkosten (PA PT MietRaum)" <= nicht selektiert, obwohl der Match kürzer ist
*/
return find_best_match(list, searchText, match_group)
}
match_group(sourceText, searchText) {
pattern := "iU)" . RegExReplace(searchText, "\s+", ".* ")
startPos := RegExMatch(sourceText, pattern, &match)
return {startPos: startPos, length: 1} ; Länge wird ignoriert
}
find_best_subsequence_match(list, searchText) {
/*
Beispiel: Suche nach "para"
Hier wird die 3. Zeile selektiert, weil "p" weit links steht und das Match früher Endet als in der 2. Zeile
- "1650 Sparbücher (Sparbuch)"
- "4100 Personalausgaben (Ausg Personal) <= "p" steht an der gleichen Position, aber das Match ist deutlich länger
- "4301 PA Allg Raummiete & Nebenkosten (PA Allg MietRaum)"
*/
return find_best_match(list, searchText, match_subsequence)
}
match_subsequence(sourceText, searchText) {
pattern := "iU)" . RegExReplace(searchText, "(.)", "$1.*")
; i: case-insensitive / U: ungreedy
startPos := RegExMatch(sourceText, pattern, &match)
length := startPos = 0 ? 0 : match.Len
return {startPos: startPos, length: length}
}
find_best_match(list, searchText, match_func) {
if (list.Length = 0 or searchText = "") {
return 0
}
bestListIdx := 0
bestListIdxMatchPos := 0 ; 0 = not found / fist index = 1
bestListIdxMatchLength := 0
loop list.Length {
row := list[A_Index]
matchInfo := match_func(row, searchText)
matchPos := matchInfo.startPos
matchLength := matchInfo.length
if (matchPos = 0) {
continue
}
if (bestListIdxMatchPos = 0
or matchPos < bestListIdxMatchPos
or (matchPos = bestListIdxMatchPos and matchLength < bestListIdxMatchLength)) {
bestListIdx := A_Index
bestListIdxMatchPos := matchPos
bestListIdxMatchLength := matchLength
}
}
return bestListIdx
}
is_subsequence(sourceText, searchText) {
return match_subsequence(sourceText, searchText).startPos > 0 ? true : false
}
import_susa_file() {
MsgBox("
(
Das Suchfenster benötigt eine Liste aller FiBu-Konten. Am einfachsten geht das über die Summen- und Saldenliste:
1. In den Sherpa-Einstellungen sicherstellen, dass der Export mit UTF-8-
Zeichensatz geschieht (Allgemein => Finanzbuchhaltung => Exportoptionen).
Hinweis: Diese Einstellung gilt nur für die aktuelle Gliederung.
2. In Sherpa: Finanzbuchhaltung => Summen- und Saldenliste => Anzeigen
-> "Nur bebuchte Konten" ausschalten (alle anzeigen)
-> CSV-Datei exportieren
Wenn die SuSa-Liste bereit ist, klicke bitte OK.
)", "Summen- und Saldenliste importieren")
selectedFile := FileSelect("1", "", "Summen- und Saldenliste auswählen (UTF-8)", "CSV-Dateien (*.csv)")
; "1": File Must Exist
if (selectedFile = "") {
return
}
newKtoList := create_kto_list_from_susa_file(selectedFile)
write_array_to_file(newKtoList, ktoListPath)
global ktoList
ktoList := newKtoList
apply_list(ktoList)
MsgBox("
(
FiBu-Konten erfolgreich importiert. Du kannst die SuSa-Datei jetzt löschen.
Das Suchfenster kannst du mit STRG+ALT+k öffnen, nachdem du den Cursor in ein Textfeld gesetzt hast.
)", "Import erfolgreich")
}
is_kto_list_imported() {
return FileExist(ktoListPath) != ""
}
create_kto_list_from_susa_file(susaFile) {
newKtoList := []
Loop read, susaFile {
if (A_Index = 1) {
Continue ; erste Zeile überspringen
}
lineNumber := A_Index
; workaround: das AutoHotKey CSV-Parsing erwartet Komma, Sherpa exportiert Semikolon
; todo: prüfen, was mit Kommata in Feldern passiert
line := StrReplace(A_LoopReadLine, ";", ",")
newKtoLine := ""
Loop parse, line, "CSV" {
if (A_Index = 4) {
break ; nur die ersten 3 Spalten sind für uns interessant
}
field := A_LoopField
if (A_Index = 3) { ; Kurzname
field := "(" . field . ")"
}
field := Trim(field)
newKtoLine := newKtoLine . " " . field
}
newKtoList.Push(Trim(newKtoLine))
}
return newKtoList
}
write_array_to_file(myArray, filePath) {
file := FileOpen(filePath, "w")
for line in myArray {
file.WriteLine(line)
}
file.Close()
}
read_array_from_file(filePath, defaultArray) {
if ( FileExist(filePath) = "" ) {
return defaultArray
}
myArray := []
Loop Read, filePath {
myArray.Push(A_LoopReadLine)
}
return myArray
}
toggle_autostart(itemName, itemPos, myMenu) {
if (is_autostart_enabled()) {
FileDelete(autostartShortcutPath)
myMenu.Uncheck(itemName)
} else {
FileCreateShortcut(A_ScriptFullPath, autostartShortcutPath)
myMenu.Check(itemName)
}
}
is_autostart_enabled() {
return FileExist(autostartShortcutPath) != ""
}
toggle_autosave(itemName, itemPos, myMenu) {
if (is_autosave_enabled()) {
FileDelete(autosaveConfigPath)
myMenu.Uncheck(itemName)
} else {
FileAppend "", autosaveConfigPath
myMenu.Check(itemName)
}
}
is_autosave_enabled() {
return FileExist(autosaveConfigPath) != ""
}
toggle_voice(itemName, itemPos, myMenu) {
if (is_voice_enabled()) {
FileDelete(voiceConfigPath)
myMenu.Uncheck(itemName)
} else {
FileAppend "", voiceConfigPath
myMenu.Check(itemName)
}
}
is_voice_enabled() {
return FileExist(voiceConfigPath) != ""
}
show_gui_on_active_monitor() {
activeWin := WinExist("A")
if (not activeWin) {
myGui.Show()
return
}
WinGetPos(&winX, &winY, &winWidth, &winHeight, activeWin)
winCenterX := winX + winWidth // 2
winCenterY := winY + winHeight // 2
myGui.Show()
; GetPos ist erst verfügbar, wenn das Fenster sichtbar ist
myGui.GetPos(&guiX, &guiY, &guiWidth, &guiHeight)
monitorIndex := get_monitor_index_from_window(winCenterX, winCenterY)
monitorInfo := get_monitor_info(monitorIndex)
guiX := monitorInfo.left + (monitorInfo.right - monitorInfo.left) // 2 - guiWidth // 2
guiY := monitorInfo.top + (monitorInfo.bottom - monitorInfo.top) // 2 - guiHeight // 2
myGui.Show(Format("x{} y{}", guiX, guiY))
}
get_monitor_index_from_window(x, y) {
monitorCount := MonitorGetCount()
Loop monitorCount {
monitorInfo := get_monitor_info(A_Index)
if (x >= monitorInfo.left && x < monitorInfo.right && y >= monitorInfo.top && y < monitorInfo.bottom) {
return A_Index
}
}
return 1
}
get_monitor_info(monitorIndex) {
MonitorGet(monitorIndex, &left, &top, &right, &bottom)
MonitorGetWorkArea(monitorIndex, &workLeft, &workTop, &workRight, &workBottom)
return {left: workLeft, top: workTop, right: workRight, bottom: workBottom}
}
say(textToSpeak) {
; todo: diese Liste als CSV-Datei speichern/laden
voice.Skip("Sentence", 1)
textToSpeak := StrReplace(textToSpeak, "Pers Net ", "Personal Netto: ", true)
textToSpeak := StrReplace(textToSpeak, "Pers SV ", "Personal Sozialversicherung: ", true)
textToSpeak := StrReplace(textToSpeak, "Pers FA ", "Personal Finanzamt: ", true)
textToSpeak := StrReplace(textToSpeak, "Pers ", "Personal ", true)
textToSpeak := StrReplace(textToSpeak, "GB Inv ", "Geschäftsbetrieb Inventar: ", true)
textToSpeak := StrReplace(textToSpeak, "GB Bü ", "Geschäftsbetrieb Büro: ", true)
textToSpeak := StrReplace(textToSpeak, "GB DL ", "Geschäftsbetrieb Dienstleistungen: ", true)
textToSpeak := StrReplace(textToSpeak, "GB Sonst ", "Geschäftsbetrieb Sonstiges: ", true)
textToSpeak := StrReplace(textToSpeak, "GB ", "Geschäftsbetrieb ", true)
textToSpeak := StrReplace(textToSpeak, "PA Allg ", "Politische Arbeit allgemein: ", true)
textToSpeak := StrReplace(textToSpeak, "PA PT ", "Politische Arbeit Parteitage: ", true)
textToSpeak := StrReplace(textToSpeak, "PA VA ", "Politische Arbeit Veranstaltungen: ", true)
textToSpeak := StrReplace(textToSpeak, "PA Gre ", "Politische Arbeit Gremien: ", true)
textToSpeak := StrReplace(textToSpeak, "PA ", "Politische Arbeit ", true)
textToSpeak := StrReplace(textToSpeak, "WK EU ", "Wahlkampf Europa-Wahl: ", true)
textToSpeak := StrReplace(textToSpeak, "WK BTW ", "Wahlkampf Bundestagswahl: ", true)
textToSpeak := StrReplace(textToSpeak, "WK Bü ", "Wahlkampf Bürgerschaftswahl: ", true)
textToSpeak := StrReplace(textToSpeak, "WK BV ", "Wahlkampf Bezirkswahl: ", true)
textToSpeak := StrReplace(textToSpeak, "WK ", "Wahlkampf ", true)
textToSpeak := StrReplace(textToSpeak, "VZ GB ", "Verrechnungszuschuss eingehend, Geschäftsbetrieb: ", true)
textToSpeak := StrReplace(textToSpeak, "VZ PA ", "Verrechnungszuschuss eingehend, Politische Arbeit: ", true)
textToSpeak := StrReplace(textToSpeak, "VZ WK ", "Verrechnungszuschuss eingehend, Wahlkampf: ", true)
textToSpeak := StrReplace(textToSpeak, "VZ ", "Verrechnungszuschuss eingehend ", true)
textToSpeak := StrReplace(textToSpeak, "vz GB ", "Verrechnungszuschuss ausgehend, Geschäftsbetrieb: ", true)
textToSpeak := StrReplace(textToSpeak, "vz PA ", "Verrechnungszuschuss ausgehend, Politische Arbeit: ", true)
textToSpeak := StrReplace(textToSpeak, "vz WK ", "Verrechnungszuschuss ausgehend, Wahlkampf: ", true)
textToSpeak := StrReplace(textToSpeak, "vz ", "Verrechnungszuschuss ausgehend ", true)
textToSpeak := StrReplace(textToSpeak, "EZ ", "Echter Zuschuss eingehend ", true)
textToSpeak := StrReplace(textToSpeak, "ez ", "Echter Zuschuss ausgehend ", true)
textToSpeak := StrReplace(textToSpeak, "GBV", "Geschäftsbesorgungsvertrag", true)
textToSpeak := StrReplace(textToSpeak, "auß ", "außerhalb ", true)
textToSpeak := StrReplace(textToSpeak, "HH", "Hamburg", true)
textToSpeak := StrReplace(textToSpeak, "BV", "Bundesverband", true)
textToSpeak := StrReplace(textToSpeak, "LV", "Landesverband", true)
textToSpeak := StrReplace(textToSpeak, "KV", "Kreisverband", true)
textToSpeak := StrReplace(textToSpeak, "GJ", "Grüne Jugend", true)
textToSpeak := StrReplace(textToSpeak, "Gl.", "Gliederung", true)
textToSpeak := StrReplace(textToSpeak, "Fr ", "Fraktion ", true)
textToSpeak := StrReplace(textToSpeak, "FiBu", "Finanzbuchhaltung", true)
textToSpeak := StrReplace(textToSpeak, "USt", "Umsatzsteuer", true)
voice.Speak(textToSpeak, 0x1)
}