DT4 - show window to review changes

  1. The question
  • Is there a better way to show a “review window”?
  • Does it make sense to ask for a display dialog (AppleScript) where I can
    • view the content of the file
    • see the new filename, attributes and tags - e.g. display review "DNtp" with new name "asdf" new tags {"tag1", "tag2"} in theNewLocation buttons {"Abort", "Cancel", "Apply"}
  1. The Setup
  • I’ve got a directory which can contain a lot files - invoices, health records, …
  • I organise my files by locations - tags are only used for some specific use cases - find files for tax applications, or find target locations
  • Each document is handled by some rules I defined early - e.g. content, kind, tags
  1. Aim
  • Based on the result, I want to get the target location for each document and some more specific tags
  • Before I apply the changes, I would like to get an idea what the document is about
  1. The relevant code
  • For now, I open a new window for the record
  • Show an “Alert”
  • Handle the response
  • Close the window
-- [...]
set theWindow to open window for record theRecord

local alertResponse
set alertResponse to display alert "DNtp" message "File: " & theName & "
Targent path: " & theTargetPath & "
Database: " & theDatabase & "
Tags: " & my convertListToString(theResultTags, ", ") buttons {"Abort", "Ignore", "Move"} default button 2 as warning

close theWindow
-- [...]
  1. The script

Based on other posts in this forum, I made this script not being cached. This is intentional.

set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/document-invoices-handle.scpt"

set ScriptHandler to load script (POSIX file theScriptPath as alias)

Here comes the “full” script whithout my specific rules.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

-- test performSmartRule
tell application id "DNtp" to my performSmartRule(selected records)

-- test scriptOutput
-- tell application id "DNtp" to my scriptOutput(selected record 1, "")

return

on scriptOutput(theRecord, theInput)
	tell application id "DNtp"
			local theScriptPath
			set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/document-invoices-handle.scpt"
			
			local ScriptHandler
			set ScriptHandler to load script (POSIX file theScriptPath as alias)
			
			ScriptHandler's ForOne(theRecord)
	end tell
end scriptOutput

on performSmartRule(theRecords)
	tell application id "DNtp"
			local theScriptPath
			set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/document-invoices-handle.scpt"
			
			local ScriptHandler
			set ScriptHandler to load script (POSIX file theScriptPath as alias)
			ScriptHandler's ForAll(theRecords)
	end tell
end performSmartRule

-- Run job for all given records
-- Uses default model for given engine
on ForAll(theRecords)
	tell application id "DNtp"
		if the length of theRecords is less than 1 then error "One or more record must be selected in DEVONthink."
		
		show progress indicator "Run job for record" steps (count of theRecords)
		
		repeat with theRecord in theRecords
			set theName to the name of theRecord
			step progress indicator theName
			my ForOne(theRecord)
		end repeat
		
		hide progress indicator
	end tell
	
	log "[INFO] Job completed."
end ForAll

-- Run job for single record
-- Uses default model for given engine
on ForOne(theRecord)
	tell application id "DNtp"
		local thisYear
		set thisYear to year of (get creation date of theRecord) as string
		
		local theResult
		set theResult to my filterDocument(theRecord, thisYear)
		
		local theResultTags
		set theResultTags to tags of theResult
		
		if theResultTags is not missing value then
			set tags of theRecord to (tags of theRecord & theResultTags)
		end if
		
		if (company of theResult) is not missing value then
			add custom meta data company of theResult for "company" to theRecord
		end if
		
		if (basePath of theResult) is not missing value and (database of theResult) is not missing value then
			local theTargetPath
			set theTargetPath to (basePath of theResult & "/" & thisYear)
			
			local theDatabase
			set theDatabase to (database of theResult)
			
			local theTarget
			set theTarget to get record at theTargetPath in database theDatabase
			
			if theTarget is missing value then
				local theParent
				set theParent to get record at (basePath of theResult) in database theDatabase
				set theTarget to create record with {record type:group, name:thisYear} in theParent
			end if
			
			local theName
			set theName to name of theRecord
			
			set theWindow to open window for record theRecord
			
			local alertResponse
			set alertResponse to display alert "DNtp" message "File: " & theName & "
			Targent path: " & theTargetPath & "
			Database: " & theDatabase & "
			Tags: " & my convertListToString(theResultTags, ", ") buttons {"Abort", "Ignore", "Move"} default button 2 as warning
			
			close theWindow
			
			if get button returned of alertResponse = "Ignore" then
				return
			end if
			
			if get button returned of alertResponse = "Abort" then
				error "Abort handling of documents"
			end if
			
			log "[DEBUG] Name of file: " & theName
			log "[DEBUG] Target Path: " & theTargetPath
			log "[DEBUG] Database: " & theDatabase
			log "[DEBUG] Tags: " & my convertListToString(theResultTags, ", ")
			
			move record theRecord to theTarget
		end if
		
	end tell
	return theRecord
end ForOne


on filterDocument(theRecord, thisYear)
	tell application id "DNtp"
		local theName
		set theName to name of theRecord
		
		local theContent
		set theContent to plain text of theRecord
		
		local theKind
		set theKind to kind of theRecord
		
		local theTags
		set theTags to tags of theRecord
		
		if theContent contains "company XY" and theContent contains "1234567" then
			return {basePath:"/Finance/Company XY/kontoauszuege", database:"business", tags:{"finance"}, company:"Company XY"}
		end if
		
		-- [ ... ]
		
		if theTags contains "health" then
			return {basePath:"/Health", database:"business", tags:missing value, company:missing value}
		end if
		
		return {basePath:"/Receipts", database:"business", tags:missing value, company:missing value}
	end tell
end filterDocument

on setTags(theRecord, theNewTags)
	tell application id "DNtp"
		set theTags to tags of theRecord
		set tags of theRecord to theTags & theNewTags
	end tell
end setTags

on setCompany(theRecord, theName)
	tell application id "DNtp"
		add custom meta data theName for "company" to theRecord
	end tell
end setCompany

on convertListToString(theList, theDelimiter)
	set AppleScript's text item delimiters to theDelimiter
	set theString to theList as string
	set AppleScript's text item delimiters to ""
	return theString
end convertListToString

Is this AI-generated code?

Is it that bad? :slight_smile: It’s written by hand, but redacted for the forum. Getting some hints what I can improve are definitively appreciated.

Haha! I wouldn’t say it’s “bad” but it just looked suspiciously similar to AI code, especially the repeated local elements and a bunch of handlers.

  • You also have errant try statements in two of your handlers.
  • For what are you using script libraries?

I use “Script Debugger” as my favourite IDE. It requires local variables to be declared with “local”. Otherwise they do not occur within variable inspector. :person_shrugging:

“try” is fixed. That happened during cleaning up the code for the forum.

All my scripts are stored within “script libraries”. For only a few I use the load mechanism to get “dynamic” loading which does not require a restart of DEVONthink, if I need to change the rules.

Normally I use set CompressImages to script "compress-images".

on performSmartRule(theRecords)
	tell application id "DNtp"
		try
			set CompressImages to script "compress-images"
			CompressImages's ForAll(theRecords)
			
		on error error_message number error_number
			display alert "DNtp" message error_message as warning
			error number -199
		end try
	end tell
end performSmartRule

The Script Libraries directory is intended for reusable code, not a general storage location for scripts.


External smart action scripts need to be located in a specific directory. Are you only using embedded scripts and just calling a large number of handlers?


Why are you using on scriptOutput(theRecord, theInput) to handle one record and on performSmartRule(theRecords) for all ?

That would probably take up too much screen space. The UI stuff available in AppleScript/JXA is quite limited.

So, the files are still in your file system, not in DT?

As to your script

  • I’d try to catch the “no record selected” situation earlier.
  • Getting the plain text of an arbitrary record might not give you what you want.
  • kind is localized. record type is guaranteed to be always the same, regardless of the locale.
  • Instead of get record at, you could use create location in the first place – that’s more robust (imo), since it creates the target group if it doesn’t exist already. Saves an if, too.
  • variable names – I’ll never understand when people use “the” or “this” in AppleScript and when they don’t. Or why. What is the difference between scriptHandler and theResult? It’s alertResponse, but theName. Is there any system to that?
  • I don’t quite get where the tags are coming from that you use in your filterDocument. In my understanding, you have a bunch of files without any information that you want to file and name in DT.

Personally, I’d use another language that permits to write terser code. But you might know that already :wink:

Thanks a lot.

So, the files are still in your file system, not in DT?

No, they are within DT

  • Getting the plain text of an arbitrary record might not give you what you want.

What attribute would use instead? It was the best guess. Content and Source don’t give me the results I expect.

It’s alertResponse , but theName . Is there any system to that?

Of course not. I just try to imitate the ecosystem.

  • I don’t quite get where the tags are coming from that you use in your filterDocument. In my understanding, you have a bunch of files without any information that you want to file and name in DT.
INBOX
 |
 + - _PROCESS

I pre-process some files with rules in INBOX - e.g. adding some tags upfront, cleaning up the filename etc. Then move the files within DT to _PROCESS directory. That approach gives me more control if some files need more attention.

Personally, I’d use another language that permits to write terser code. But you might know that already :wink:

Sure, I do. Unfortunately there’s no debugger for that language. :wink:

plain text is fine if it exists. If you use only PDFs and e-mails – no problem. But I’d at least check for record type before accessing plain text.

Which seems to be blissfully empty of any system for variable names. I think that all these useless thes come from the fact that AS would not accept a variable called name if you’re inside a tell id "DNtp" block, as name is a property of record (or theRecord:wink: ).

I see. So, you presumably set tags in the pre-process and then in some cases (eg “health”) use (one of) the tags to decide on the target group? In that case, ie if the tag suffices to decide on the target, I’d just move the files to the corresponding group. No need to go through all the subsequent processing. I would want to get the simple cases out of the way as early as possible.

Only brain 1.0, or sometimes even 0.9. And Script Editor is EOL, too. Scripting on macOS will not become easier. Linus Torvalds, btw, despises debuggers :wink:

My scripts are mostly shims wrapping the library code. This makes testing/developing easier for me.

I use embedded scripts which call my libraries - see comment below.

The given code is a template to be copied into embedded scripts - Smart Rules, Batch processing. While developing in Script Debugger, I use this code to call the same method as from within the embedded script. This makes sure the code in the embedded script is working/correct.

tell application id "DNtp" to my performSmartRule(selected records)

First part of the script

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

-- test performSmartRule
tell application id "DNtp" to my performSmartRule(selected records)

-- Copy and paste template used in DEVONthink
on performSmartRule(theRecords)
	tell application id "DNtp"
		local theScriptPath
		set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/document-invoices-handle.scpt"
		
		local theScriptHandler
		set theScriptHandler to load script (POSIX file theScriptPath as alias)
		theScriptHandler's ForAll(theRecords)
	end tell
end performSmartRule

-- Run job for all given records
-- Uses default model for given engine
on ForAll(theRecords)
-- [...]
end ForAll

I just came across

set theResult to display group selector "Destination" buttons {"Abort", "Ignore", "Move"} for theDatabase with name and tags		

@BLUEFROG What I would love to see in the final DT4:

set theResult to display group selector "Destination" buttons {"Abort", "Ignore", "Move"} default name theName default button 2 default tags theTags default location theLocation for theDatabase with name and tags		

With the possibility to set defaults, it would be possible to pre-fill the dialog and fix wrong values later on. Having a default button would make it behave similar to “display alert”.

default button 2 
default tags theTags 
default location theLocation
default name theName

Hope that make sense.

Yes, I see what you’re saying. However, @cgrunenberg is the wizard in charge of this. I’m frontline support, not development. :slight_smile:

Apologies for revisiting this topic. @cgrunenberg, is this something your development team might consider?

Having the ability to review a file before it is moved by smart rules is crucial for me. My filters don’t always perform as anticipated, and this feature would save considerable time otherwise spent searching for documents misplaced in incorrect locations.

I can’t speak for what development will or won’t do, but your issue is part of why the On Demand is the default and only event trigger when you create a smart rule. This allows you to test and retest as you work out the wrinkles in your smart rules.

1 Like

We might consider this but not anytime soon, it’s the first request of its kind.

Thanks for the feedback. You’re right. Testing combined with utilizing the “On Demand” trigger significantly streamlines the development process.

Though I’ve opted for a different approach for this particular smart rule: I decided to go for a set of specific rules and some sensible default rules I implemented and tested upfront, but then I modify and (re-)test them over the time to strike a balance between time invested in refining rules and achieving optimal results —specifically, ensuring documents are moved to the correct location and appropriately modified or renamed.

For digitizing my paper invoices, I go for greyscale scanning with the EPSON ES-580W, which, in my experience, yields the best OCR accuracy. Black/White yields the worst results. However, OCR frequently confuses a lowercase “L” with an uppercase “I,” and vice versa.

I’d like to outline the challenges I’ve encountered in regard of writing/applying rules to documents:

  1. Straightforward: I apply specific rules for handling native PDF invoices from insurance companies, banks, and similar institutions. These documents follow consistent patterns that I can use in my rules. That’s straightforward to implement and test.
  2. Intermediate: I’ve implemented tailored rules for scanned PDF invoices from similar sources, also utilizing their recurring structures. This approach has helped resolve some problematic cases, but requires more work to get the rules right.
  3. Advanced: Same as 2. but already scanned black/white documents I recently moved into DEVONthink are quite a challenge. The error rate is quite high. Re-scanning those documents would be an option, but I doubt I will find all duplicates.
  4. Intermediate to Advanced: I employ default rules for invoices and other recurring documents that match keywords like “invoice,” “Rechnung,” and others relevant to the context.

Since maximizing my tax returns is important to me, I tag these documents with labels such as “tax-2024” during review, simplifying retrieval when it’s time to prepare my tax application. As these are default rules, there’s a higher risk of false positives. For now, when new documents are misclassified, I pause processing, adjust the rules as needed, and repeat the workflow.

Adding or modifying tags often means setting the document aside for now and revisiting it during a second review cycle. Revising the rules isn’t always warranted. Occasionally, it’s more efficient to manually file a unique document in its appropriate location.

My workflow

[Scan documents]                 |
[Store documents in filesystem]  | -> [Hazel: Import documents] -> [Apply rules] -> [Specific rules] -> [Default rules based on tags or pattern like "invoice"] -> [Set tags] -> [Move to location]
[Download files with OpenCloud]  |

Have a good day.

This is the very last take on making reviewing documents easier before moving them.

  1. I use rules to gather meta data for a record
  2. I use a window to show the current document
  3. I use another window to show metadata gathered via rules - the review window has a dropdown box to set default values for several fields - see Change elements dynamically using Shane's Dialog Toolkit? - #11 by t.spoon - AppleScript | Mac OS X - MacScripter for more about this technique
  4. I can overwrite some fields with default using a dropdown
  5. The shared script contains some test rules I use to test the script
  6. Abort: Aborts the whole process, Ignore: Moves to the next file, Move: Apply changes to the file and move the target location
  7. Create Yearly location: Append the year to the end of the location when the file was added to the database
  8. I use “Dialog Toolkit Plus” version “1.1.0” to show the window for the metadata review
  9. Entering a “tax year” adds a tag “taxes-YEAR” to the document
  10. I use custom metadata to store some attributes, e.g. company

My setup

Test files
I use RTF in my test setup - but I cannot upload these, so I provide PDF versions of the documents

health-document.pdf (16.6 KB)
invoice.pdf (17.2 KB)

The popup - examples

The smart rule

The tag given is not used right now, but might be a filter in the future.

on performSmartRule(theRecords)
	tell application id "DNtp"
		local theScriptPath
		set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/document-invoices-handle.scpt"
		
		-- local theScriptHandler
		set theScriptHandler to load script (POSIX file theScriptPath as alias)
		
		theScriptHandler's ForAll(theRecords)
	end tell
end performSmartRule

The script V1.0

~/Library/Script\ Libraries/document-invoices-handle.scpt:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.0"
use script "RegexAndStuffLib" version "1.0.7"

-- test performSmartRule
property isDev : false

-- Update UI fields
property companyField : {}
property locationPathField : {}
property databaseField : {}
property tagsField : {}
property presetsPopup : {}
property yearlyLocationCheckbox : {}

-----------
-- code to run within Script Debugger
tell application id "DNtp" to my performSmartRule(selected records)
return
-----------

-- copy template for smart rules
on performSmartRule(theRecords)
	tell application id "DNtp"
		local theScriptPath
                -- this prevents caching of the script by DT
		set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/document-invoices-handle.scpt"
		
		-- local theScriptHandler
		set theScriptHandler to load script (POSIX file theScriptPath as alias)
		
                -- this allows breakpoints
                -- controlled by property
		if isDev is true then
			set theScriptHandler to script "document-invoices-handle"
			display alert "Development Mode enabled"
		end if
		
		theScriptHandler's ForAll(theRecords)
	end tell
end performSmartRule

-- Run job for all given records
on ForAll(theRecords)
	tell application id "DNtp"
		if the length of theRecords is less than 1 then error "One or more record must be selected in DEVONthink."
		
		show progress indicator "Handle documents" steps (count of theRecords)
		-- https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_xmpls.html
		
		try
			repeat with theRecord in theRecords
				set theName to the name of theRecord
				--step progress indicator theName
				my ForOne(theRecord)
			end repeat
		on error errStr number errorNumber
			-- If our own error number, warn about bad data.
			if the errorNumber is equal to 1000 then
				return
			else
				-- An unknown error occurred. Resignal, so the caller
				-- can handle it, or AppleScript can display the number.
				error errStr number errorNumber
			end if
		end try
		
		hide progress indicator
	end tell
	
	log "[INFO] Job completed."
end ForAll

-- Run job for single record
on ForOne(theRecord)
	tell application id "DNtp"
		local thisYear
		set thisYear to year of (get creation date of theRecord) as string
		
		-- use rules to get metadata about file
		-- default is missing value if no rule matches
		local recordMetadata
		set recordMetadata to my getMetadataForRecord(theRecord, thisYear)
		
		-- local theName
		set theName to name of theRecord
		
		if recordMetadata is missing value then
			-- display alert "DNtp" message "File: " & theName & " has no matching rule. I ignore this file and move on if multiple files are selected"
			-- log "File: " & theName & " has no matching rule. I ignore this file and move on if multiple files are selected"
			return
		end if
		
		if (basePath of recordMetadata) is missing value or (databaseName of recordMetadata) is missing value then
			display alert "DNtp" message "basePath or database is missing for the file " & theName & ". This is not allowed. Going on with the next file."
			return
		end if
		
		-- create window to show record and check it with given metadata
		local theWindow
		set theWindow to open tab for record theRecord
		
		-- show dialog and ask user for "help"
		local dialogResponse
		set dialogResponse to my showDialog(recordMetadata, thisYear)
		set theButtonPressed to get buttonName of dialogResponse
		
		-- handle buttons
		if theButtonPressed = "Ignore" or theButtonPressed = "Gave up" then
			-- if windows has been already closed prevents error
			try
				close theWindow
			end try
			
			return
		end if
		
		if theButtonPressed = "Abort" then
			-- no: close theWindow to make review and handling easier
			error "Abort handling of documents" number 1000
		end if
		
		-- get information about record from dialog
		local parsedDialogResponse
		set parsedDialogResponse to my parseDialogResponse(dialogResponse)
		
		-- get metadata from response
		local updatedTags, theDatabaseName, theCompany, locationPath, theButtonPressed
		set updatedTags to (updatedTags of parsedDialogResponse)
		set theDatabaseName to (databaseName of parsedDialogResponse)
		set theCompany to (companyName of parsedDialogResponse)
		set locationPath to (locationPath of parsedDialogResponse)
		
		-- if windows has been already closed prevents error
		try
			close theWindow
		end try
		
		-- create location
		local createdLocation
		set createdLocation to create location locationPath in database theDatabaseName
		
		-- get database from location
		local theDatabase
		set theDatabase to (database of createdLocation)
		
		-- update tags
		if updatedTags is not missing value then
			set tags of theRecord to (updatedTags of parsedDialogResponse & updatedTags)
		end if
		
		-- set company name (custom attribute) from metadata
		if (companyName of parsedDialogResponse) is not missing value then
			add custom meta data companyName of parsedDialogResponse for "company" to theRecord
		end if
		
		-- move record to new location
		move record theRecord to createdLocation
	end tell
	
	return theRecord
end ForOne

on parseDialogResponse(dialogResponse)
	local companyName, updatedTags, databaseName, locationPath, addedYear, taxYear, locationPath, yearlyLocation
	set addedYear to (addedYear of dialogResponse)
	set taxYear to (taxYear of dialogResponse)
	set companyName to (companyName of dialogResponse)
	set databaseName to (databaseName of dialogResponse)
	set updatedTags to (updatedTags of dialogResponse)
	set locationPath to (locationPath of dialogResponse)
	set yearlyLocation to (yearlyLocation of dialogResponse)
	
	-- if user set year where this document is relevant for tax
	if taxYear is not "" then
		set taxYear to taxYear as number
		set updatedTags to updatedTags & {"taxes-" & taxYear}
	end if
	
	-- make sure we append the year the record was added to the database
	if yearlyLocation is true then
		set locationPath to (locationPath & "/" & addedYear)
	end if
	
	-- "missing value" is correct!
	if locationPath is "missing value" or locationPath is "" or locationPath is missing value then
		error "Undefined individual location for file: Either select a preset with default locations or an individual one"
	end if
	
	-- return gathered metadata
	return {companyName:companyName, updatedTags:updatedTags, databaseName:databaseName, locationPath:locationPath}
end parseDialogResponse

on showDialog(recordMetadata, thisYear)
	local yearlyLocation
	set yearlyLocation to (yearlyLocation of recordMetadata)
	
	local theDatabaseName
	set theDatabaseName to (databaseName of recordMetadata)
	
	local theCompanyName
	set theCompanyName to (companyName of recordMetadata)
	
	local updatedTags
	set updatedTags to (addTags of recordMetadata)
	
	local theLocationPath
	set theLocationPath to (basePath of recordMetadata)
	
	set accViewWidth to 600
	
	-- set buttons
	set {theButtons, minWidth} to create buttons {"Abort", "Ignore", "Move"} default button "Ignore" with equal widths
	
	if minWidth > accViewWidth then set accViewWidth to minWidth -- make sure buttons fit
	
	-- to make it look better, we can get the length of the longest label we will use, and use that to align the controls
	set theLabelStrings to {"Added year", "Tax year", "Company name", "Location", "Database", "Tags", "Chosen preset"}
	set maxLabelWidth to max width for labels theLabelStrings
	set controlLeft to maxLabelWidth + 8
	
	-- set names for the presets used in dropdown
	local presetNames
	set presetNames to {"no preset", "Receipts and Cancelations", "Health and Insurance", "Retirement", "Holidays"}
	
	-- empty added year to workaround namespace issue
	local addedYear
	set addedYear to ""
	
	if yearlyLocation is true then
		set addedYear to thisYear
	end if
	
	-- space between items
	local spacer
	set spacer to 20
	
	-- place tags into single input field by joining them into single string
	set joinedTags to (join strings updatedTags using delimiter ",")
	
	local addedDocYearField
	-- start from the bottom
	set {taxesYearField, taxesYearLabel, theTop, fieldLeft} to create side labeled field "" placeholder text "Tax relevant" left inset 0 bottom 0 total width (accViewWidth / 2) - spacer label text (item 2 of theLabelStrings) field left controlLeft
	set {addedDocYearField, addedDocYearLabel, theTop, fieldLeft} to create side labeled field addedYear placeholder text "Added date" left inset (accViewWidth / 2) + spacer bottom 0 total width (accViewWidth / 2) - spacer label text (item 1 of theLabelStrings) field left controlLeft
	set {companyField, companyLabel, theTop, fieldLeft} to create side labeled field theCompanyName placeholder text "the name of the company" left inset 0 bottom (theTop + 12) total width accViewWidth label text (item 3 of theLabelStrings) field left controlLeft
	set {yearlyLocationCheckbox, theTop, newWidth} to create checkbox "Create yearly location" left inset controlLeft bottom (theTop + 12) max width accViewWidth initial state yearlyLocation
	set {locationPathField, locationPathLabel, theTop, fieldLeft} to create side labeled field theLocationPath placeholder text "The path to location in DB" left inset 0 bottom (theTop + 12) total width accViewWidth label text (item 4 of theLabelStrings) field left controlLeft
	set {databaseField, databaseLabel, theTop, fieldLeft} to create side labeled field theDatabaseName placeholder text "The name of the DT database" left inset 0 bottom (theTop + 12) total width accViewWidth label text (item 5 of theLabelStrings) field left controlLeft
	set {tagsField, tagsLabel, theTop, fieldLeft} to create side labeled field joinedTags placeholder text "List of newly added tags" left inset 0 bottom (theTop + 12) total width accViewWidth label text (item 6 of theLabelStrings) field left controlLeft
	set {presetsPopup, presetsLabel, theTop} to create labeled popup presetNames left inset 0 bottom (theTop + 12) popup width 435 max width accViewWidth label text (item 7 of theLabelStrings) popup left controlLeft initial choice "no preset"
	
	-- make it possible that the dropdown updates other fields
	-- https://www.macscripter.net/t/change-elements-dynamically-using-shanes-dialog-toolkit/70409/11
	presetsPopup's setTarget:me
	presetsPopup's setAction:"updateOtherFields:"
	
	-- make list of cotronls and pass to display command
	set allControls to {yearlyLocationCheckbox, addedDocYearField, addedDocYearLabel, taxesYearField, taxesYearLabel, companyField, companyLabel, locationPathField, locationPathLabel, databaseField, databaseLabel, tagsField, tagsLabel, presetsPopup, presetsLabel}
	
	-- controlResults will in the same order as allControls
	local buttonName, controlsResults
	set {buttonName, controlsResults} to display enhanced window "Set metadata for file" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons initial position {} giving up after 0 with align
	
	-- gather data from result
	local companyName, updatedTags, databaseName, locationPath, taxYear, locationPath, chosenPreset, yearlyLocation
	set yearlyLocation to (item 1 of controlsResults)
	set addedYear to (item 2 of controlsResults)
	set taxYear to (item 4 of controlsResults)
	set companyName to (item 6 of controlsResults)
	set locationPath to (item 8 of controlsResults)
	set databaseName to (item 10 of controlsResults)
	set updatedTags to split string (item 12 of controlsResults) using delimiters ","
	set chosenPreset to (item 14 of controlsResults)
	
	return {yearlyLocation:yearlyLocation, presetNames:presetNames, chosenPreset:chosenPreset, buttonName:buttonName, addedYear:addedYear, taxYear:taxYear, companyName:companyName, databaseName:databaseName, updatedTags:updatedTags, locationPath:locationPath}
