Legato is great at supplementing features that GoFiler already has and hooking into existing functions and modifying how they work slightly. Probably its most powerful ability, however, is to add entirely new functionality to GoFiler. One feature GoFiler doesn’t have natively is the ability to have multiple currency types for a single fact in XBRL. Well, we can add support for that using Legato! The new functionality will be achieved by combining two different XFR files.
Friday, April 21. 2017
LDC #31: XBRL Merger, Part 1
To get started let’s assume that we have a completed XFR file that needs multiple currencies for one or more facts. Make sure all the facts are the same currency and follow the steps below:
1) Save a new copy of the file with an alternate name, preferably using the currency in the name. So if the file is “MyFancy.xfr”, the user could save a new file with the name “MyFancyCAD.xfr”.
2) Edit the facts into the alternate currencies in the newly saved file to use the new currency instead of the original’s currency and change the values as appropriate.
3) Run the script, which will export both files, then combines the instance files into a single document.
4) Proof and/or file the merged XBRL file set.
This will be the most complicated script we have done in this blog to this point, so it is going to be broken up through multiple weeks. This first post will cover the relatively minor user interface (UI) and validation of user input.
Our script so far consists of two files: the resource file that represents the dialog and the merger script. The merger script looks like this:
#include "XBRLMerger.rc" string edit_windows[][]; handle FileOneWindow; handle FileTwoWindow; int run (int f_id, string mode); int validate_file (string file, string display); /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Common */ item["Class"] = "XBRLExtension"; /* Function Code */ /* o Define Function */ item["Code"] = "XBRL_MERGE"; /* Function Code */ item["MenuText"] = "&Merge XFR Files"; /* Menu Text */ item["Description"] = "<B>Merge XBRL Files</B>"; /* description */ item["Description"].= "\r\rMerges two XBRL Instance Files."; /* description */ /* o Check for Existing */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* o Registration */ rc = MenuAddFunction(item); /* Add the item */ if (IsError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook(item["Code"], fnScript, "xbrl_merge"); /* Set the Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int run(f_id,mode){ /* main run loop */ /****************************************/ int rc; /* result */ /* */ if (mode!="preprocess"){ /* if not in preprocess */ return ERROR_NONE; /* return no error */ } /* */ edit_windows = EnumerateEditWindows(); /* get open edit windows */ rc = DialogBox("MergeXBRLDlg", "merge_"); /* open selector dialog */ CloseHandle(FileOneWindow); /* close window handle */ CloseHandle(FileTwoWindow); /* close window handle */ return ERROR_NONE; } /****************************************/ int main(){ /* main function */ /****************************************/ string s1; /* General */ /* */ s1 = GetScriptParent(); /* Get the parent */ if (s1 == "LegatoIDE") { /* Is run from the IDE (debug) */ setup(); /* run setup */ run(0,"preprocess"); /* run as though hooked */ } /* end IDE run */ return ERROR_NONE; } /* */ /****************************************/ int merge_load(){ /* Setup Action */ /****************************************/ string file_one,file_two; /* old file paths */ /* */ file_one = GetSetting("XBRLMerge","File One"); /* get path to file one */ file_two = GetSetting("XBRLMerge","File Two"); /* get path to file two */ /* */ EditSetText(XBRL_ONE_TEXT,file_one); /* set edit text */ EditSetText(XBRL_TWO_TEXT,file_two); /* set edit text */ return ERROR_NONE; } /****************************************/ int merge_action(int c_id, int c_ac) { /* Control Action */ /****************************************/ string s1; /* General */ /* */ /* ** Control Actions */ /* * Browse for XML 1 */ if (c_id == XBRL_RESET){ /* if resetting */ EditSetText(XBRL_ONE_TEXT,""); /* reset text of box */ PutSetting("XBRLMerge","File One",""); /* reset setting file */ EditSetText(XBRL_TWO_TEXT,""); /* reset text of box */ PutSetting("XBRLMerge","File Two",""); /* reset setting file */ } /* */ /* */ if (c_id == XBRL_ONE_BROWSE) { /* Control ID (button) */ s1 = EditGetText(XBRL_ONE_TEXT); /* Get the current path */ s1 = BrowseOpenFile("Select First XBRL File","*.xfr|*.xfr", s1); /* Browse for the folder */ if (s1 != "") { /* Returned a value (OK) */ EditSetText(XBRL_ONE_TEXT, s1); /* Get the current path */ PutSetting("XBRLMerge","File One",s1); /* store setting for later */ } /* end has string */ return ERROR_NONE; /* Done */ } /* end browse */ /* * Browse for XML 2 */ if (c_id == XBRL_TWO_BROWSE) { /* Control ID (button) */ s1 = EditGetText(XBRL_TWO_TEXT); /* Get the current path */ if (s1 == "") { /* Empty, pick up source */ s1 = EditGetText(XBRL_ONE_TEXT); /* Get the current path */ } /* end has string */ s1 = BrowseOpenFile("Select Second XBRL File","*.xfr|*.xfr",s1); /* Browse for the folder */ if (s1 != "") { /* Returned a value (OK) */ EditSetText(XML_TWO_TEXT, s1); /* Get the current path */ PutSetting("XBRLMerge","File Two",s1); /* store setting for later */ } /* end has string */ return ERROR_NONE; /* Done */ } /* end browse */ return ERROR_NONE; /* Exit no error */ } /* end routine */ /****************************************/ int merge_validate(){ /* Validate Action */ /****************************************/ int valid_one,valid_two; /* validations of files */ string file_one,file_two; /* file paths */ /* */ file_one = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ file_two = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ if (file_one == "" || file_two == ""){ /* if either file is blank */ MessageBox('x',"Two files must be selected."); /* make sure two files are selected */ return ERROR_EXIT; /* exit with error */ } /* */ if (file_one == file_two){ /* test if same file */ MessageBox('x',"You cannot merge a file into itself."); /* display error message */ return ERROR_EXIT; /* return with error */ } valid_one = validate_file(file_one,"One"); /* validate file_one */ if (IsWindowHandleValid(FileTwoWindow)){ /* if our validate set a file handle */ FileOneWindow = FileTwoWindow; /* store handle as file one */ FileTwoWindow = NULL_HANDLE; /* close file two window */ } /* */ valid_two = validate_file(file_two,"Two"); /* validate file_two */ if (valid_one==valid_two && valid_two==ERROR_NONE){ /* if both validates returned ERROR_NONE*/ return ERROR_NONE; /* return no error */ } /* */ return ERROR_EXIT; /* exit with an error */ } /****************************************/ int validate_file(string file, string display, handle file_handle){ /* validate an individual file */ /****************************************/ int rc; /* return code from our file */ int depth; /* number of windows open */ int ix; /* array index counter */ /* */ depth = ArrayGetAxisDepth(edit_windows); /* get number of windows open */ if (IsFile(file)){ /* check if file one is a file */ if (CanAccessFile(file,FO_WRITE)){ /* check if we can write to the file */ if (MakeLowerCase(GetExtension(file))==".xfr"){ /* make sure file ends in .xfr */ return ERROR_NONE; /* return no error */ } /* */ else{ /* if file doesn't end in .xfr */ MessageBox('x',"File "+display+" Must be an XFR file."); /* display error message */ } /* */ } /* */ else{ /* if we cannot write to file */ for (ix = 0; ix<depth; ix++){ /* scan all open windows */ if (edit_windows[ix]["Filename"] == file){ /* if the file is already open */ FileTwoWindow=MakeHandle(edit_windows[ix]["ClientHandle"]); /* get the handle to it */ rc = GetLastError(); /* check if we got an error */ if (IsError(rc)){ /* if we have an error */ MessageBox('x',"Cannot create handle, error %0x",rc); /* display error message */ } /* */ else{ /* if we don't have an error */ return ERROR_NONE; /* the file is already open, return */ } /* */ } /* */ } /* */ MessageBox('x',"Cannot open File "+display); /* give error message */ } /* */ } /* */ else{ /* if we cannot open file one */ MessageBox('x',"File "+display+" does not exist."); /* give error message */ } /* if we haven't exited yet */ return ERROR_EXIT; /* return an error */ } /****************************************/ int merge_ok(){ /* OK Action */ /****************************************/ string file_one,file_two; /* file paths */ /* */ file_one = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ file_two = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ /* */ if (IsWindowHandleValid(FileOneWindow)){ /* check if file is already open */ MessageBox("File One is already open."); /* display message */ } /* */ if (IsWindowHandleValid(FileTwoWindow)){ /* check if file is already open */ MessageBox("File Two is already open."); /* display message */ } /* */ /* */ return ERROR_NONE; // *.* TODO - Add logic to open files, compare files, merge files. }
The resource file referenced in the above script looks like:
// // MERGE XBRL FILES // ------------------- // #define XBRL_RESET 101 #define IDC_BUTTON1 101 #define XBRL_ONE_BROWSE 202 #define XBRL_TWO_BROWSE 204 #define XBRL_TWO_TEXT 203 #define XBRL_ONE_TEXT 201 #define XML_ONE_TEXT 201 #define XML_ONE_BROWSE 202 #define XML_TWO_TEXT 203 #define XML_TWO_BROWSE 204 #define DEST_FN 205 MergeXBRLDlg DIALOGEX 0, 0, 270, 142 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Merge XBRL Instance Files" FONT 8, "MS Sans Serif" { CONTROL "Locations:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 69, 38, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 44, 73, 222, 1, 0 CONTROL "&First XFR File:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 83, 48, 8, 0 CONTROL "", XBRL_ONE_TEXT, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 70, 81, 170, 12, 0 CONTROL "...", XBRL_ONE_BROWSE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 248, 81, 15, 12, 0 CONTROL "&Second XFR File:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 99, 55, 8, 0 CONTROL "", XBRL_TWO_TEXT, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 70, 97, 170, 12, 0 CONTROL "...", XBRL_TWO_BROWSE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 248, 97, 15, 12, 0 CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 160, 117, 50, 14 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 216, 117, 50, 14 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 137, 260, 1, 0 CONTROL "Instructions:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 5, 38, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 44, 10, 222, 1, 0 CONTROL "This function is intended to be used to merge 2 XFR files to allow for multiple unit types for a single fact. For best results, complete the XBRL report in a single unit type, make a copy of it, erase all facts from the copy, and enter facts in as a new unit type. Combine the XFR files from the old and new report, and file it to the SEC.", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 20, 240, 50, 0 CONTROL "Reset", XBRL_RESET, "button", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 6, 117, 50, 14, 0 }
We haven’t really talked much about resource files, but they are a standard way of writing dialog controls in Windows. For more complete documentation on exactly how they work, check out Microsoft’s SDK page here. The resource file, when actually called, produces a dialog that should look like this:
Our script will handle what happens when buttons are pressed on the dialog. First, our merger script makes its declarations and global variables. The only import is going to be “XBRLMerger.rc”. This is the resource file that controls our user input dialog. For functions, our script will use the standard setup, run, and main functions we’ve talked about before. In addition to them, though, we have the validate_file function, which will take a file path and display name as parameters and make sure the file exists, that we can access it, and check if it’s already open. In addition to this, we will have several functions with the prefix “merge_”, which are called automatically by our dialog at specific points. They do not need to be declared at the top with the other functions. We’ll talk about this again in a bit. Our global variables will be edit_windows, a two-dimensional string array that will store information about currently open edit windows, and FileOneWindow and FileTwoWindow, which store handles to the files we are going to open and combine.
#include "XBRLMerger.rc" string edit_windows[][]; handle FileOneWindow; handle FileTwoWindow; int run (int f_id, string mode); int validate_file (string file, string display);
Our setup function this time is pretty much identical to most of what we’ve seen before. The main difference is that in the array item, we are defining an index for “Class” as well. By specifying this is an XBRLExtension class, the menu option will appear under the tools area of the XBRL toolbar instead of the tools area of the File toolbar in GoFiler.
/****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Common */ item["Class"] = "XBRLExtension"; /* Function Code */ /* o Define Function */ item["Code"] = "XBRL_MERGE"; /* Function Code */ item["MenuText"] = "&Merge XFR Files"; /* Menu Text */ item["Description"] = "<B>Merge XBRL Files</B>"; /* description */ item["Description"].= "\r\rMerges two XBRL Instance Files."; /* description */ /* o Check for Existing */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* o Registration */ rc = MenuAddFunction(item); /* Add the item */ if (IsError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook(item["Code"], fnScript, "xbrl_merge"); /* Set the Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */
Our run function is actually very small here because most of the execution occurs in the specific handler functions for dialog actions. All the run function has to do is determine if we are running as preprocess, and if so, then enumerate the edit windows to fill our array edit_windows. Once we have that, we can use the DialogBox function to open our dialog UI. This function takes two parameters: the name of the dialog in the resource file and the prefix we want to use for our automatically called UI functions. In this case, we will use the prefix “merge_” as mentioned previously. This means we will have to define other functions below like the “merge_load” function to handle how the UI loads or the “merge_ok” function to handle what happens when the user presses the OK button. Now our specific dialog event handlers are hooked into our dialog.
Once our dialog is called, this function blocks execution of the rest of the run function until the dialog is ended, so afterwards all we need to do is close our handles to our open files.
/****************************************/ int run(f_id,mode){ /* main run loop */ /****************************************/ int rc; /* result */ /* */ if (mode!="preprocess"){ /* if not in preprocess */ return ERROR_NONE; /* return no error */ } /* */ edit_windows = EnumerateEditWindows(); /* get open edit windows */ rc = DialogBox("MergeXBRLDlg", "merge_"); /* open selector dialog */ CloseHandle(FileOneWindow); /* close window handle */ CloseHandle(FileTwoWindow); /* close window handle */ }
The main function is very basic, but this time we’re going to check if the current script is running in our development IDE. If so, we’re going to run the setup function and then run our run function. This will let us do debugging in our development environment instead of having to run our script from the menu hook every time, where getting debug information is more difficult.
/****************************************/ int main(){ /* main function */ /****************************************/ string s1; /* General */ /* */ s1 = GetScriptParent(); /* Get the parent */ if (s1 == "LegatoIDE") { /* Is run from the IDE (debug) */ setup(); /* run setup */ run(0,"preprocess"); /* run as though hooked */ } /* end IDE run */ } /* */
So now that we have our basic functions defined, we can actually start writing handlers for UI actions. The first one, merge_load, will handle loading information that the dialog will need to display properly to the user. For this script, the GetSetting function is used to get the last used file_one and file_two variables. Then the EditSetText function is used to set their values into the file selection boxes on our UI Dialog.
/****************************************/ int merge_load(){ /* Setup Action */ /****************************************/ string file_one,file_two; /* old file paths */ /* */ file_one = GetSetting("XBRLMerge","File One"); /* get path to file one */ file_two = GetSetting("XBRLMerge","File Two"); /* get path to file two */ /* */ EditSetText(XBRL_ONE_TEXT,file_one); /* set edit text */ EditSetText(XBRL_TWO_TEXT,file_two); /* set edit text */ }
Next, the merge_action function is defined. This function is called whenever a user interacts with the UI. So within it, we can define specific handlers for every control by checking the c_id parameter. If this parameter matches up with a specific control, we can tell the user has interacted with that control or in our case, pressed a specific button. First, we test for the XBRL_RESET button. This button, when pressed, triggers the EditSetText and function for both of our inputs to set them back to blank strings and runs the PutSetting function to reset our stored values for file_one and file_two back to blank strings as well. These settings are stored in the first place because when making changes to a file, there may be a reason to merge the same files repeatedly, but if you want to start a new job, a reset button is handy.
/****************************************/ int merge_action(int c_id, int c_ac) { /* Control Action */ /****************************************/ string s1; /* General */ /* */ /* ** Control Actions */ /* * Browse for XML 1 */ if (c_id == XBRL_RESET){ /* if resetting */ EditSetText(XBRL_ONE_TEXT,""); /* reset text of box */ PutSetting("XBRLMerge","File One",""); /* reset setting file */ EditSetText(XBRL_TWO_TEXT,""); /* reset text of box */ PutSetting("XBRLMerge","File Two",""); /* reset setting file */ } /* */
After that, we can test for the XBRL_ONE_BROWSE button. If the user presses this button, we need to first use the EditGetText function to get the path of the first file that was selected in case a path already exists there. If the path exists, we pass it to our BrowseOpenFile function, which prompts the user to pick a file with the “.xfr” file extension. If the user actually chooses a path, we can then use the EditSetText function to change the text field to show the path and use the PutSetting function to store the location of the selected file for later.
if (c_id == XBRL_ONE_BROWSE) { /* Control ID (button) */ s1 = EditGetText(XBRL_ONE_TEXT); /* Get the current path */ s1 = BrowseOpenFile("Select First XBRL File","*.xfr|*.xfr", s1); /* Browse for the folder */ if (s1 != "") { /* Returned a value (OK) */ EditSetText(XBRL_ONE_TEXT, s1); /* Get the current path */ PutSetting("XBRLMerge","File One",s1); /* store setting for later */ } /* end has string */ return ERROR_NONE; /* Done */ } /* end browse */
Testing for the XBRL_TWO_BROWSE button is basically the exact same thing as above. However, if the user hasn’t entered a path for the second file, we can take the path from the first file to use as a base. This makes it so that the BrowseOpenFile function will open up to the same folder that the first file is in, which is good and more convenient because it’s likely that the first file and the second file are in the same folder. This finishes off the merge_action function.
/* * Browse for XML 2 */ if (c_id == XBRL_TWO_BROWSE) { /* Control ID (button) */ s1 = EditGetText(XBRL_TWO_TEXT); /* Get the current path */ if (s1 == "") { /* Empty, pick up source */ s1 = EditGetText(XBRL_ONE_TEXT); /* Get the current path */ } /* end has string */ s1 = BrowseOpenFile("Select Second XBRL File","*.xfr|*.xfr",s1); /* Browse for the folder */ if (s1 != "") { /* Returned a value (OK) */ EditSetText(XML_TWO_TEXT, s1); /* Get the current path */ PutSetting("XBRLMerge","File Two",s1); /* store setting for later */ } /* end has string */ return ERROR_NONE; /* Done */ } /* end browse */ return ERROR_NONE; /* Exit no error */ } /* end routine */
The merge_validate function is called by the UI after the OK button is pressed to handle the event. If this function returns ERROR_EXIT, the UI does nothing. If it returns ERROR_NONE, the UI continues onto the function merge_ok. First, we need to get the values for our two selected files using the EditGetText functions. Then, we test if file_one or file_two are blank. If so, we display an error message and then return with an error. We also need to check if file_one is the same as file_two. If so, again we display an error and return with an error.
If we’re still going, we can run our validate_file function on file_one. The function will be explained more next, but it pretty much just makes sure we can actually work with this file, and if the file is already open, it sets the value of our global FileTwoWindow variable to the handle of the file. Because we’re concerned about file_one still, after we validate file_one, we can check the value of the handle FileTwoWindow using the IsWindowHandleValid function. If this function returns true, it means file_one is open already, and we have a handle to it. So then we need to store that handle in our global FileOneWindow, and reset the handle of FileTwoWindow by setting it to NULL_HANDLE. Then we can call the validate_file function on file_two to make sure it’s valid. If the responses from our validate_file function equal each other, and at least one of them is ERROR_NONE, we can assume both are ERROR_NONE, and then return ERROR_NONE. Otherwise, we need to return ERROR_EXIT, and let the user fix whatever errors occurred.
/****************************************/ int merge_validate(){ /* Validate Action */ /****************************************/ int valid_one,valid_two; /* validations of files */ string file_one,file_two; /* file paths */ /* */ file_one = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ file_two = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ if (file_one == "" || file_two == ""){ /* if either file is blank */ MessageBox('x',"Two files must be selected."); /* make sure two files are selected */ return ERROR_EXIT; /* exit with error */ } /* */ if (file_one == file_two){ /* test if same file */ MessageBox('x',"You cannot merge a file into itself."); /* display error message */ return ERROR_EXIT; /* return with error */ } valid_one = validate_file(file_one,"One"); /* validate file_one */ if (IsWindowHandleValid(FileTwoWindow)){ /* if our validate set a file handle */ FileOneWindow = FileTwoWindow; /* store handle as file one */ FileTwoWindow = NULL_HANDLE; /* close file two window */ } /* */ valid_two = validate_file(file_two,"Two"); /* validate file_two */ if (valid_one==valid_two && valid_two==ERROR_NONE){ /* if both validates returned ERROR_NONE*/ return ERROR_NONE; /* return no error */ } /* */ return ERROR_EXIT; /* exit with an error */ }
The validate_file function is responsible for checking to make sure that the file chosen exists, is a “.xfr” file, and if it’s not open in GoFiler, it ensures it actually can be opened. This is very important, because we don’t want to assume these things without checking first. Firstly, we can check the number of open edit windows and store that for later. Then, we check if the user input was actually a file or not with the function IsFile. If not, we need to display an error message. If it is a file, we can test if we can access it by using the CanAccessFile function. Read/Write access is required to the file, so we use the parameter FO_WRITE to check the correct access level. If this function returns true, then we can use the MakeLowerCase and GetExtension functions to make sure the selected file is an “.xfr” file. If not, we can display an error message. If all of that is true, we can return right now without error because this is a valid “.xfr” file that GoFiler has the ability to open.
/****************************************/ int validate_file(string file, string display, handle file_handle){ /* validate an individual file */ /****************************************/ int rc; /* return code from our file */ int depth; /* number of windows open */ int ix; /* array index counter */ /* */ depth = ArrayGetAxisDepth(edit_windows); /* get number of windows open */ if (IsFile(file)){ /* check if file one is a file */ if (CanAccessFile(file,FO_WRITE)){ /* check if we can write to the file */ if (MakeLowerCase(GetExtension(file))==".xfr"){ /* make sure file ends in .xfr */ return ERROR_NONE; /* return no error */ } /* */ else{ /* if file doesn't end in .xfr */ MessageBox('x',"File "+display+" Must be an XFR file."); /* display error message */ } /* */ } /* */
If the function CanAccessFile returns false, it’s possible that the file is already open in GoFiler, so we need to check that. To do that, we have a for loop that iterates over the open edit windows. For each edit window, we can check the “Filename” column of the table, and if it matches the filename of the file we’re testing, it means that the file is an open tab in GoFiler. Thus we have access to the file. Then we can store the handle to that window by using the MakeHandle function on the “ClientHandle” index for that row of our edit windows table. Next, we need to get the last error to ensure our handle was actually created. If not, then we can display an error message. If it was created, we can return ERROR_NONE right here. If we exit the for loop without returning, then we can display an error message that we cannot open the file. This function is designed to return ERROR_NONE if it’s valid, so if we’re still running this function at the end and we haven’t returned yet, we can assume there was a problem, so we can return ERROR_EXIT.
else{ /* if we cannot write to file */ for (ix = 0; ix<depth; ix++){ /* scan all open windows */ if (edit_windows[ix]["Filename"] == file){ /* if the file is already open */ FileTwoWindow=MakeHandle(edit_windows[ix]["ClientHandle"]); /* get the handle to it */ rc = GetLastError(); /* check if we got an error */ if (IsError(rc)){ /* if we have an error */ MessageBox('x',"Cannot create handle, error %0x",rc); /* display error message */ } /* */ else{ /* if we don't have an error */ return ERROR_NONE; /* the file is already open, return */ } /* */ } /* */ } /* */ MessageBox('x',"Cannot open File "+display); /* give error message */ } /* */ } /* */ else{ /* if we cannot open file one */ MessageBox('x',"File "+display+" does not exist."); /* give error message */ } /* if we haven't exited yet */ return ERROR_EXIT; /* return an error */ }
The last function is merge_ok. This function is called after the merge_validate if the validation passed. For now, it gets the file paths from the UI using the EditGetText functions. Then it tests the FileOneWindow and FileTwoWindow variables to see if those files are already open. If so, it just prints out a message to the user for now. This function will be modified next week.
/****************************************/ int merge_ok(){ /* OK Action */ /****************************************/ string file_one,file_two; /* file paths */ /* */ file_one = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ file_two = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ /* */ if (IsWindowHandleValid(FileOneWindow)){ /* check if file is already open */ MessageBox("File One is already open."); /* display message */ } /* */ if (IsWindowHandleValid(FileTwoWindow)){ /* check if file is already open */ MessageBox("File Two is already open."); /* display message */ } /* */ /* */ // *.* TODO - Add logic to open files, compare files, merge files. }
This is intended to be a base point for our script; from here we can add in the rest of our functionality. Next week, we will look at expanding this script to open the files, export them to XBRL filesets, and test to make sure we can actually merge the two files.
Steven Horowitz has been working for Novaworks for over five years as a technical expert with a focus on EDGAR HTML and XBRL. Since the creation of the Legato language in 2015, Steven has been developing scripts to improve the GoFiler user experience. He is currently working toward a Bachelor of Sciences in Software Engineering at RIT and MCC. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato