This blog will be featured in an upcoming presentation at the GPUG Summit Conference at Orlando this October with David Musgrave. The prior blog on this topic was quite functional, but David suggested that I follow more closely the coding standards preferred by many programmers, which may differ from the older techniques in the books. Rather than replace the blog, I decided to reissue it and discuss the changes so everyone can learn something new.
We will jump into the start of our prior blog:
In this simple Dexterity modification, we add the Purchase Order Number and the first associated POP Receipt Number to the Accounts Payable window. Also, if the user presses the ZOOM buttons for these fields, they open the associated inquiry windows.
ADDING ZOOM BUTTONS TO ADDITIONAL INQUIRY WINDOWS
You can use these same techniques to add the vendor/customer ID and document numbers to the General Ledger detail inquiry windows, and also drill down to the original PM/RM transactions.
We will select the PM_Transaction_Inquiry form and then open the main window in Dexterity:
This is what this window looks like under the Windows -> Preview menu:
We are going to add another column for POP Receipt Number and PO Number.
We want to make the new fields wide as the ORIGINAL AMOUNT field, we click on that field, and select the LAYOUT -> PROPERTIES menu option, and we see the SIZE-WIDTH is 137. We can drag the fields to the right, and grab the end of lines at the top and bottom of the window and stretch them. We open the LAYOUT -> TOOLBOX, click on the “A” icon, type “POP Receipt Number” and “PO Number”. We highlight each of these fields, and under LAYOUT -> PROPERTIES we click ZOOM to true. This property really only changes the display.
The actual zooming script is on the ZOOM BUTTONS, since this is an array field, you drag the next element in the array onto the window. The VISIBLE property of the ZOOM BUTTONS should be false.
Note: I am adding the Zoom2 and Zoom3 buttons here, it would have been safest to create a custom Zoom field array. If a future upgrade of Dynamics GP uses these fields you will have an exception to deal with. This is unlikely, but possible.
Next you double click on the scrolling window and add the POP INVOICE NUMBER and PO NUMBER fields. You would then resize them and lengthen the lines in the scrolling window. Since this is an inquiry window, it doesn’t matter whether the fields are editable or not, the whole window is marked as “BROWSE ONLY.”
CAUTION: NEVER, NEVER, NEVER delete fields on windows that have logic attached to them. Not only will you destroy the logic of the original code and create error messages when the Great Plains code tries to run, you will probably run into major problems when you need to update your code to the next version of Dynamics when you upgrade. Instead, move the fields you want to hide off the window to the side, and change their VISIBLE and EDIT flags to false.
Some user interface stuff: you should lengthen the two lines in the body of the scrolling window, and add a line to the left of the two new fields, so the scrolling window looks right.
SOME SUGGESTIONS FROM DAVID MUSGRAVE
Appearances matter a great deal, having misspelled words and sloppiness in window design can harm the boss’ confidence in your work. So try to move the new fields around with the arrow keys so they mate properly with their neighbors.
David’s additional suggestions:
When dropping fields into scrolling windows, set Border=False
When drawing lines into scrolling windows, set Appearance=2D Border
When adding fields to windows watch for double thick lines where fields join. Use Shift-Right arrow to make field one pixel wider. Needed for Header of Scrolling window.
Note: there is a little bit of space to the right of the new fields in the scrolling window, this is to avoid the problem where sometimes the display hides the pennies. David Musgrave’s GP Power Tools solves this problem for all windows in Dynamics GP.
An older technique was to have the ZOOM button scripts run a script on a boolean field in the scrolling window that popped up the additional inquiry window (see my prior blog.) David had two suggestions:
1. You could attach the script directly to the ZOOM buttons, but it is more elegant to write a trigger script that is fired when the ZOOM button is pressed.
2. You do not need any intermediate steps, the ZOOM button trigger scripts can open the inquiry windows directly.
If you can avoid attaching your scripts to your new fields and call them with trigger scripts, then all of your custom code will be in one place, and you will never have to search for the custom code.
We are done with the user interface, now we need to write some Dexterity scripts.
DEXTERITY SCRIPTS THAT POPULATE THE NEW FIELDS
First, we will write the trigger scripts that populate the POP Invoice Number and PO Number fields. We like to re-use our scripts, so we will first write the subroutines that grabs the data we need. We will create a new function:
We will forgo the explanation of these scripts, the manual can tell you what these commands mean, but they are fairly self explanatory. The pragma commands decrease the number of bogus compile messages.
{ procedure GetFirstPOPInvoiceFromHistory }
function returns string strPOPInvoiceNumber;
in string strVendorID;
in string strDocumentNumber;
pragma(disable warning LiteralStringUsed);
clear field strPOPInvoiceNumber;
range clear table POP_ReceiptHist;
‘Vendor ID’ of table POP_ReceiptHist = strVendorID;
‘Vendor Document Number’ of table POP_ReceiptHist = strDocumentNumber;
clear field ‘POP Receipt Number’ of table POP_ReceiptHist;
range start table POP_ReceiptHist by number 2;
‘Vendor ID’ of table POP_ReceiptHist = strVendorID;
‘Vendor Document Number’ of table POP_ReceiptHist = strDocumentNumber;
fill ‘POP Receipt Number’ of table POP_ReceiptHist;
range end table POP_ReceiptHist by number 2;
get first table POP_ReceiptHist by number 2;
if err() = OKAY then
strPOPInvoiceNumber = ‘POP Receipt Number’ of table POP_ReceiptHist;
elseif err() <> MISSING or err() <> EOF then
call ErrorMessage,_script_,technicalname(table POP_ReceiptHist),err(),
“Finding VendorID/DocNo “+strVendorID+”/”+strDocumentNumber;
end if;
range clear table POP_ReceiptHist;
pragma(enable warning LiteralStringUsed);
{ procedure GetFirstPOFromPOPInvoice }
function returns string strPONumber;
in string strPOPInvoice;
local string strTable;
pragma(disable warning LiteralStringUsed);
strTable = technicalname(table POP_ReceiptLine);
range clear table POP_ReceiptLine;
‘POP Receipt Number’ of table POP_ReceiptLine = strPOPInvoice;
clear field ‘Receipt Line Number’ of table POP_ReceiptLine;
range start table POP_ReceiptLine by number 1;
‘POP Receipt Number’ of table POP_ReceiptLine = strPOPInvoice;
fill ‘Receipt Line Number’ of table POP_ReceiptLine;
range end table POP_ReceiptLine by number 1;
get first table POP_ReceiptLine by number 1;
if err() = OKAY then
strPONumber = ‘PO Number’ of table POP_ReceiptLine;
elseif err() = MISSING then
call ErrorMessage,_script_,strTable,err(),”MISSING “+strPOPInvoice;
elseif err() = EOF then
strTable = technicalname(table POP_ReceiptLineHist);
range clear table POP_ReceiptLineHist;
‘POP Receipt Number’ of table POP_ReceiptLineHist = strPOPInvoice;
clear field ‘Receipt Line Number’ of table POP_ReceiptLineHist;
range start table POP_ReceiptLineHist by number 1;
‘POP Receipt Number’ of table POP_ReceiptLineHist = strPOPInvoice;
fill ‘Receipt Line Number’ of table POP_ReceiptLineHist;
range end table POP_ReceiptLineHist by number 1;
get first table POP_ReceiptLineHist by number 1;
if err() = OKAY then
strPONumber = ‘PO Number’ of table POP_ReceiptLineHist;
elseif err() = MISSING then
call ErrorMessage,_script_,strTable,err(),”MISSING “+strPOPInvoice;
elseif err() = EOF then
{ no message }
else
call ErrorMessage,_script_,strTable,err(),”Finding “+strPOPInvoice;
end if;
else
call ErrorMessage,_script_,strTable,err(),”Finding “+strPOPInvoice;
end if;
range clear table POP_ReceiptLine;
range clear table POP_ReceiptLineHist;
pragma(enable warning LiteralStringUsed);
NOTE: In this updated blog, rather than typing in the table name as a string for the error message, we are using grabbing the proper table name: technicalname(table POP_ReceiptLineHist)
This ensures that the table name can never be misspelled.
Now we will post the script that handles errors when you get, change, or save data in your tables. Years ago I had copied and pasted the table error codes from the PDF manual into this script, which you may find useful. Commented out is the script where I have the application email the error messages to support. We also deleted some obvious BTrieve/CTree eror messages. We use the newer try/end try construct when we call COM or .NET objects, but we prefer the old style error messages for table errors using this script.
{ procedure ErrorMessage }
in string l_script;
in string l_tablename;
in integer l_error_code;
in string l_msg;
local string l_errmsg,l_displayedmsg;
{local text txtMessage;}
pragma(disable warning LiteralStringUsed);
{ deleted some btrieve/ctree only errors }
case l_error_code
in [10]
set l_errmsg to “Locked record”;
in [16]
set l_errmsg to “End of file”;
in [17]
set l_errmsg to “Duplicate record”;
in [18]
set l_errmsg to “Missing record”;
in [23]
set l_errmsg to “Attempted to lock two records”;
in [24]
set l_errmsg to “No lock on update”;
in [25]
set l_errmsg to “Table doesn’t match definition”;
in [26]
set l_errmsg to “The disk is full”;
in [27]
set l_errmsg to “Unknown error”;
in [30]
set l_errmsg to “A record was changed with a passive lock”;
in [31]
set l_errmsg to “Deadlocked”;
in [34]
set l_errmsg to “Invalid key definition”;
in [36]
set l_errmsg to “Maximum number of SQL connections reached”;
in [37]
set l_errmsg to “Error accessing SQL data”;
in [38]
set l_errmsg to “Error converting SQL data”;
in [39]
set l_errmsg to “Error generating SQL data”;
else
set l_errmsg to “Unknown Error”;
end case;
l_displayedmsg = “ERROR: “+l_errmsg+” [Code “+str(l_error_code)+”] / “+
l_msg+ ” [TABLE- “+l_tablename+”] [SCRIPT- “+l_script+”]”;
{ Created message 22000: ERROR: %1 [Code %2 ] / %3 [TABLE- %4 ] [SCRIPT- %5 ]. }
l_displayedmsg = getmsg(22000);
substitute l_displayedmsg,l_errmsg,str(l_error_code),l_msg,l_tablename,l_script;
warning l_displayedmsg;
{txtMessage = l_displayedmsg; COMMENTED OUT:
call SendGPSimpleEMail,’User ID’ of globals,’User Name’ of globals,txtMessage;}
pragma(enable warning LiteralStringUsed);
DYNAMICS GP MESSAGES
Note we first compose our message using boring strings, and then by using the GP Message numbers. You first need to add the message with a 22,000 series resource number:
Messages have an obvious advantage if you want to distribute your application in several languages.
SCRIPTS THAT CHECK FOR THE PROPER ALTERNATE WINDOW SECURITY SETTINGS
These scripts prevent Great Plains from throwing an ugly series of error messages when your trigger scripts refer to fields that you have added to a GP window, but these fields are not there because the security for the alternate window has not been properly set. This is from David Musgrave’s blog at:
https://support.microsoft.com/en-us/help/941327/error-message-when-you-access-new-fields-from-an-alternate-window-in-m
This magical script works from both your Development and Runtime dictionaries, for comments see David’s blog above.
{Function Check_Security_Alternate}
function returns boolean Check_Security_Alternate;
in string FormTechnicalName;
optional in boolean CheckForTestMode = false;
local integer requestdictid,DictionaryID;
local integer has_access,FormResID;
FormResID = Resource_GetID(DYNAMICS,FORMTYPE,FormTechnicalName);
requestdictid = DYNAMICS;
DictionaryID = Runtime_GetCurrentProductID();
has_access = Security(requestdictid,FORMTYPE,FormResID);
if ( has_access = OKAY ) and (((requestdictid = DictionaryID) or
((CheckForTestMode = true) and isTestMode() of form LibSystem ))) then
Check_Security_Alternate = true;
else
Check_Security_Alternate = false;
end if;
To get this bad boy to work, we need to create a global variable,
‘PM Trx Inquiry Alternate’ of globals, with a boolean data type.
This global variable is set to true if the security for alternate window PM_Transaction_Inquiry has been properly set. This happens when the following trigger script runs just before this window has been opened:
{ Trigger_PM_Trx_Inquiry_OpenPRE }
‘PM Trx Inquiry Alternate’ of globals =
Check_Security_Alternate(technicalname(form ‘PM_Transaction_Inquiry’));
TRIGGER SCRIPT TO DISPLAY OUR ADDITIONAL FIELDS
When the user selects the vendors and documents he wants to view in the PM Inquiry window, this script fills the value of the POP Invoice Number and PO Number fields in the scrolling window. This event triggers after the FILL event in the scrolling window:
{ procedure Trigger_PM_Trx_Inquiry_Scroll_FILL }
local string strVendorID,strDocumentNumber,strPOPInvoiceNumber;
pragma(disable warning LiteralStringUsed);
if not ‘PM Trx Inquiry Alternate’ of globals then
{ you can display a message here }
abort script;
end if;
default form to PM_Transaction_Inquiry;
default window to PM_Transaction_Inquiry_Scroll;
strVendorID = ‘Vendor ID’ of table PM_Inquiry_TEMP;
strDocumentNumber = ‘Document Number’ of table PM_Inquiry_TEMP;
strPOPInvoiceNumber =
GetFirstPOPInvoiceFromHistory(strVendorID,strDocumentNumber);
‘POP Invoice Number’ = strPOPInvoiceNumber;
if empty(‘PO Number’) then
‘PO Number’ = GetFirstPOFromPOPInvoice(strPOPInvoiceNumber);
end if;
pragma(enable warning LiteralStringUsed);
Note that our remaining trigger scripts first check to see if the alternate security has been set, and if not, we ABORT SCRIPT.
The Startup script is the magical script that makes all this work. When Dynamics GP starts up it looks for and runs the Startup script. All your trigger scripts should either reside in Startup or in a subroutine called by Startup. There does not appear to be any limitation to the number of trigger scripts, we have dictionaries with hundreds of trigger scripts. (We will show the complete Startup script later in this blog.)
{ Startup }
{ dexterity looks for this script name, expecting to find trigger scripts }
local integer intResult;
pragma(disable warning LiteralStringUsed);
intResult = Trigger_RegisterFocus(anonymous(
window PM_Transaction_Inquiry_Scroll of form PM_Transaction_Inquiry),
TRIGGER_FOCUS_FILL, TRIGGER_BEFORE_ORIGINAL,
script Trigger_PM_Trx_Inquiry_Scroll_FILL);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_PMTrxInquiry_Scroll_FILL has failed.”;
end if;
pragma(enable warning LiteralStringUsed);
DEXTERITY TRIGGER SCRIPTS THAT CALL OTHER INQUIRY WINDOWS
These trigger scripts will run when the user clicks on either of the two ZOOM buttons. Since the ZOOM fields are not visible, the user thinks he is clicking on the POP Invoice Number or the Purchase Order Number.
If you want to open an inquiry window or transaction entry window with certain document numbers or master record numbers, look for a ‘Display Record’ or ‘Redisplay Button’ field, if it exists you should first try running that field script, like I do in this script:
{ Trigger_PMTrxInquiry_POPDocument_Zoom3 }
local string strMessage,strPONumber,strVendorID;
pragma(disable warning LiteralStringUsed);
if not ‘PM Trx Inquiry Alternate’ of globals then
{ you can display a message here }
abort script;
end if;
strPONumber = ‘PO Number’ of
window PM_Transaction_Inquiry_Scroll of form PM_Transaction_Inquiry;
strVendorID = ‘Vendor ID’
of window PM_Transaction_Inquiry_Scroll of form PM_Transaction_Inquiry;
if empty(strPONumber) then
strMessage = “This PM Trx has no purchase order.”;
{ Created message 22001: This PM Trx has no purchase order. }
strMessage = getmsg(22001);
warning strMessage;
else
close form POP_Document_Inquiry;
open form POP_Document_Inquiry;
default form to POP_Document_Inquiry;
default window to POP_Document_Inquiry;
‘All Or Range2’ = 1;
‘Start Vendor ID’ = strVendorID;
‘End Vendor ID’ = strVendorID;
‘Sort By’ = 1; {DDL}
‘All Or Range’ = 1;
‘Start PO Number’ = strPONumber;
‘End PO Number’ = strPONumber;
‘(L) Open POs CB’ = true;
‘(L) Historical POs CB’ = true;
‘(L) Receipts Received CB’ = true;
run script delayed ‘Redisplay Button’;
run script delayed ‘Scrolling Window Expand Button’;
end if;
pragma(enable warning LiteralStringUsed);
That approach does not always work, especially in the more quirky POP and SOP modules. Another approach to try is to run the script for the key field, or run a script log when opening the inquiry window to get a clue as to which script you need to run. From the script log we learn that we need to run a special magical POP function to open the POP Invoice Inquiry form (email me for the source code to see the tab indents):
{ Trigger_PMTrxInquiry_POPInvoice_Zoom2 }
local string strMessage,strPOPInvoiceNumber,strTable;
local integer intOpen4Hist3;
pragma(disable warning LiteralStringUsed);
if not ‘PM Trx Inquiry Alternate’ of globals then
{ you can display a message here }
abort script;
end if;
strPOPInvoiceNumber = ‘POP Invoice Number’ of
window PM_Transaction_Inquiry_Scroll of form PM_Transaction_Inquiry;
if empty(strPOPInvoiceNumber) then
warning “This PM Trx has no POP Invoice Number.”;
{ Created message 22002: This PM Trx has no POP Invoice Number. }
strMessage = getmsg(22002);
warning strMessage;
else
strTable = technicalname(table POP_ReceiptHist);
‘POP Receipt Number’ of table POP_ReceiptHist = strPOPInvoiceNumber;
get table POP_ReceiptHist by number 1;
if err() = OKAY then
intOpen4Hist3 = 3;
elseif err() = MISSING then
strTable = technicalname(table POP_Receipt);
‘POP Receipt Number’ of table POP_Receipt = strPOPInvoiceNumber;
get table POP_Receipt by number 1;
if err() = OKAY then
intOpen4Hist3 = 4;
else
call ErrorMessage,_script_,strTable,err(),
“Finding POP Invoice Number “+strPOPInvoiceNumber;
end if;
else
call ErrorMessage,_script_,strTable,err(),
“Finding POP Invoice “+strPOPInvoiceNumber;
end if;
if intOpen4Hist3 <> 0 then
OpenWindow(strPOPInvoiceNumber,2{POP_BYNOTSET},
intOpen4Hist3,1) of form POP_Inquiry_Invoice_Entry;
run script delayed ‘Scrolling Window Expand Button’ of
window POP_Inquiry_Invoice_Entry of form POP_Inquiry_Invoice_Entry;
end if;
end if;
pragma(enable warning LiteralStringUsed);
NOW FOR THE COMPLETE STARTUP SCRIPT
Everytime you log into Great Plains the Startup script is run, which registers all the trigger functions. The Startup script does not run again when you switch companies.
{ Startup }
{ dexterity looks for this script name,expecting to find trigger scripts }
local integer intResult;
pragma(disable warning LiteralStringUsed);
{ NOTE: The code will include triggers for all three of our Dexterity projects. }
intResult = Trigger_RegisterFocus(anonymous(
window PM_Transaction_Inquiry_Scroll of form PM_Transaction_Inquiry),
TRIGGER_FOCUS_FILL, TRIGGER_BEFORE_ORIGINAL,
script Trigger_PM_Trx_Inquiry_Scroll_FILL);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_PMTrxInquiry_Scroll_FILL has failed, GPUG Sample App.”;
end if;
intResult = Trigger_RegisterFocus(anonymous(
‘Zoom Buttons'[2] of window PM_Transaction_Inquiry of form PM_Transaction_Inquiry),
TRIGGER_FOCUS_POST, TRIGGER_AFTER_ORIGINAL,
script Trigger_PMTrxInquiry_POPInvoice_Zoom2);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_PMTrxInquiry_POPInvoice_Zoom2 has failed, GPUG Sample App.”;
end if;
intResult = Trigger_RegisterFocus(anonymous(
‘Zoom Buttons'[3] of window PM_Transaction_Inquiry of form PM_Transaction_Inquiry),
TRIGGER_FOCUS_POST, TRIGGER_AFTER_ORIGINAL,
script Trigger_PMTrxInquiry_POPDocument_Zoom3);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_PMTrxInquiry_POPDocument_Zoom3 has failed, GPUG Sample App.”;
end if;
intResult = Trigger_RegisterFocus(anonymous(form PM_Transaction_Inquiry),
TRIGGER_FOCUS_PRE, TRIGGER_BEFORE_ORIGINAL,
script Trigger_PM_Trx_Inquiry_OpenPRE);
if intResult <> SY_NOERR then
warning “Trigger registration for pre form Trigger_PM_Trx_Inquiry_OpenPRE has failed, GPUG Sample App.”;
end if;
pragma(enable warning LiteralStringUsed);
Note how we include the application name in the error message, so the user will know which program is throwing a registration error, which almost never happens, except when it does.
Here is the completed production version of these modifications, showing that the scripts will successfully open the inquiry windows, using the Fabrikam vendor ADVANCE0001 who has lots of purchase orders:
POSSIBLE ADDITIONAL MODIFICATIONS
Due to our business practices, we nearly always have a one-to-one correlation between purchase orders and payables invoices, and over ninety-five percent of our purchase orders have only one invoice. But if that is not the case with your business, this is a possible solution you can implement. Your logic can go like this: if a payables invoice has more than one purchase order and/or POP Invoice, when the user opens one of these inquiry windows you can open your third party window that has a scrolling window for all the Purchase Orders and POP Invoices for that payables invoice, and you can let the user access the inquiry windows from this scrolling window.
SOME ADDITIONAL DISCUSSION
It is best practice is to prefix all the field names and table names you create with your company or product initials. Many developers also either include this prefix for their custom script names. You will note that I do not do that here, often I will include these initials somewhere in the script name. Only once have I contacted Dexterity support asking why a funky named script did not work only to find out that it was a script I wrote, or miswrote.
I like for my scripts to be as English-like as possible, like:
if YouSayYes(“Do you want to save transaction “+strDocNo+”?”) then
save table whatever;
end if;
{ YouSayYes }
function returns boolean bAnsweredYes;
in string strQuestion;
local integer intAsk;
pragma(disable warning LiteralStringUsed);
intAsk = ask(strQuestion,”YES”,”NO”,””);
if intAsk = ASKBUTTON1 then
bAnsweredYes = true;
else
bAnsweredYes = false;
end if;
pragma(enable warning LiteralStringUsed);
NEXT STEPS
You need to create a chunk file so these changes can be incorporated into Dynamics GP. When I asked Dr Google about “create chunk file dexterity” he suggested several sites, including this site:
https://support.microsoft.com/en-us/help/894700/how-to-create-a-chunk-file-in-dexterity-in-microsoft-dynamics-gp
Likewise, Dr Google suggested several sites for “dynamics gp alternate forms security,” including:
https://support.microsoft.com/en-us/help/942507/how-to-give-users-access-to-a-modified-form-or-to-a-modified-report-in
We published this blog on how to transfer your Dexterity code to the next version of Dynamics GP:
https://dynamicsgptipsandtraps.wordpress.com/2018/10/23/how-to-upgrade-your-dexterity-code-to-a-new-version-of-dynamics/