end showDialog

-- update fields by using information from the dropdown
-- requires framework "Foundation" and properties for fields to be updated 
on updateOtherFields:sender
        -- this can be optimised, but for now I'm fine with the implementation
	local defaultLocations
	set defaultLocations to {"", "/Receipts-and-Cancelations", "/Health-and-Insurance/andere", "/Retirement", "/_HOLIDAYS"}
	
	local defaultTags
	set defaultTags to {"", "invoice", "health", "retirement", "holidays"}
	
	local defaultDatabases
	set defaultDatabases to {"", "business", "business", "business", "personal"}
	
	local defaultYearlyLocation
	set defaultYearlyLocation to {false, true, true, true, true}
	
	-- set index of selected dropdown item
	local selectedPresetIndex
	set selectedPresetIndex to (my presetsPopup's indexOfSelectedItem() as integer) + 1
	
	-- get values for selected dropdown
	local selectedLocation, selectedTags, selectedDatabase, selectedYearlyLocation
	set selectedLocation to item selectedPresetIndex of defaultLocations
	set selectedTags to item selectedPresetIndex of defaultTags
	set selectedDatabase to item selectedPresetIndex of defaultDatabases
	set selectedYearlyLocation to item selectedPresetIndex of defaultYearlyLocation
	
	-- set values
	-- https://www.macscripter.net/t/change-elements-dynamically-using-shanes-dialog-toolkit/70409/11
	my (locationPathField's setStringValue:selectedLocation)
	my (tagsField's setStringValue:selectedTags)
	my (databaseField's setStringValue:selectedDatabase)
	my (yearlyLocationCheckbox's setState:selectedYearlyLocation)
end updateOtherFields:

-- rules to set metadata for record based on content, email etc.
on getMetadataForRecord(theRecord, thisYear)
	tell application id "DNtp"
		local theName
		set theName to name of theRecord
		
		local theContent
		set theContent to plain text of theRecord
		
		local theRecordType
		set theRecordType to record type of theRecord
		
		local theKind
		set theKind to kind of theRecord
		
		local theTags
		set theTags to tags of theRecord
		
		local theMetaData
		set theMetaData to meta data of theRecord
		
		local theAuthor
		set theAuthor to missing value
		
		local theMailAddress
		set theMailAddress to missing value
		
		local theSubject
		set theSubject to missing value
		
		if theMetaData is not missing value and theKind is "Email Message" then
			try
				set theAuthor to |kMDItemAuthors| of theMetaData
			on error
				set theAuthor to |kMDItemAuthorEmailAddresses| of theMetaData
			end try
			
			set theMailAddress to |kMDItemAuthorEmailAddresses| of theMetaData
			set theSubject to |kMDItemSubject| of theMetaData
		end if
		
		if theContent contains "Health Report" and theContent contains "Allcome" then
			return {yearlyLocation:false, basePath:"/document-invoices-handle", databaseName:"test-db", addTags:{"health"}, companyName:"Dr. Allcome"}
		end if
		
		if theContent contains "Invoice" and theContent contains "Company: Test Company" then
			return {yearlyLocation:true, basePath:"/document-invoices-handle", databaseName:"test-db", addTags:{"invoice"}, companyName:"Test Company LLC"}
		end if
		
		-- [ ... removed ... ]
		
		return missing value
	end tell
end getMetadataForRecord
1 Like

I’m definitely going to look over this in more detail once I’ve finished my e-mail shenanigans.

I like the idea of being able to review before filing, just have to see how it fits in with the way I generate and import my files.

Sean

Files

This is the latest iteration of the script.

EDIT: Added a fix for selecting database

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.3"
use script "RegexAndStuffLib" version "1.0.7"

property globalSuccessTags : {"auto-processed-invoice-content"}

-- Update UI fields
property fileNameField : {}
property companyField : {}
property locationPathField : {}
property databaseField : {}
property tagsField : {}
property presetsPopup : {}
property yearlyLocationCheckbox : {}
property originalFileNameLabel : {}

property currentYear : year of (current date)

property dialogPresets : {¬
	{presetTitle:"Global: No preset", yearlyLocation:true, basePath:"", databaseName:"", addTags:{}, companyName:""}, ¬
	{presetTitle:"Global: No rule", yearlyLocation:false, basePath:"/_Review/.NORULE", databaseName:"Inbox", addTags:{}, companyName:""}, ¬
	{presetTitle:"Global: Handbücher", yearlyLocation:false, basePath:"/Handbuecher", databaseName:"knowledge", addTags:{"handbuch"}, companyName:""}, ¬
	{presetTitle:"Global: Rente", yearlyLocation:true, basePath:"/Rente", databaseName:"business", addTags:{"rente"}, companyName:""}, ¬
	{presetTitle:"Global: Projekte", yearlyLocation:false, basePath:"/_Projekte/" & currentYear & "/CHANGEME", databaseName:"personal", addTags:{"projekt"}, companyName:""}, ¬
	{presetTitle:"Global: Sonstiges-Behoerden", yearlyLocation:true, basePath:"/Vertraege-und-Rechnungen/_Sonstiges-Behoerden", databaseName:"business", addTags:{"verwaltung", "behoerden"}, companyName:""}, ¬
	{presetTitle:"Global: Sostiges-Gesundheit", yearlyLocation:true, basePath:"/Vertraege-und-Rechnungen/_Sonstiges-Gesundheit", databaseName:"business", addTags:{"health", "gesundheit"}, companyName:""}, ¬
	{presetTitle:"Global: Sonstiges-Kaufbelege", yearlyLocation:true, basePath:"/Vertraege-und-Rechnungen/_Sonstiges-Kaufbelege", databaseName:"business", addTags:{"rechnungen", "invoice"}, companyName:""}, ¬
	{presetTitle:"Global: Tipps", yearlyLocation:false, basePath:"/Tipps/Assets", databaseName:"knowledge", addTags:{"tipp"}, companyName:""}, ¬
	{presetTitle:"Global: Urlaub", yearlyLocation:true, basePath:"/_Urlaub", databaseName:"personal", addTags:{"urlaub", "holidays"}, companyName:""}, ¬
	{presetTitle:"Global: Vorlagen", yearlyLocation:false, basePath:"/_Neu", databaseName:"templates", addTags:{"template"}, companyName:""} ¬
		}
-----------
-- code to run within Script Debugger
-- tell application "MstyStudio" to activate
-- tell application id "DNtp" to activate

tell application id "DNtp" to my performSmartRule(selected records)
-- test scriptOutput
-- tell application id "DNtp" to my scriptOutput(selected record 1, "")
-----------

-- test performSmartRule
property globalIsDev : true

-- copy template for smart rules
on performSmartRule(theRecords)
	local libraryHandler
	set libraryHandler to script "library-handling"
	
	tell application id "DNtp"
		set currentDb to current database
	end tell
	
	libraryHandler's canRun(currentDb, theRecords)
	
	local scriptName
	set scriptName to "document-invoices-handle"
	
	local theScriptHandler
	set theScriptHandler to libraryHandler's loadScript(scriptName, globalIsDev)
	
	theScriptHandler's globalLog("INFO", "Run script", "script=" & quoted form of scriptName)
	
	theScriptHandler's ForAll(theRecords, globalIsDev)
	
	theScriptHandler's globalLog("INFO", "Script finished", "script=" & quoted form of scriptName)
end performSmartRule

-- Run job for all given records
on ForAll(theRecords, localIsDev)
	local titleProgressBar
	set titleProgressBar to "Handle documents"
	
	tell application id "DNtp"
		show progress indicator titleProgressBar steps (count of theRecords) with cancel button
	end tell
	-- https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_xmpls.html
	
	set len to length of theRecords
	set i to 1
	
	my globalLog("INFO", quoted form of "Job started", "count_of_records=" & len)
	
	try
		local theRecord
		repeat with theRecord in theRecords
			tell application id "DNtp"
				if cancelled progress then
					my recordLog(theRecord, "INFO", quoted form of "Cancel current run of script", missing value)
					
					exit repeat
				end if
			end tell
			
			tell application id "DNtp"
				set theFilename to the filename of theRecord
				step progress indicator theFilename
			end tell
			
			my recordLog(theRecord, "INFO", quoted form of "Work on record", "index=" & i & "/" & len)
			
			local modifiedRecord
			set modifiedRecord to my ForOne(theRecord, localIsDev)
			
			if modifiedRecord is not missing value then
				tell application id "DNtp"
					set tags of modifiedRecord to (get tags of modifiedRecord) & globalSuccessTags
				end tell
			end if
			
			set i to i + 1
		end repeat
	on error errMsg number errNum partial result partialError
		
		tell application id "DNtp"
			hide progress indicator
		end tell
		
		-- if errNum = -10000 then
		--  	my globalLog("WARN", quoted form of "Job execution aborted", "error=-10000 count_of_records=" & len)
		--	return
		-- end if
		
		if errNum = 1000 then
			my globalLog("WARN", quoted form of "Job execution aborted by user", "error=1000 count_of_records=" & len)
			return
		end if
		
		set AppleScript's text item delimiters to {return}
		-- An unknown error occurred. Resignal, so the caller
		-- can handle it, or AppleScript can display the number
		
		display alert errMsg & ("Error number: ") & errNum & return & (partialError as text)
		error errMsg & ("Error number: ") & errNum & return & (partialError as text)
	end try
	
	tell application id "DNtp"
		hide progress indicator
	end tell
	
	my globalLog("INFO", quoted form of "Job completed", "count_of_records=" & len)
end ForAll

-- Run job for single record
on ForOne(theRecord, localIsDev)
	local thisYear
	tell application id "DNtp"
		set thisYear to year of (get creation date of theRecord) as string
	end tell
	
	-- repeat for "redo" of a single record
	-- * load rules
	-- * set metadata from rules
	-- * show dialog
	
	local dialogResponse
	set dialogResponse to missing value
	
	-- create window to show record and check it with given metadata
	local theWindow
	tell application id "DNtp"
		set theWindow to open window for record theRecord
	end tell
	
	repeat
		set dialogResponse to my showDialogInLoop(theRecord, dialogResponse, thisYear, localIsDev)
		
		if exitLoop of dialogResponse is true then
			exit repeat
		end if
	end repeat
	
	local theButtonPressed
	set theButtonPressed to buttonPressed of dialogResponse
	
	-- handle buttons
	if theButtonPressed = "Ignore" or theButtonPressed = "Gave up" then
		my recordLog(theRecord, "DEBUG", quoted form of "Button pressed by user", "button=" & theButtonPressed & return & "action=" & quoted form of "Ignore file due to user interaction or time out")
		
		-- if windows has been already closed prevents error
		try
			tell application id "DNtp"
				close theWindow
			end tell
		end try
		
		return missing value
	end if
	
	if theButtonPressed = "Abort" then
		my recordLog(theRecord, "DEBUG", quoted form of "Button pressed by user", "button=" & theButtonPressed & return & "action=" & quoted form of "Stop script interaction")
		
		-- no: close theWindow to make review and handling easier
		error "Abort handling of documents" number 1000
	end if
	
	if theButtonPressed = "Delete" then
		my recordLog(theRecord, "DEBUG", quoted form of "Button pressed by user", "button=" & theButtonPressed & return & "action=" & quoted form of "Move file to trash")
		
		-- if windows has been already closed prevents error
		try
			tell application id "DNtp"
				close theWindow
			end tell
		end try
		
		tell application id "DNtp"
			set deleteAlertResult to display alert "Delete record?" buttons {"Yes", "No"} default button "No" giving up after 0
		end tell
		
		if button returned of deleteAlertResult = "Yes" then
			tell application id "DNtp"
				move record theRecord to trash group of (database of theRecord)
			end tell
		end if
		
		return missing value
	end if
	
	my recordLog(theRecord, "DEBUG", quoted form of "Button pressed by user", "button=" & theButtonPressed & return & "action=" & quoted form of "Set attributes and location of record based on rules")
	
	-- get information about record from dialog
	local parsedDialogResponse
	set parsedDialogResponse to my parseDialogResponse(dialogResponse)
	
	-- get metadata from response
	local updatedTags, existingTags, theDatabaseName, theCompany, locationPath, theButtonPressed
	set updatedTags to (updatedTags of parsedDialogResponse)
	set existingTags to (existingTags of parsedDialogResponse)
	set theDatabaseName to (databaseName of parsedDialogResponse)
	set theCompany to (companyName of parsedDialogResponse)
	set locationPath to (locationPath of parsedDialogResponse)
	set newFilename to (newFilename of parsedDialogResponse)
	
	-- if windows has been already closed prevents error
	try
		tell application id "DNtp"
			close theWindow
		end tell
	end try
	
	-- set name of record	
	local newName
	local oldName
	
	tell application id "DNtp"
		set oldName to name of theRecord
		set newName to newFilename of dialogResponse
		
		set name of theRecord to newName
	end tell
	
	-- create location
	local oldLocation
	tell application id "DNtp"
		set oldLocation to location of theRecord
	end tell
	
	local newLocation
	tell application id "DNtp"
		set newLocation to create location locationPath in database theDatabaseName
	end tell
	
	-- get database from location
	local theDatabase
	tell application id "DNtp"
		set theDatabase to (database of newLocation)
	end tell
	
	-- update tags
	if updatedTags is missing value then
		set updatedTags to {}
	end if
	
	if existingTags is missing value then
		set existingTags to {}
	end if
	
	tell application id "DNtp"
		set tags of theRecord to updatedTags & existingTags
	end tell
	
	-- set company name (custom attribute) from metadata
	if (companyName of parsedDialogResponse) is not missing value then
		tell application id "DNtp"
			add custom meta data companyName of parsedDialogResponse for "company" to theRecord
		end tell
	end if
	
	-- move record to new location
	local movedRecord
	tell application id "DNtp"
		set movedRecord to move record theRecord to newLocation
	end tell
	
	tell application id "DNtp"
		set newLocationName to location with name of newLocation
	end tell
	
	my recordLog(movedRecord, "INFO", quoted form of "Moved and renamed document", "old_location=" & quoted form of oldLocation & return & "new_location=" & quoted form of (newLocationName) & return & "old_name=" & oldName & return & "new_name=" & newName, missing value)
	
	return movedRecord
end ForOne

on showDialogInLoop(theRecord, dialogResponse, thisYear, localIsDev)
	local libraryHandler
	set libraryHandler to script "library-handling"
	
	-- (re)load rules
	local theScriptHandler
	set theScriptHandler to libraryHandler's loadScriptSilent("document-invoices-rules", localIsDev)
	
	tell application id "DNtp"
		local theFilename
		set theFilename to filename of theRecord
	end tell
	
	-- use rules to get metadata about file
	-- default is missing value if no rule matches
	local recordMetadata
	set recordMetadata to theScriptHandler's getMetadataForRecord(theRecord, thisYear)
	-- set recordMetadata to my getMetadataForRecord(theRecord, thisYear)
	
	if recordMetadata is missing value then
		my recordLog(theRecord, "WARN", quoted form of "No matching rule. Ignore file and go on", missing value)
		error "File: " & theFilename & " has no matching rule. I ignore this file and move on if multiple files are selected"
	end if
	
	if (basePath of recordMetadata) is missing value or (databaseName of recordMetadata) is missing value then
		display alert "DNtp" message "basePath or database is missing for the file " & theFilename & ". This is not allowed. Going on with the next file."
		error "basePath or database is missing for the file " & theFilename & ". This is not allowed. Going on with the next file."
	end if
	
	-- show dialog and ask user for "help"
	set dialogResponse to my showDialog(theRecord, recordMetadata, dialogResponse, thisYear)
	
	local theButtonPressed
	set theButtonPressed to buttonPressed of dialogResponse
	
	if (theButtonPressed is not "Redo" and theButtonPressed is not "AI Rename") then
		set exitLoop of dialogResponse to true
		return dialogResponse
	end if
	
	-- nothing to to with theButtonPressed = "Redo"
	
	-- ask model for new name file if AI rename script is asked
	if theButtonPressed = "AI Rename" then
		my recordLog(theRecord, "DEBUG", quoted form of "Button pressed by user", "button=" & theButtonPressed & return & "action=" & quoted form of "Asking AI for new filename")
		
		set exitLoop of dialogResponse to false
		
		local libraryHandler
		set libraryHandler to script "library-handling"
		
		local theScriptHandler
		set theScriptHandler to libraryHandler's loadScriptSilent("document-rename", localIsDev)
		
		local thePrompt
		set thePrompt to globalPrompt of theScriptHandler
		
		local aiRequestTimeout
		set aiRequestTimeout to globalAiRequestTimeout of theScriptHandler
		
		local aiEngine
		tell application id "DNtp"
			set aiEngine to Ollama
		end tell
		
		local newAiGeneratedName
		set aiGeneratedName to theScriptHandler's GetNewNameLoop(theRecord, localIsDev, aiEngine, "phi4", thePrompt, aiRequestTimeout)
		
		set newFilename of dialogResponse to aiGeneratedName
	end if
	
	my recordLog(theRecord, "DEBUG", quoted form of "Button pressed by user", "button=" & theButtonPressed & return & "action=" & quoted form of "Re-evaluate rules")
	
	return dialogResponse
end showDialogInLoop

on parseDialogResponse(dialogResponse)
	local companyName, updatedTags, existingTags, databaseName, locationPath, addedYear, taxYear, locationPath, yearlyLocation
	set addedYear to (addedYear of dialogResponse)
	set taxYear to (taxYear of dialogResponse)
	set newFilename to (newFilename of dialogResponse)
	set companyName to (companyName of dialogResponse)
	set databaseName to (databaseName of dialogResponse)
	set updatedTags to (updatedTags of dialogResponse)
	set existingTags to (existingTags of dialogResponse)
	set locationPath to (locationPath of dialogResponse)
	set yearlyLocation to (yearlyLocation of dialogResponse)
	
	-- if user set year where this document is relevant for tax
	if taxYear is not "" then
		set taxYear to taxYear as number
		set updatedTags to updatedTags & {"taxes-" & taxYear}
	end if
	
	-- make sure we append the year the record was added to the database
	if yearlyLocation is true then
		set locationPath to (locationPath & "/" & addedYear)
	end if
	
	-- "missing value" is correct!
	if newFilename is "missing value" or newFilename is "" or newFilename is missing value then
		error "Missing filename"
	end if
	
	-- "missing value" is correct!
	if locationPath is "missing value" or locationPath is "" or locationPath is missing value then
		error "Undefined individual location for file: Either select a preset with default locations or an individual one"
	end if
	
	-- return gathered metadata
	return {newFilename:newFilename, companyName:companyName, updatedTags:updatedTags, existingTags:existingTags, databaseName:databaseName, locationPath:locationPath}
end parseDialogResponse

on showDialog(theRecord, recordMetadata, previousDialogResponse, thisYear)
	-- gather data
	local originalFilename
	local newFilename
	local existingTags
	
	tell application id "DNtp"
		set originalFilename to (filename of theRecord)
		set existingTags to (tags of theRecord)
	end tell
	
	if previousDialogResponse is missing value then
		tell application id "DNtp"
			set newFilename to (name without extension of theRecord)
		end tell
	else if newFilename of previousDialogResponse is not missing value then
		set newFilename to (newFilename of previousDialogResponse)
	else if newFilename of recordMetadata is not missing value then
		set newFilename to (newFilename of recordMetadata)
	else
		tell application id "DNtp"
			set newFilename to (name without extension of theRecord)
		end tell
	end if
	
	local yearlyLocation
	set yearlyLocation to (yearlyLocation of recordMetadata)
	
	local theDatabaseName
	set theDatabaseName to (databaseName of recordMetadata)
	
	local theCompanyName
	set theCompanyName to (companyName of recordMetadata)
	
	local updatedTags
	set updatedTags to (addTags of recordMetadata)
	
	local theLocationPath
	set theLocationPath to (basePath of recordMetadata)
	
	local taxYear
	set taxYear to (taxYear of recordMetadata)
	
	-- missing value cannot be handled by dialogue
	-- optional
	
	if yearlyLocation is missing value then
		set yearlyLocation to false
	end if
	
	if theCompanyName is missing value then
		set theCompanyName to ""
	end if
	
	if updatedTags is missing value then
		set updatedTags to {}
	end if
	
	if taxYear is missing value then
		set taxYear to ""
	end if
	
	set accViewWidth to 600
	
	-- set buttons
	set {theButtons, minWidth} to create buttons {"Delete", "AI Rename", "Redo", "Abort", "Ignore", "Move"} default button "Ignore" without equal widths
	if minWidth > accViewWidth then set accViewWidth to minWidth -- make sure buttons fit
	
	-- to make it look better, we can get the length of the longest label we will use, and use that to align the controls
	set theLabelStrings to {"Location year", "Tax year", "Company name", "Location", "Database", "New tags", "Existing tags", "Chosen preset", "New filename"}
	set maxLabelWidth to max width for labels theLabelStrings
	set controlLeft to maxLabelWidth + 8
	
	-- set names for the presets used in dropdown
	-- needs to be in the
	local presetNames
	set presetNames to {}
	repeat with preset in dialogPresets
		set end of presetNames to (presetTitle of preset)
	end repeat
	
	-- empty added year to workaround namespace issue
	local addedYear
	set addedYear to ""
	
	if yearlyLocation is true then
		set addedYear to thisYear
	end if
	
	-- space between items
	local spacer
	set spacer to 20
	
	local joinedUpdatedTags
	-- place tags into single input field by joining them into single string
	set joinedUpdatedTags to (join strings updatedTags using delimiter ",")
	
	local joinedExistingTags
	set joinedExistingTags to (join strings existingTags using delimiter ",")
	
	-- make current record the no preset record
	set oldPreset to item 1 of dialogPresets
	set newPreset to {presetTitle:"Global: No preset", yearlyLocation:yearlyLocation, basePath:theLocationPath, databaseName:theDatabaseName, addTags:updatedTags, companyName:theCompanyName}
	set dialogPresets to {newPreset} & (items 2 thru -1 of dialogPresets)
	
	-- get list of known databases from DT
	set sortedListOfDatabases to my getDatabaseList()
	
	-- start from the bottom
	set {taxesYearField, taxesYearLabel, theTop, fieldLeft} to create side labeled field taxYear placeholder text "Tax relevant" left inset (accViewWidth / 2) + spacer bottom 8 total width (accViewWidth / 2) - spacer label text (item 2 of theLabelStrings) field left controlLeft
	
	local addedDocYearField
	set {addedDocYearField, addedDocYearLabel, theTop, fieldLeft} to create side labeled field addedYear placeholder text "Added date" left inset 0 bottom 8 total width (accViewWidth / 2) - spacer label text (item 1 of theLabelStrings) field left controlLeft
	set {yearlyLocationCheckbox, theTop, newWidth} to create checkbox "Create yearly location" left inset controlLeft bottom (theTop + 16) max width accViewWidth initial state yearlyLocation
	set {locationPathField, locationPathLabel, theTop, fieldLeft} to create side labeled field theLocationPath placeholder text "The path to location in DB" left inset 0 bottom (theTop + 16) total width accViewWidth label text (item 4 of theLabelStrings) field left controlLeft
	set {databaseField, databaseLabel, theTop, fieldLeft} to create labeled popup sortedListOfDatabases left inset 0 bottom (theTop + 14) popup width 435 max width accViewWidth label text (item 5 of theLabelStrings) popup left controlLeft initial choice theDatabaseName
	set {companyField, companyLabel, theTop, fieldLeft} to create side labeled field theCompanyName placeholder text "the name of the company" left inset 0 bottom (theTop + 16) total width accViewWidth label text (item 3 of theLabelStrings) field left controlLeft
	
	set {existingTagsField, existingTagsLabel, theTop} to create top labeled field joinedExistingTags placeholder text "List of tags" bottom (theTop + 3) field width accViewWidth - controlLeft label text (item 7 of theLabelStrings) left inset controlLeft extra height 30
	set {tagsField, tagsLabel, theTop} to create top labeled field joinedUpdatedTags placeholder text "List of tags" bottom (theTop + 3) field width accViewWidth - controlLeft label text (item 6 of theLabelStrings) left inset controlLeft extra height 30
	
	set {originalFileNameField, theTop} to create label "Original filename with ext: " & originalFilename bottom theTop + 16 max width (accViewWidth - 100) left inset controlLeft
	set {fileNameField, fileNameLabel, theTop} to create top labeled field newFilename placeholder text "The new filename" bottom (theTop + 3) field width accViewWidth - controlLeft label text (item 9 of theLabelStrings) left inset controlLeft extra height 60
	
	set {presetsPopup, presetsLabel, theTop} to create labeled popup presetNames left inset 0 bottom (theTop + 14) popup width 435 max width accViewWidth label text (item 8 of theLabelStrings) popup left controlLeft initial choice "Global: No preset"
	
	-- make it possible that the dropdown updates other fields
	-- https://www.macscripter.net/t/change-elements-dynamically-using-shanes-dialog-toolkit/70409/11
	presetsPopup's setTarget:me
	presetsPopup's setAction:"updateOtherFields:"
	
	-- make list of cotronls and pass to display command
	set allControls to {yearlyLocationCheckbox, addedDocYearField, addedDocYearLabel, taxesYearField, taxesYearLabel, companyField, companyLabel, locationPathField, locationPathLabel, databaseField, databaseLabel, existingTagsField, existingTagsLabel, tagsField, tagsLabel, originalFileNameField, fileNameField, fileNameLabel, presetsPopup, presetsLabel}
	
	-- controlResults will in the same order as allControls
	local buttonPressed, controlsResults
	set {buttonPressed, controlsResults} to display enhanced window "Set metadata for file" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons initial position {} giving up after 0 with align
	
	-- debugging
	-- my updateOtherFields()
	
	-- gather data from result
	local newFilename, companyName, updatedTags, existingTags, databaseName, locationPath, taxYear, locationPath, chosenPreset, yearlyLocation
	set yearlyLocation to (item 1 of controlsResults)
	set addedYear to (item 2 of controlsResults)
	set taxYear to (item 4 of controlsResults)
	set companyName to (item 6 of controlsResults)
	set locationPath to (item 8 of controlsResults)
	set databaseName to (item 10 of controlsResults)
	set existingTags to split string (item 12 of controlsResults) using delimiters ","
	set updatedTags to split string (item 14 of controlsResults) using delimiters ","
	set newFilename to (item 17 of controlsResults)
	set chosenPreset to (item 19 of controlsResults)
	
	my recordLog(theRecord, "DEBUG", quoted form of "Result of dialog with user", "new_filename=" & newFilename & return & "yearly_location=" & yearlyLocation & return & "chosen_prese=" & chosenPreset & return & "button_pressed=" & buttonPressed & return & "added_year=" & addedYear & return & "tax_year=" & taxYear & return & "company_name=" & companyName & return & "database_name=" & databaseName & return & "existing_tags=" & (join strings existingTags using delimiter ",") & return & "updated_tags=" & (join strings updatedTags using delimiter ",") & return & "location_path=" & locationPath)
	
	return {exitLoop:missing value, newFilename:newFilename, yearlyLocation:yearlyLocation, presetNames:presetNames, chosenPreset:chosenPreset, buttonPressed:buttonPressed, addedYear:addedYear, taxYear:taxYear, companyName:companyName, databaseName:databaseName, updatedTags:updatedTags, existingTags:existingTags, locationPath:locationPath}
end showDialog

-- update fields by using information from the dropdown
-- requires framework "Foundation" and properties for fields to be updated 
-- on updateOtherFields()
on updateOtherFields:sender
	try
		-- set index of selected dropdown item
		local selectedPresetIndex
		set selectedPresetIndex to (my presetsPopup's indexOfSelectedItem() as integer) + 1
		set selectedPreset to item selectedPresetIndex of dialogPresets
		
		-- get values for selected dropdown
		local selectedCompanyName, selectedLocation, selectedTags, joinedTags, selectedDatabase, selectedYearlyLocation
		set selectedCompanyName to (companyName of selectedPreset as text)
		set selectedLocation to (basePath of selectedPreset as text)
		
		local selectedTags
		set selectedTags to (addTags of selectedPreset)
		
		set joinedTags to (join strings selectedTags using delimiter ",")
		
		set selectedDatabase to (databaseName of selectedPreset as text)
		set selectedYearlyLocation to (yearlyLocation of selectedPreset as boolean)
		
		-- set values
		-- https://www.macscripter.net/t/change-elements-dynamically-using-shanes-dialog-toolkit/70409/11
		my (companyField's setStringValue:selectedCompanyName)
		my (locationPathField's setStringValue:selectedLocation)
		my (tagsField's setStringValue:joinedTags)
		my (yearlyLocationCheckbox's setState:selectedYearlyLocation)
		
		local dbList
		set dbList to getDatabaseList()
		
		local dbIndex
		repeat with n from 1 to count of dbList
			if selectedDatabase is (item n of dbList) then
				set dbIndex to n - 1
				exit repeat
			end if
		end repeat
		
		my (databaseField's selectItemAtIndex:dbIndex)
	on error errMsg number errNum partial result partialError
		set AppleScript's text item delimiters to {return}
		-- An unknown error occurred. Resignal, so the caller
		-- can handle it, or AppleScript can display the number.
		display alert errMsg & ("Error number: ") & errNum & return & (partialError as text)
		error errMsg & ("Error number: ") & errNum & return & (partialError as text)
	end try
end updateOtherFields:

on recordLog(theRecord, level, msg, msgInfo)
	if msgInfo is missing value then
		set logMsg to "msg=" & msg
	else
		set logMsg to "msg=" & msg & " " & msgInfo
	end if
	
	if level is not "DEBUG" then
		tell application id "DNtp"
			log message record theRecord info "level=INFO " & logMsg
		end tell
		
		return
	end if
	
	if globalIsDev is true then
		tell application id "DNtp"
			log message record theRecord info "level=DEBUG " & logMsg
		end tell
	end if
end recordLog

on globalLog(level, msg, msgInfo)
	if msgInfo is missing value then
		set logMsg to "msg=" & msg
	else
		set logMsg to "msg=" & msg & " " & msgInfo
	end if
	
	if level is not "DEBUG" then
		tell application id "DNtp"
			log message "level=INFO " & logMsg
		end tell
		
		return
	end if
	
	if globalIsDev is true then
		tell application id "DNtp"
			log message "level=DEBUG " & logMsg
		end tell
	end if
end globalLog

on getDatabaseList()
	local listOfDatabases
	tell application id "DNtp"
		set listOfDatabases to name of every database
	end tell
	
	-- sort list of databases
	-- https://stackoverflow.com/questions/78218289/applescript-how-do-i-sort-a-list-of-sentences-paragraphs-with-an-assigned-numbe
	set array to current application's NSArray's arrayWithArray:listOfDatabases
	set sortedListOfDatabases to (array's sortedArrayUsingSelector:"localizedStandardCompare:") as list
	
	return sortedListOfDatabases
end getDatabaseList

ai-base.scpt

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

property globalIsDev : false

on AskModelTimeout(theRecord, theEngine, theModel, thePrompt, theResultFormat, theTimeout)
	if theResultFormat is missing value then
		set theResultFormat to "JSON"
	end if
	
	if theTimeout is missing value then
		set theTimeout to 60 as integer
	end if
	
	tell application id "DNtp"
		with timeout of theTimeout seconds
			local theResponse
			
			my recordLog(theRecord, "DEBUG", "Get response based on engine and model selection", missing value)
			
			if theModel is missing value then
				set theResponse to get chat response for message thePrompt engine theEngine record theRecord temperature 0 as theResultFormat
			else
				set theResponse to get chat response for message thePrompt engine theEngine model theModel record theRecord temperature 0 as theResultFormat
			end if
			
			return theResponse
		end timeout
	end tell
end AskModelTimeout

library-handling.scpt

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

property globalIsDev : true

on loadScript(scriptName, localIsDev)
	if localIsDev is true then
		display alert "Development Mode enabled"
	end if
	
	set theScriptHandler to my loadScriptSilent(scriptName, localIsDev)
	
	return theScriptHandler
end loadScript

on loadScriptSilent(scriptName, localIsDev)
	local theScriptPath
	set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/" & scriptName & ".scpt"
	
	my globalLog("DEBUG", "Set script path", "path=" & quoted form of theScriptPath)
	
	-- local theScriptHandler
	set theScriptHandler to load script (POSIX file theScriptPath as alias)
	
	if localIsDev is true then
		set theScriptHandler to script scriptName
	end if
	
	return theScriptHandler
end loadScriptSilent

on canRun(theDatabase, theRecords)
	tell application id "DNtp"
		if not (exists theDatabase) then error "No database is in use."
		if the length of theRecords is less than 1 then error "One or more record must be selected."
	end tell
end canRun

on globalLog(level, msg, msgInfo)
	if msgInfo is missing value then
		set logMsg to "msg=" & msg
	else
		set logMsg to "msg=" & msg & " " & msgInfo
	end if
	
	if level is not "DEBUG" then
		tell application id "DNtp"
			log message "level=INFO " & logMsg
		end tell
		
		return
	end if
	
	if globalIsDev is true then
		tell application id "DNtp"
			log message "level=DEBUG " & logMsg
		end tell
	end if
end globalLog

document-invoices-rules.scpt

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

property globalSuccessTags : {}
property globalIsDev : false

-- rules to set metadata for record based on content, email etc.
on getMetadataForRecord(theRecord, thisYear)
	my recordLog(theRecord, "DEBUG", quoted form of "Gathering metadata for evaluating rules", missing value)
	
	tell application id "DNtp"
		local theFilename
		set theFilename to filename of theRecord
		
		local theContent
		set theContent to plain text of theRecord
		
		local theRecordType
		set theRecordType to record type of theRecord
		
		local theKind
		set theKind to kind of theRecord
		
		local theTags
		set theTags to tags of theRecord
		
		local theMetaData
		set theMetaData to meta data of theRecord
		
		local recordsDatabaseName
		set recordsDatabaseName to (name of (database of theRecord))
		
		local recordsLocation
		set recordsLocation to location of theRecord
	end tell
	
	local theAuthor
	set theAuthor to ""
	
	local theMailAddress
	set theMailAddress to ""
	
	local theSubject
	set theSubject to ""
	
	my recordLog(theRecord, "DEBUG", quoted form of "Get current date for evaluating rules", missing value)
	
	set now to current date
	
	-- Get date components
	set currentYear to year of now
	set currentMonth to text -2 thru -1 of ("00" & ((month of now) as integer))
	set currentDay to text -2 thru -1 of ("00" & (day of now))
	set currentDate to currentYear & "-" & currentMonth & "-" & currentDay as string
	
	-- Get time components
	set currentHours to text -2 thru -1 of ("00" & (hours of now))
	set currentMinutes to text -2 thru -1 of ("00" & (minutes of now))
	set currentSeconds to text -2 thru -1 of ("00" & (seconds of now))
	set currentTime to currentHours & ":" & currentMinutes & ":" & currentSeconds
	
	if theMetaData is not missing value and theKind is "Email Message" then
		try
			set theAuthor to |kMDItemAuthors| of theMetaData
		on error
			set theAuthor to |kMDItemAuthorEmailAddresses| of theMetaData
		end try
		
		set theMailAddress to |kMDItemAuthorEmailAddresses| of theMetaData
		set theSubject to |kMDItemSubject| of theMetaData
	end if
	
	-- ##### Global Rules #####
	
	set globalReturn to {taxYear:"", newFilename:theFilename}
	
	if theContent contains "Steuerbescheinigung" then
		set globalReturn to {taxYear:thisYear - 1}
	end if
	
	tell application id "DNtp"
		
		my recordLog(theRecord, "DEBUG", quoted form of "Evaluating test rules", missing value)
		
		-- ##### TEST #####
		
		if theContent contains "Health Report" and theContent contains "Allcome" then
			return globalReturn & {yearlyLocation:false, basePath:"/document-invoices-handle", databaseName:"test-db", addTags:{"health", "gesundheit"}, companyName:"Dr. Allcome"}
		end if
		
		if theContent contains "Invoice" and theContent contains "Company: Test Company" then
			return globalReturn & {yearlyLocation:true, basePath:"/document-invoices-handle", databaseName:"test-db", addTags:{"invoice", "rechnung"}, companyName:"Test 123 Company LLC"}
		end if
		
		if theContent contains "My Redo Record" then
			-- change me: change company during rule run to evaluate redo functionality
			return globalReturn & {yearlyLocation:true, basePath:"/document-invoices-handle", databaseName:"test-db", addTags:{"invoice", "rechnung"}, companyName:"YYYY Company LLC"}
		end if
		
		-- ##### Rules #####
		
		my recordLog(theRecord, "DEBUG", quoted form of "Evaluating normal rules", missing value)
		
		-- [...]
		
		--- any any
		
		my recordLog(theRecord, "DEBUG", quoted form of "No rules match. Setting default values for missing rules", missing value)
		
		return globalReturn & {yearlyLocation:false, basePath:recordsLocation & ".NORULE", databaseName:recordsDatabaseName, addTags:{}, companyName:""}
		-- return missing value
	end tell
end getMetadataForRecord

on recordLog(theRecord, level, msg, msgInfo)
	if msgInfo is missing value then
		set logMsg to "msg=" & msg
	else
		set logMsg to "msg=" & msg & " " & msgInfo
	end if
	
	if level is not "DEBUG" then
		tell application id "DNtp"
			log message record theRecord info "level=INFO " & logMsg
		end tell
		
		return
	end if
	
	if globalIsDev is true then
		tell application id "DNtp"
			log message record theRecord info "level=DEBUG " & logMsg
		end tell
	end if
end recordLog

on globalLog(level, msg, msgInfo)
	if msgInfo is missing value then
		set logMsg to "msg=" & msg
	else
		set logMsg to "msg=" & msg & " " & msgInfo
	end if
	
	if level is not "DEBUG" then
		tell application id "DNtp"
			log message "level=INFO " & logMsg
		end tell
		
		return
	end if
	
	if globalIsDev is true then
		tell application id "DNtp"
			log message "level=DEBUG " & logMsg
		end tell
	end if
end globalLog