/* 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 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) }