diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/Change.log b/Change.log new file mode 100644 index 0000000..f5c834b --- /dev/null +++ b/Change.log @@ -0,0 +1,68 @@ +Änderungshistorie + +Version 1.4: + (1) + Bei mehreren Buchungsjahren wurden die Protokolle in die verkehrte /Versendet-Gruppe geschrieben. + Beispiel: + /Belege + ├── /2026 + ├── /01-Januar + └── [Quittung-1] + └── [Beleg-x] + [...] + /Versendet + ├── /2027 + ├── /01-Januar + └── [Quittung-1] + └── [Beleg-y] + [...] + /Provisionsabrechnungen + /Versendet + + Die Protokolle landen jetzt in der /Versendet-Gruppe des Jahres, aus dem die versendeten Belege stammen. + + (2) + Beleg-Versand über mehrere Jahre wird untersagt: + Die Protokolle liegen in einer einzigen /Versendet-Gruppe. Um eine Split-Funktion zu vermeiden (beleg-1 in 2026, beleg-2 in 2027), wird vor der Pack-/Sende-Routine sichergestellt, dass lediglich Dokumente aus einem einzigen Buchungsjahr in der Auswahl sind. + +Version 1.3 + + Dry Run schreibt immer eine vollständige Vorschau-Datei + Dry Run zeigt zusätzlich eine Kurzliste direkt im Dialog + Vorschau-Datei kann direkt im Finder angezeigt werden + Echtlauf läuft nach Bestätigung ohne zweite Rückfrage weiter + Abschlussmeldung weist auf die Prüfung des automatisch eingefügten Mailtexts hin + menschenlesbares Markdown-Log wird in Sendelauf-Blöcken gegliedert + neuer Config-Key PATH_TO_DRYRUN_PREVIEW + README und Konfigurationsbeschreibung entsprechend erweitert + +Version 1.2.1 + + interne Scripts in Unterordner Scripts + Mailbody in _config/mail_vorlagen/belegversand + Mail-Betreff ohne Batch-ID + konfigurierbares Dokument-Tag (SENT_TAG_FOR_DOCS) + Konfigurationsdatei klarer gegliedert + interne README und Archiv-README ergänzt + +Version 1.2 + + Mailbody-Datei in DEVONthink eingeführt + README in DEVONthink eingeführt + Welcome-/Hinweisdialog über SHOW_README + Konfiguration in _config weiter ausgebaut + +Version 1.1 + + Konfiguration in DEVONthink eingeführt + Setup-Script ergänzt + Startscript ergänzt + Root-relative Suche der Konfiguration + +Version 1.0 + + erster funktionaler Versandablauf + ZIP-Erzeugung + Mail-Entwurf + Tagging + NDJSON-/Markdown-Logging diff --git a/Helpers/create-year-cycle.applescript b/Helpers/create-year-cycle.applescript new file mode 100644 index 0000000..2ad58ba --- /dev/null +++ b/Helpers/create-year-cycle.applescript @@ -0,0 +1,79 @@ +tell application id "DNtp" + set monthNames to {"01_Januar", "02_Februar", "03_MŠrz", "04_April", "05_Mai", "06_Juni", "07_Juli", "08_August", "09_September", "10_Oktober", "11_November", "12_Dezember"} + + set monthVariants to {"januar", "jan", "january",  + "februar", "feb", "february",  + "mŠrz", "maerz", "mrz", "mŠr", "march", "mar",  + "april", "apr",  + "mai", "may",  + "juni", "jun", "june",  + "juli", "jul", "july",  + "august", "aug",  + "september", "sep", "sept",  + "oktober", "okt", "october", "oct",  + "november", "nov",  + "dezember", "dez", "december", "dec"} + + set selRecs to selection as list + if (count of selRecs) is 0 then + display dialog "Bitte einen Ordner in DEVONthink auswŠhlen." buttons {"OK"} default button "OK" + return + end if + + set targetGroup to first item of selRecs + set targetName to name of targetGroup + + set normalizedTargetName to my normalizeMonthFolderName(targetName) + if monthVariants contains normalizedTargetName then + set answerButton to button returned of (display dialog "Du stehst im Ordner " & targetName & ". Willst du hier trotzdem einen Jahreszyklus anlegen?" buttons {"Abbrechen", "Ja"} default button "Ja" with icon caution) + if answerButton is "Abbrechen" then return + end if + + repeat with i from 1 to count of monthNames + set monthName to item i of monthNames + try + create record with {name:monthName, type:group} in targetGroup + end try + end repeat + + try + create record with {name:"Versendet", type:group} in targetGroup + end try + + display dialog "Fertig." buttons {"OK"} default button "OK" +end tell + +on normalizeMonthFolderName(theName) + set t to (do shell script "printf %s " & quoted form of theName & " | tr '[:upper:]' '[:lower:]'") + set t to my replaceText(".", "", t) + set t to my replaceText("_", " ", t) + set t to my replaceText("-", " ", t) + set t to my trimText(t) + try + set t to do shell script "printf %s " & quoted form of t & " | sed -E 's/^[0-9]{1,3}[[:space:]]+//; s/^[0-9]{1,3}//'" + on error + -- falls sed zickt, einfach Original weiterverwenden + end try + set t to my trimText(t) + return t +end normalizeMonthFolderName + +on replaceText(findText, replaceWith, sourceText) + set AppleScript's text item delimiters to findText + set textItems to every text item of sourceText + set AppleScript's text item delimiters to replaceWith + set newText to textItems as text + set AppleScript's text item delimiters to "" + return newText +end replaceText + +on trimText(t) + set whitespaceChars to {space, tab, return, linefeed} + repeat while t is not "" and (character 1 of t is in whitespaceChars) + set t to text 2 thru -1 of t + end repeat + repeat while t is not "" and (character -1 of t is in whitespaceChars) + set t to text 1 thru -2 of t + end repeat + return t +end trimText \ No newline at end of file diff --git a/Helpers/scripts/_belegversand.applescript b/Helpers/scripts/_belegversand.applescript new file mode 100644 index 0000000..1470bec Binary files /dev/null and b/Helpers/scripts/_belegversand.applescript differ diff --git a/Helpers/scripts/_belegversand_setup.applescript b/Helpers/scripts/_belegversand_setup.applescript new file mode 100644 index 0000000..131a382 Binary files /dev/null and b/Helpers/scripts/_belegversand_setup.applescript differ diff --git a/Helpers/show_mail_templates.applescript b/Helpers/show_mail_templates.applescript new file mode 100644 index 0000000..61883a1 Binary files /dev/null and b/Helpers/show_mail_templates.applescript differ diff --git a/Helpers/uuid_anzeigen.applescript b/Helpers/uuid_anzeigen.applescript new file mode 100644 index 0000000..bf2892c --- /dev/null +++ b/Helpers/uuid_anzeigen.applescript @@ -0,0 +1,36 @@ +(* +uuid_anzeigen.scpt + +Zeigt Name, Typ und UUID des aktuell ausgewŠhlten DEVONthink-Objekts an. +Die UUID wird zusŠtzlich in die Zwischenablage kopiert. + +Zweck: +- Diagnose +- manuelles Nachschlagen einer DEVONthink-UUID +- Vorbereitung fŸr das manuelle Aktualisieren von BELEGE_ROOT_UUID +*) + +tell application id "DNtp" + set sel to selection + if sel is {} then error "Bitte zuerst ein Objekt in DEVONthink auswŠhlen." + + set r to item 1 of sel + + set recName to name of r as text + + try + set recType to type of r as text + on error + set recType to "unbekannt" + end try + + set recUUID to uuid of r as text +end tell + +set the clipboard to recUUID + +display dialog "AusgewŠhltes Objekt:" & return & return &  + "Name: " & recName & return &  + "Typ: " & recType & return &  + "UUID: " & recUUID & return & return &  + "Die UUID wurde in die Zwischenablage kopiert." buttons {"OK"} default button "OK" \ No newline at end of file diff --git a/START.applescript b/START.applescript new file mode 100644 index 0000000..f608a9a --- /dev/null +++ b/START.applescript @@ -0,0 +1,319 @@ +(* +START.scpt - Version 1.4 +Rolle dieses Scripts +-------------------- +Dieses Script ist der sichtbare Einstiegspunkt fŸr Anwender:innen. +Es selbst verschickt keine Belege und fŸhrt auch keine Einrichtung durch, +sondern Ÿbernimmt nur zwei Aufgaben: +1. PrŸfen, ob relativ zur aktuellen DEVONthink-Auswahl eine Konfigurationsdatei + fŸr den Belegversand existiert. +2. Falls in dieser Konfiguration SHOW_README=true gesetzt ist, + einen kurzen Willkommen-/Hinweisdialog anzeigen. +3. Danach das Setup-Script _belegversand_setup.scpt starten. +Das Setup-Script entscheidet anschlie§end selbst: +- ob bereits eine gŸltige Einrichtung vorliegt und direkt das Hauptscript + gestartet werden kann, +- oder ob zunŠchst die Konfiguration eingerichtet werden muss. +Wichtig: +-------- +START.scpt ist bewusst klein gehalten. +Die eigentliche Fachlogik liegt in: +- _belegversand_setup.scpt +- _belegversand.scpt +*) +-- Name des Setup-Scripts, das im Unterordner "Helpers/scripts" liegt. +-- Dieses Script wird von START.scpt immer am Ende aufgerufen. +property setupScriptName : "_belegversand_setup.scpt" +-- Name der DEVONthink-Gruppe, in der Konfigurationsdateien liegen. +property configGroupName : "_config" +-- Name der eigentlichen Konfigurationsdatei in DEVONthink. +property configRecordName : "belegversand.conf" +(* +runSiblingScript(scriptFileName) +-------------------------------- +LŠdt und startet ein weiteres AppleScript aus dem Unterordner "Helpers/scripts". +Parameter: +- scriptFileName : Dateiname des aufzurufenden Scripts, z. B. + "_belegversand_setup.scpt" +Ablauf: +- absolute Basis-Path setzen (Home + fest Pfad zu Belegversand_1-4) +- daran den Unterordner "Helpers/scripts" und den Dateinamen anhŠngen +- prŸfen, ob die Scriptdatei existiert +- Script laden +- Script ausfŸhren +Fehlerbehandlung: +- wenn die Datei nicht gefunden wird -> Hinweisdialog +- wenn das Laden oder Starten fehlschlŠgt -> Hinweisdialog mit Fehlermeldung +Wichtig: +-------- +Der absolute Pfad ist invariant und unabhŠngig vom Speicherort dieses Scripts. +*) +on runSiblingScript(scriptFileName) + -- Absolute Basis-Path (Home + fest Installation) + set basePath to (do shell script "echo ~") & "/Library/Application Scripts/com.devon-technologies.think/Menu/Belegversand_1-4" + set targetPath to basePath & "/Helpers/scripts/" & scriptFileName + + try + set targetAlias to POSIX file targetPath as alias + on error + display dialog "Hinweis: Scriptdatei nicht gefunden:" & return & targetPath buttons {"OK"} default button "OK" + return + end try + + try + set loadedScript to load script targetAlias + run loadedScript + on error errMsg number errNum + display dialog "Hinweis: Konnte Script nicht starten:" & return & scriptFileName & return & return & errMsg buttons {"OK"} default button "OK" + end try +end runSiblingScript +(* +recordText(theRecord) +--------------------- +Liest den Textinhalt eines DEVONthink-Records. +Verwendung in START.scpt: +------------------------- +Hier wird dieser Handler genutzt, um den Inhalt der Datei +"_config/belegversand.conf" als Text einzulesen. +Ablauf: +- zunŠchst plain text versuchen +- wenn das fehlschlŠgt, auf content ausweichen +- wenn beides fehlschlŠgt, leeren String zurŸckgeben +Hinweis: +-------- +Die Konvertierung erscheint hier als "rich text", weil AppleScript/Script Editor +solche Stellen beim Kompilieren teilweise selbststŠndig umschreiben kann. +Der funktionale Zweck ist hier aber: Textinhalt lesbar zurŸckgeben. +*) +on recordText(theRecord) + tell application id "DNtp" + try + set t to plain text of theRecord + if t is missing value then set t to "" + return t as rich text + on error + try + set t to content of theRecord + if t is missing value then set t to "" + return t as rich text + on error + return "" + end try + end try + end tell +end recordText +(* +findChildByName(parentRecord, childName) +---------------------------------------- +Sucht innerhalb einer DEVONthink-Gruppe ein direktes Kind mit bestimmtem Namen. +Parameter: +- parentRecord : die Gruppe, in der gesucht werden soll +- childName : Name des gesuchten direkten Kinds +RŸckgabe: +- den gefundenen DEVONthink-Record +- oder missing value, wenn nichts passt +Verwendung in START.scpt: +------------------------- +- _config-Gruppe unter der DB-Wurzel suchen +- belegversand.conf innerhalb von _config suchen +*) +on findChildByName(parentRecord, childName) + tell application id "DNtp" + try + set kids to children of parentRecord + repeat with i from 1 to count of kids + set c to item i of kids + if (name of c as rich text) is childName then return c + end repeat + end try + end tell + return missing value +end findChildByName +(* +getTopParentGroup(theRecord) +---------------------------- +Ermittelt die oberste Gruppe einer DEVONthink-Hierarchie ausgehend von einem Record. +Ablauf: +- starte mit dem aktuell Ÿbergebenen Record +- frage wiederholt nach dessen Eltern +- solange es noch einen Parent gibt, gehe eine Ebene nach oben +- sobald kein Parent mehr vorhanden ist, ist die Wurzel erreicht +Verwendung in START.scpt: +------------------------- +START.scpt kennt die gewŸnschte Datenbank nicht direkt. +Darum nimmt es irgendein ausgewŠhltes Objekt in DEVONthink +und geht davon Ÿber die Parent-Kette bis zur obersten Gruppe hoch. +Dort wird dann _config gesucht. +Vorteil: +-------- +Das Script ist dadurch nicht an einen festen Datenbanknamen gebunden, +sondern arbeitet relativ zur aktuellen Auswahl. +*) +on getTopParentGroup(theRecord) + tell application id "DNtp" + set currentRecord to theRecord + + repeat + try + set p to parents of currentRecord + if p is {} then return currentRecord + set currentRecord to item 1 of p + on error + return currentRecord + end try + end repeat + end tell +end getTopParentGroup +(* +configValue(configText, keyName) +-------------------------------- +Liest aus einem einfachen key=value-Konfigurationstext den Wert zu einem bestimmten SchlŸssel. +Parameter: +- configText : kompletter Textinhalt der Konfigurationsdatei +- keyName : gesuchter SchlŸssel, z. B. "SHOW_README" +RŸckgabe: +- Textwert des gefundenen SchlŸssels +- oder missing value, wenn kein passender Eintrag existiert +Funktionsweise: +--------------- +- zeilenweise durch die Konfiguration gehen +- prŸfen, ob eine Zeile mit "KEY=" beginnt +- den Teil rechts vom "=" zurŸckgeben +*) +on configValue(configText, keyName) + set keyPrefix to keyName & "=" + repeat with oneLine in paragraphs of configText + set lineText to oneLine as text + if lineText starts with keyPrefix then + if (length of lineText) is equal to (length of keyPrefix) then + return "" + else + set val to text ((length of keyPrefix) + 1) thru -1 of lineText + return val as text + end if + end if + end repeat + return missing value +end configValue +(* +findConfigInCurrentDB() +----------------------- +Sucht die Konfigurationsdatei belegversand.conf relativ zur aktuellen DEVONthink-Auswahl. +Ablauf: +1. aktuelle Auswahl holen +2. falls nichts ausgewŠhlt ist -> missing value +3. von der Auswahl zur DB-Wurzel hochgehen +4. dort die Gruppe _config suchen +5. darin die Datei belegversand.conf suchen +RŸckgabe: +- den gefundenen Config-Record +- oder missing value, wenn keine passende Konfiguration existiert +Wichtig: +-------- +Es wird absichtlich nicht global in allen Datenbanken gesucht, +sondern immer nur relativ zur aktuellen Auswahl. +So bleibt das Verhalten bei mehreren Datenbanken stabil und nachvollziehbar. +*) +on findConfigInCurrentDB() + tell application id "DNtp" + set sel to selection + if sel is {} then return missing value + set selectedRecord to item 1 of sel + end tell + + set dbRootRecord to my getTopParentGroup(selectedRecord) + set configGroupRecord to my findChildByName(dbRootRecord, configGroupName) + if configGroupRecord is missing value then return missing value + + set configRecord to my findChildByName(configGroupRecord, configRecordName) + return configRecord +end findConfigInCurrentDB +(* +showWelcomeDialogIfNeeded() +--------------------------- +Zeigt den Start-/Willkommensdialog an, falls die Konfiguration dies wŸnscht. +Ablauf: +1. Config-Datei relativ zur aktuellen Auswahl suchen +2. Config-Datei lesen +3. Wert SHOW_README auslesen +4. falls SHOW_README=true -> Hinweisdialog anzeigen +5. falls User "Diesen Dialog nicht mehr anzeigen" wŠhlt: + SHOW_README auf false in der Config zurŸckschreiben +Hinweis: +-------- +START.scpt fŸhrt selbst keine Einrichtung und keinen Versand durch. +Der Dialog ist nur eine Hilfe fŸr Anwender:innen. +Er verweist auf die interne README-Datei in _config. +Bewusst einfach: +---------------- +Es gibt hier nur zwei Buttons: +- "Weiter" +- "Diesen Dialog nicht mehr anzeigen" +Die README wird nicht direkt aus dem Dialog gešffnet. +*) +on showWelcomeDialogIfNeeded() + set configRecord to my findConfigInCurrentDB() + if configRecord is missing value then return + + set cfgText to my recordText(configRecord) + set showReadmeVal to my configValue(cfgText, "SHOW_README") + if showReadmeVal is missing value then return + + set showReadmeLower to do shell script "printf %s " & quoted form of showReadmeVal & " | tr '[:upper:]' '[:lower:]'" + if showReadmeLower is not "true" then return + + set msgText to "Willkommen beim Belegversand, Version 1.4." & return & return &  + "Mit dieser Funktion kannst du Dokumente aus dieser DEVONthink-Datenbank auswŠhlen, in ein ZIP-Archiv packen und als Mail-Entwurf vorbereiten." & return & return &  + "Anschlie§end kšnnen die verarbeiteten Dokumente mit dem Tag 'gesendet' markiert werden. Der Versand wird zusŠtzlich in Protokolldateien dokumentiert." & return & return &  + "Hinweise zur Einrichtung und Benutzung findest du in der Datei:" & return &  + "README-Belegversand" & return &  + "im Ordner:" & return &  + "_config" + + set choice to button returned of (display dialog msgText buttons {"Diesen Dialog nicht mehr anzeigen", "Weiter"} default button "Weiter") + + if choice is "Diesen Dialog nicht mehr anzeigen" then + set newCfgText to "" + repeat with oneLine in paragraphs of cfgText + set lineText to oneLine as text + if lineText starts with "SHOW_README=" then + set newCfgText to newCfgText & "SHOW_README=false" & return + else + set newCfgText to newCfgText & lineText & return + end if + end repeat + + tell application id "DNtp" + try + set plain text of configRecord to newCfgText + on error + set content of configRecord to newCfgText + end try + end tell + end if +end showWelcomeDialogIfNeeded +(* +run +--- +Haupteinstiegspunkt des START-Scripts. +Ablauf: +1. optionalen Startdialog anzeigen +2. Setup-Script starten +Wichtig: +-------- +START.scpt delegiert die eigentliche Entscheidung an das Setup-Script. +Dort wird geprŸft: +- existiert schon eine gŸltige Konfiguration? + -> dann Hauptscript starten +- oder muss erst eingerichtet werden? +*) +on run + + with timeout of 3600 seconds + + my showWelcomeDialogIfNeeded() + my runSiblingScript(setupScriptName) + + end timeout + +end run \ No newline at end of file