Dynamics GP verus Dynamics 365/NAV/AX, and some Covid19 observations

When scanning the developer listings for contracts and positions on the various recruiter websites, I realized there is much confusion on the distinction between Dynamics GP and Dynamics 365/NAV/AX. These are very different product lines purchased by Microsoft that were coded by different tearms of programmers.

Although Dynamics GP (formerly Great Plains) and Dynamics 365 are both accounting packages marketed by Microsoft, they are very different packages, and like all high-end accounting packages they can take years to master, and they require years of practical experience to learn their quirks.  An analogy would be to compare horses to camels.  Although horses and camels are both mammals, and they both have four legs, and they both can run, nevertheless they are very, very different animals.

Most Dynamics 365 installations are crippled versions of Dynamics NAV (formerly Navision), though some are a crippled version of Dynamics AX (formerly Axapta).  Most Dynamics 365 installations are the NAV flavor; Dynamics AX is priced to compete with SAP installations, I have not seen the dealer price list in decades, but dealers tell me that once you add the charges for needed features to Dynamics 365, the cost of Dynamics GP vs Dynamics 365 are sufficiently comparable that cost is not a significant factor.

Dynamics CRM is a contact manager that can be integrated with Dynamics GP/BC/365/NAV/AX or any other accounting package using SmartConnect, Scribe, etc.

There is no doubt that the developer and administrator positions for Dynamics 365/BC/NAV/AX outnumber those for Dynamics GP by over twenty to one.  However, at the user group conferences the Great Plains GPUG members outnumber the total all the other user groups combined.  Demand for third party enhancements for Dynamics GP remains strong.  And there are new installations, a leading manufacturing/distribution vendor, SalesPad, integrates with Dynamics GP, but does not integrate with Dynamics 365.  (Is SalesPad secretly developing a Dynamics 365 integration?)

We have every indication that Microsoft will formally support Dynamics GP for the next decade or longer, the recent release of their Service Based Architecture integration software confirms this.  At the last GPUG Summit show, MicroSoft committed to supporting GP formally through the year 2028.

Which leads us to two main questions:

QUESTION 1: When should current users of Dynamics GP convert to Dynamics 365/NAV/AX?

At a recent local GPUG user group we asked a leading local VAR to discuss under which situations a current Dynamics GP user would be compelled to convert to Dynamics 365/NAV based on functional features.  One specific scenario where there is a compelling reason to convert is if you rely on the accounting package for your warehouse functionality, that NAV has a more capable inventory module than GP.  Other than that, there were no compelling reasons to convert.

There are some other differences worth mentioning.  Great Plains has only main GL account segment, and a location segment that can be flagged in the inventory module, while NAV has logic tied to multiple GL account segments.  When you see referential integrity listed as a Dynamics NAV plus do not be concerned, Dynamics GP enforces data integrity through code rather than SQL foreign keys.

How should the CLOUD affect your decision?  The CLOUD is not magic, it is simply remote servers where the security logins have been very carefully thought out.  There are vendors like Njevity that specialize in offering a cloud option for Dynamics GP.  However, if you have over several dozen remote sites you may wish to consult with your VAR whether Dynamics 365 makes sense for you.  And unfortunately, YES, the commissions and consulting fees they would earn during this transition will affect the VAR’s opinion in many cases.

Data conversion from Dynamics GP to Dynamics 365 is not really seen as an issue by many VARs.  Many VARs have already written their own data conversion process using SmartConnect or other tools, and you can easily use Power BI to provide the necessary merged views into your historical data.  For these reasons, Microsoft may not need to write a data conversion routine.

However, even when you only need to convert the main customer/vendor/inventory/GL records, it is always costly and painful to convert from one accounting package to another, ESPECIALLY when you have significant customizations created over many years.

QUESTION 2: Would Dynamics GP be a good choice for a new installation?

If you have experience using Dynamics GP, by all means, feel free to choose Dynamics GP for a new installation.  Do not worry about whether you can find support for the product, much of GP support is shifting to the national GPUG groups and the many consultants and users who are active in GPUG (Great Plains User Group).  You do not have to worry about the support issue for another decade or more.

However, the fact that Microsoft has laid off their top marketing and support staff, including David Musgrave, shows a certain ambivalence towards the product.  Developers who know how to program for both the Dynamics GP and Dynamics 365/NAV/AX platforms usually prefer to develop in Dexterity for Dynamics GP.  Since Dynamics GP stores its data in SQL tables, there is nothing internally that will prevent Dynamics GP from running on any future hardware or software platforms.  Since Dynamics GP was originally developed first for BTrieve and CTree tables, adding support for MS SQL in version 3X, it is missing some SQL-specific features like foreign keys, but this does not cause any upgrade or performance headaches.

Also, my job and gig hunting experience these past few months reveals that for every high-end Dynamics GP opening there are twenty Dynamics 365/BC/AX/NAV positions.  Which also means that every year that passes will means that it will be more and more difficult to find qualified professionals to fill Dynamics GP Administrator and Consultant positions.  When your Dynamics GP staff decide they want to retire, their replacements may also be near retirement age, but you cannot ask them that question, because then you will be discriminating against people who are nearing retirement age!

In summary, Great Plains has many years of life left. Long live Great Plains!

COVID-19 OBSERVATIONS

Unfortunately, the question of whether or not to wear a mask has been politicized.  Personally, I wear a mask anytime I plan to be near anybody, or when I plan to be inside for a long period of time.  To me, there is a false assurance in knowing that if I catch COVID-19 I “only” have a two percent chance of dying.  What a two percent mortality rate implies is that I have a ten to twenty percent chance of being miserable and/or hospitalized wondering whether I might die, and if I don’t die there will be significant chance that my breathing and/or vitality would be permanently impaired.  Fortunately, I do not need to take that risk, and I will not work onsite just to demonstrate that I am indeed working.  I have no problem going onsite for periodic meetings, as long as everyone else INSIDE in the building is also wearing masks and taking appropriate precautions.

IMHO, the best overall COVID-19 articles are by the Science Editor of Atlantic magazine:
https://www.theatlantic.com/author/ed-yong/

Customizing and Enhancing Dynamics GP

My objective in this blog is to lay out my professional opinion on the various approaches to customizing GP-Great Plains. My experience for the past fourteen years is with a wholesale company with an unlimited Dexterity support plan for some years, and during that time I submitted an unlimited number of support requests to learn as much as I can from the excellent Dexterity support.

When scanning the developer listings for contracts and positions on the various recruiter websites, I get the impression that some people out there may have some unrealistic expectations regarding Microsoft Dynamics GP customizations.

My objective in this blog is to lay out my professional opinion on the various approaches to customizing GP-Great Plains.  My experience for the past fourteen years is with a wholesale company with an unlimited Dexterity support plan for some years, and during that time I submitted an unlimited number of support requests to learn as much as I can from the excellent Dexterity support.

INTEGRATING EXTERNAL TRANSACTIONS USING SMARTCONNECT/ SSIS

SmartConnect is basically a wrapper around the eConnect engine that will also read and write to most any data source.  It appears to be every bit as capable as the SSIS Microsoft tool in the integrations it supports.  Since you can use C#/VBasic and SQL scripts in the SmartConnect integrations, there is no reason other than cost to use SmartConnect rather than eConnect.

Using SmartConnect to integrate data from external applications into Dynamics GP has many advantages for users.  SmartConnect can run as a service on Windows Server either periodically or overnight, and it is optimized via the eConnect engine underneath to write to the Great Plains data structures efficiently and dependably, SmartConnect handles writing the link/keys and batch tables automatically.  Users should appreciate

The trick to using any of the SQL based integration and reporting tools, including SmartConnect, SSIS, SSRS, Crystal, and Power BI, is to do as much of the heavy lifting as possible using SQL queries.  My preferred technique is to save the data to be uploaded in files with assigned run numbers for each upload.  This came in handy when we wrote an SSIS package to transfer data from Dynamics GP to text files that would be uploaded into the parent company’s accounting software, we actually wrote out the contents of each comma delimited record into a text field.  If a run is missing on their side, you only need to copy and paste this field from the SQL Enterprise Manager into a text file, which came in handy when my boss and the archive folder were both on vacation and inaccessible.

The best practice for writing transactions to Dynamics (or any other) SQL tables is the same whatever tool you choose to use.  You simply enter a transaction in vanilla Great Plains that matches the custom transaction as closely as possible, then generate a transaction using the method you choose, then compare the data in each table field by field to make sure the data pattern is identical.  This is also true for SmartConnect/eConnect, some consultants have reported difficulties on the GPUG discussion board using the eConnect/SmartConnect tools in unique situations.  Usually, you do not need to write to the note index fields, but once I had to generate a note index field for the inventory table.

In addition, when you are creating transactions with inventory items, keep in mind that if an external application is creating and/or updating those inventory items, or any other master records, you need to ensure that inventory items and/or other master records in the transactions contain valid data.  We often write a check routine verifying that the master records referenced in the transactions are valid before the application will post the transaction batch.

PROGRAMMING WITH DEXTERITY

Dynamics GP itself was written using the Dexterity programming environment, both Dynamics GP and Dexterity were developed simultaneously.  Dexterity was designed so any Dexterity modifications could appear to be part of Great Plains, without touching source code, while interacting with the internal Dynamics code and windows, and scrolling windows.  Upgrading to the next version of Great Plains is painless, usually the developer only needs to do a Developer Update to rebuild the chunk file.  Rarely does the developer need to update their enhancement code for an upgrade.  (I plan to publish my Developer Update checklist in a future blog.)

Just because you use SmartConnect to integrate data into Dynamics GP does not automatically exclude using Dexterity in your data integrations.  Another advantage of staging the data to be integrated in a run number table is you can use custom Dexterity windows to display data originating in the external package in custom vendor/customer/transaction inquiry windows in Dynamics GP.

Depending on the situation, Dexterity can be just as good a solution for writing to the GP tables as SmartConnect, particularly if the client does not own a SmartConnect license.  Over the years, I have developed Dexterity functions to write to the major transaction tables that includes rollback capability when an error is encountered.  Also, I have written Dexterity routines that read data directly from Excel spreadsheets.

One of my prior Dexterity projects is reading EDI files into POP Invoice EDI transactions that would be compared to the purchase orders, permitting the user to make changes before these could be selectively uploaded into the POP Invoice batches in Great Plains.  As far as the user was concerned this was done on a real-time basis, each time the user opened the POP Invoice Entry window the program uploaded any new EDI invoices into Great Plains.

In addition, Dexterity has a full complement of functions that can trigger off Dexterity form, window, field, and table events, which means you can quickly customize the GP Inquiry windows.  When updating the POP and SOP windows in particular, you need to use the pseudo-object scripts to interact the native Great Plains objects, fields, and data, which can be challenging.

Dexterity programs can write to the GP SQL databases using either the Dexterity table functions, or pass-through SQL commands, or by calling stored procedures.  My preference is to create all third party SQL tables through the Dynamics GP menus so they are visible to Dexterity.  Dexterity can also see SQL views as read-only tables.

Dexterity is usually the best tool to use when modifying Dynamics GP functionality.  This is especially true when you need to modify Dynamics windows, and most especially true when you are modifying scrolling windows, or the Dexterity flavor of grids.  You can add tables and fields and menus to the existing Dynamics forms, you can also hide or rearrange the existing fields and prompts.

When you program your enhancements in Dexterity, the Modifier and Extender products are mostly redundant.  However, some developers use VBA to customize fields added to windows using the Modifier.

 

PROGRAMMING WITH VISUAL STUDIO TOOLS AND DEXTERITY

Dexterity is missing some fundamental functionality that can be added by the Visual Studio Tools, which enables you to write functions that trigger off events in Dynamics.  We use the Visual Studio Tools to read the system environment variables, specifically the Citrix server name to display in error messages, and also to return a directory listing useful in EDI applications.

Dynamics GP still has many years of life left, the number of Great Plains GPUG members outnumber the total of all the other users at the user group conference, but the number of Dynamics AX new installs are greater than Dynamics GP new installs.  So if you are developing involved applications such as EFT or EDI applications, it makes sense to write these as .NET objects that are called by Visual Studio Tools C# codes that are called from a Dexterity/Dynamics GP user interface, and could later be called from a Dynamics AX user interface.  This keeps your options open.

The Dexterity manual suggests that you can manipulate scrolling windows with the Visual Studio Tools functions, but to the best of my knowledge and experience, this functionality is not dependable.  Developers who specialize in .Net programming have not been able to get this to work properly, and this was confirmed by a support case I submitted to Great Plains and some GPUG online discussions.  There might be some funky secret knowledge that is need to access the scrolling window table buffers properly, if so, I am unenlightened.

Visual Studio Tools can also run any Dexterity script in any dictionary, in addition to triggering off any Dexterity script.  This gives you more flexibility if you need to write code that interacts with other third party Dexterity applications.  You can also create .Net forms and grids that can be opened in Dynamics GP, but my preference is to create these in Dexterity.

If you wish to write a .Net program that independently reads and writes to the Great Plains tables, you do not need to use the Visual Studio Tools.  A really good example of this type of program is Salespad.

GP POWER TOOLS

David Musgrave did an early morning remote presentation for our local GPUG chapter, since early morning in Florida is like midnight in Australia, and we were absolutely blown away.  Everyone clapped.  Truly, IMHO, whether you are a user, or a system administrator, or a developer, GP Power Tools needs to be part of your Great Plains installation.  I mean that literally, GP Power Tools is not merely an add-on, it indeed becomes part of Great Plains.

Without a doubt, David Musgrave is a first rate Dexterity developer.  After Microsoft hired him he helped write enhancements for both Dynamics and Great Plains.  He wrote the field level security module, provided input to some trigger functionality that dramatically improved the Report Writer, and wrote a remote debugging module that grew into the current GP Power Tools, which Microsoft allows him to market independently today, for everyone’s benefit.

Please, review his listing of the chief benefits of the GP Power Tools:
https://winthropdc.wordpress.com/2016/03/02/gppt-top-benefits-of-gp-power-tools/

You may not pick up on the true power of GP Power Tools by just reading this list, so let me paraphrase what I find most appealing about these tools.

David Musgrave was a high-level escalation support person for Microsoft, and had to troubleshoot an intermittent mission critical anomaly for a large VIP client.  He was quite frustrated, so he wrote a quick application to capture the various SQL dexlog and Dexterity script logs while Great Plains was running to help him troubleshoot this problem successfully.  This tool evolved into the GP Support Debugging Tool to benefit other users, which evolved into today’s GP Power Tools.

While at Microsoft, David also help troubleshoot funky problems encountered by large installs during version update conversions.  With this knowledge, he added some routines to GP Power Tools to check the GP SQL databases for problems that could possibly cause problems during updates.  If you run these routines before you upgrade to a new version they will also help control your blood pressure and help alleviate other stress related disorders and perhaps even prevent temporary baldness, which sometime occurs when you start pulling out your hair.

David probably has the deepest knowledge of Great Plains security on anyone outside of Great Plains, especially since he was involved in developing the pessimistic security model, so the GP Power Tools includes both additional security functions and improved reporting on which users can do what in GP.

GP Power Tools will also help you find your way in Great Plains, so you will never forget where you are.  You can know whether you are in your peaceful production copy of Great Plains, or whether you are in the light blue test environment of Great Plains, or whether you are in the yellow development version of Great Plains, or whatever color scheme you care to setup.  And, you don’t have to worry about being in limbo in an invisible session, GP Power Tools can pull GP back into the visible desktop.

FUTURE BLOGS

In the first versions of Dynamics Report Writer you could change the position of the fields on the report and add fields from the current tables to the report, but you were not able add fields from additional tables to the report.  You can circumvent these limitations using triggers and also by using the Dexterity Report Writer trigger functions that our hero David Musgrave helped develop while he was at Microsoft.  We will write a blog on these advanced Report Writer topics.  We will also write a blog on modifying other third party dictionary products using triggers and cross-dictionary development strategies.

SUMMATION OF LONG TERM STRATEGY

Although the headhunters tell me that though there still numerous developer positions for Dynamics GP, the developer positions for Dynamics AX outnumber those for Dynamics GP by about five to one.  However, at the user group conferences the Great Plains GPUG members outnumber the total all the other user groups combined.  Demand for third party enhancements for Dynamics GP remains strong.

We have every indication that Microsoft will formally support Dynamics GP for the next decade or longer, the recent release of their Service Based Architecture integration software confirms this.  At the last GPUG Summit show, MicroSoft committed to supporting GP formally through the year 2028.  However, the fact that Microsoft has laid off their top marketing and support staff, including David Musgrave, shows a certain ambivalence towards the product.  Developers who know how to program for both the Dynamics GP and AX platforms usually prefer to develop in Dexterity for Dynamics GP.  Since Dynamics GP stores its data in SQL tables, there is nothing internally that will prevent Dynamics GP from running on any future hardware or software platforms.  Since Dynamics GP was originally developed first for BTrieve and CTree tables, adding support for MS SQL in version 3X, it is missing some SQL-specific data elements like foreign keys, but this does not cause any upgrade or performance headaches.  In summary, Great Plains has many years of life left.   Long live Great Plains!  Or at least until the day when I am singing with Elvis.

Updating Blog on Adding Fields and Capabilities to GP Inquiry Windows

You may not want to become an expert Dexterity programmer, but it could be useful to learn just enough Dexterity to customize your inquiry windows. The objective of this article is to do just that. We have confidence that you can do this. If you do not agree, you can quickly scan through to realize what is possible so you can suggest inquiry enhancements to your Dexterity consultant. And if you are not a neophyte, maybe you can pick up a tip or two. And you can learn tips on how to make your Dexterity code more professional.

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:
dexPMinq1

This is what this window looks like under the Windows -> Preview menu:
dexPMinq2

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.
dexPMinq3

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.
dexPMinq4

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.”dexPMinq5

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.

scrollingwindow

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:

dexPMinq8dexPMinq7

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

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:
dexPMinq9

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/

Using Dexterity Trigger Scripts to Modify the Body of GP Posting Reports

The vanilla Report Writer in Great Plains gives you limited flexibility in modifying existing reports, but with Dexterity you can remove all limits and make any customizations you wish in the existing Report Writer reports that come with Great Plains. In a prior blog we showed how you can use special functions that can access data in third party tables or any other data source to include on your reports. In this blog we will show you how you can add lines to the TEMP tables that Report Writer generates for so many of its reports.

Do you remember when you first started using Dynamics, and learned how you can modify any report in Dynamics GP?  And how frustrated you were when you wanted to modify the body of your transaction posting reports?  Sure, you can delete some sections in the header, but how can you modify the body of the report, the detail lines and the journal entries?  You find that there is no obvious way to modify the body of a transaction batch posting report because it contains some cryptic fields in a one-line report body where you were expecting to see more complexity that you could modify.

As an example, we can print the POP Purchasing Batch Entry batch to screen, and then press the MODIFY button to view this screen in the Report Writer:
ModifyPostingReport

We see one line in the BODY just one line of cryptic fields like String2, String3, String4, String5, and String6:
POPRcvgPostingJournal

We click on the TABLES button to determine which is the TEMP table used for this report:
POPBatchReportTables

And we find the table in Dexterity, and there we see the cryptic String2, String3, String4, String5, and String6 fields:
POPBatchReportTableSpecs

How wonderful!  What is worse, you can’t see what is in the TEMP table because it is a C-Tree table that Dynamics deletes immediately after it prints the report!

Which leads us to ask the question:

HOW CAN WE MODIFY OUR BATCH POSTING REPORT?

Or, a much better question to ask is:
How can we modify the batch posting report by adding additional lines to the TEMP table that contains the body of this report?

It is possible to do this using Dexterity trigger functions.  For example, in this blog we first enter an invoice with these quantities using the GP 2018 sample data, and then we will show you how you can add a line that shows the QUANTITY NOT INVOICED for any partially invoiced PO line on the POP Invoice:
FinalPOPBatchReportOkay

What else can we do with this code?

We used the basic code in this blog to add lines to the POP Batch Posting Report to include line data from our third-party tables containing discount allowances that applied to the individual lines of this posting report, plus adding additional information after the GP AJE lines.  But you are not interested in this, so we will show you something else that is also interesting, those PO-POP Invoice lines that are not completely invoiced.

LET US GET STARTED ON THIS PROJECT

The first step is to capture the contents of our problem TEMP file so we can figure out what data we want to insert into the report.

In Dexterity, we duplicate the POP Posting Journal table:
Copytemptablestructures

Then we change the database type to SQL and change the other table names in the TABLE DEFINITION of our new table:
NewTableName

We then create the table and stored procedures in the Maintenance -> SQL menu option, and run the Grant.SQL script granting permissions to the new table.

We then write a simple database trigger script that copies the contents of the TEMP table to our new GP_POP_PostingJournal SQL table each time a record is inserted into the TEMP table.

NOTE: If you want to follow along with our other Dexterity projects described in our blogs on GP Inquiry window enhancements and Report Writer enhancements, you may want to put all the Dexterity code in one dictionary.

{ Startup }
{ dexterity looks for this script name, expecting to find trigger scripts }
local integer intResult;
pragma(disable warning LiteralStringUsed);
intResult = Trigger_RegisterDatabase(anonymous(table POP_PostingJournal_TEMP), 0,
TRIGGER_ON_DB_ADD, script Trigger_POPPostingJournal_TEMP_Insert);
if intResult <> SY_NOERR then
warning “Database Trigger_POPPostingJournal_TEMP_Insert registration failed.”;
end if;

{ procedure Trigger_POPPostingJournal_TEMP_Insert }
inout table POP_PostingJournal_TEMP;
pragma(disable warning LiteralStringUsed);
copy from table POP_PostingJournal_TEMP to table GP_POP_PostingJournal;
save table GP_POP_PostingJournal;
if err() <> OKAY then
call ErrorMessage,_script_,”GP_POP_PostingJournal”,err(),”Saving temp file”;
end if;
pragma(enable warning LiteralStringUsed);

The ErrrorMessage and left scripts are included in our beginning Dexterity blog on GP Inquiry windows.

After we do this, we can run the POP Batch report to screen, and we can see what this TEMP table looks like by pasting the contents of our new SQL table into Excel from the SQL Enterprise Manager:
GPPOPPostJrnl_Table

The first six columns are obviously the key fields, which we can double-check in the Dexterity table definition.  We only need to increment the value in the Ord_Line field to add a line to our report.

We next run the batch report and capture the script log that generates the TEMP posting table.  We do this using the Debug -> Script Log command before and after we print the report to screen.

The script log can be quite long, but you can tell from the data and the function names about where the inventory item creation scripts loop around.  In this report there are two inventory lines, and there are two occurrences of the script CreateLineDetail, the first around line 500, which is the script we want.  We look in the CoreParams_140.txt file in the SDK to determine the signature for this script, which we will need to write our trigger.
scriptlog

Writing trigger scripts can be a matter of trial and error.  Because this script uses an anonymous table, when we were debugging this script the Dynamics script generating the TEMP table simply aborted with no message.  We had to capture a script log and look near the end of the script to find the error message we needed to debug our script.

HERE IS THE MAGICAL DEXTERITY CODE

So, now we know the event after which we want to add the line, QUANTITY NOT INVOICED when that is applicable.  We will show you, in reverse order, the scripts to add this magical message.

First, the line to add to the Startup script that creates the reference to the trigger script that runs after the CreateLineDetail script runs:

{ Startup }
intResult = Trigger_RegisterProcedure(
script CreateLineDetail of form POP_PostingJournal,
TRIGGER_AFTER_ORIGINAL,script Trigger_GPPOPIvc_ReportRecLine);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_GPPOPIvc_ReportRecLine has failed.”;
end if;

 

The trick is when you trigger off a script, your trigger script has to match the parameters in number and in data type, and in script type (function vs. procedure), which is why we needed to determine the signature of the CreateDetailLine script, as you can see in the actual trigger script.  Note that we only add a line when there is a QUANTITY NOT INVOICED:

{ Trigger_GPPOPIvc_ReportRecLine }
{ parameters match CreateDetailLine script }
inout  POP_PostingJournalState Me;
inout  table   POP_PostingJournal_TEMP;
inout  POP_ReceiptLineState       CReceiptLineState;
inout  anonymous table POP_ReceiptLineAnon;

local long longPOLineNumber,longReceiptLineNumber;
local integer intPOLineStatus;
local dollar dlrQTYOrdered,dlrQTYInvoicedSUM,dlrQTYNotInvoiced;
local string strPONumber,strItemNumber,strPOPReceiptNumber,strArray[6];
pragma(disable warning LiteralStringUsed);
strPONumber = ‘PO Number’ of table POP_ReceiptLineAnon;
strItemNumber = ‘Item Number’ of table POP_ReceiptLineAnon;
dlrQTYOrdered = GetQTYOrderedForFirstPOLineForItemNumber(
longPOLineNumber,intPOLineStatus,strPONumber,strItemNumber);
if dlrQTYOrdered > 0 and intPOLineStatus = 4 then
{ 5: completely invoiced, 6: cancelled, 4: shipment received }
strPOPReceiptNumber = ‘POP Receipt Number’ of table POP_ReceiptLineAnon;
longReceiptLineNumber = ‘Receipt Line Number’ of table POP_ReceiptLineAnon;
dlrQTYInvoicedSUM = GetPOPInvoicedQTYTotal(strPONumber,longPOLineNumber);
dlrQTYNotInvoiced = dlrQTYOrdered – dlrQTYInvoicedSUM;
if dlrQTYNotInvoiced > 0 then
strArray[3] = “QTY Not Invoiced: “+strRound(dlrQTYNotInvoiced,0);
if not SavePOPPostJrnlTEMP(
‘Batch Source’ of table POP_PostingJournal_TEMP,
‘Batch Number’ of table POP_PostingJournal_TEMP,
‘POP Type’ of table POP_PostingJournal_TEMP,
‘POP Receipt Number’ of table POP_PostingJournal_TEMP,
‘Ord’ of table POP_PostingJournal_TEMP,
‘Ord Line’ of table POP_PostingJournal_TEMP + 1000,
strArray,table POP_PostingJournal_TEMP) then
{ this is not a good day }
end if;
end if;
else
{ this is probably not the second line of an inventory item }
end if;
pragma(enable warning LiteralStringUsed);

If you pass in the table in the SAVE function you also pass in the table buffer, which works in this application.

{ procedure SavePOPPostJrnlTEMP }
function returns boolean bOkay;

in string strBatchSource;
in string strBatchNumber;
in integer intPOPType;
in string strPOPReceiptNumber;
in long longOrd;
in long longOrdLine;
in string stringArray[6];
inout table POP_PostingJournal_TEMP;

pragma(disable warning LiteralStringUsed);
‘Batch Source’ of table POP_PostingJournal_TEMP = strBatchSource;
‘Batch Number’ of table POP_PostingJournal_TEMP = strBatchNumber;
‘POP Type’ of table POP_PostingJournal_TEMP = intPOPType;
‘POP Receipt Number’ of table POP_PostingJournal_TEMP = strPOPReceiptNumber;
‘Ord’ of table POP_PostingJournal_TEMP = longOrd;
‘Ord Line’ of table POP_PostingJournal_TEMP = longOrdLine;
‘String2’ of table POP_PostingJournal_TEMP = left(stringArray[2],30);
‘String3’ of table POP_PostingJournal_TEMP = left(stringArray[3],30);
‘String4’ of table POP_PostingJournal_TEMP = left(stringArray[4],30);
‘String5’ of table POP_PostingJournal_TEMP = left(stringArray[5],30);
‘String6’ of table POP_PostingJournal_TEMP = left(stringArray[6],30);
save table POP_PostingJournal_TEMP;
if err() <> OKAY then
call ErrorMessage,_script_,”POP_PostingJournal_TEMP”,err(),”Saving”;
end if;
bOkay = ( err() = OKAY );
pragma(enable warning LiteralStringUsed);

The ErrorMessage procedure has been mentioned previously in this blog.

And the other Dexterity scripts we call are:

{ procedure GetQTYOrderedForFirstPOLineForItemNumber }
function returns dollar dlrQTYOrdered;

inout long longINOUT_POLineNumber;
inout integer intINOUT_POLineStatus;
in string strPONumber;
in string strItemNumber;

pragma(disable warning LiteralStringUsed);
range clear table POP_POLine;

‘PO Number’ of table POP_POLine = strPONumber;
‘Item Number’ of table POP_POLine = strItemNumber;
clear field ‘Ord’ of table POP_POLine;
range start table POP_POLine by number 5;
‘PO Number’ of table POP_POLine = strPONumber;
‘Item Number’ of table POP_POLine = strItemNumber;
fill ‘Ord’ of table POP_POLine;
range end table POP_POLine by number 5;
get first table POP_POLine by number 5;
if err() = OKAY then
dlrQTYOrdered = ‘QTY Ordered’ of table POP_POLine;
intINOUT_POLineStatus = ‘PO Line Status’ of table POP_POLine;
longINOUT_POLineNumber = ‘Ord’ of table POP_POLine;
else
dlrQTYOrdered = 0;
longINOUT_POLineNumber = 0;
intINOUT_POLineStatus = 0;
if err() <> MISSING and err() <> EOF then
call ErrorMessage,_script_,”POP_POLine”,err(),
“Finding line for PO “+strPONumber+”, item “+strItemNumber;
end if;
end if;
pragma(enable warning LiteralStringUsed);

{ procedure strRound }
function returns string strRound;

in dollar dlrAmount;
in integer intIN_DecimalPlaces;

local integer intDecimalPlaces;
if intIN_DecimalPlaces = 1 then
intDecimalPlaces  = 0;
elseif intIN_DecimalPlaces = 3 then
intDecimalPlaces  = 2;
elseif intIN_DecimalPlaces = 5 then
intDecimalPlaces  = 4;
else
intDecimalPlaces  = intIN_DecimalPlaces;
end if;
strRound = format( dlrAmount , false , true , intDecimalPlaces , PARENNEG );

SOME LAST HOUSEKEEPING SCRIPTS

Did we comment out the Startup reference to the initial trigger script that made a copy of the report TEMP table?

If we want to keep this script active so we can peek at the last TEMP table contents for debugging purposes, we need to write a simple trigger script that initializes our posting table copy each time the POP Batch report is run, including during posting:

{ Startup }
intResult = Trigger_RegisterFocus(anonymous(
‘WindowPrint’ of window POP_Batch_Entry of form POP_Batch_Entry),
TRIGGER_FOCUS_PRE, TRIGGER_BEFORE_ORIGINAL,
script Trigger_DeleteOurPOPBatchTempTable);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_DeleteOurPOPBatchTempTable1 has failed.”;
end if;
intResult = Trigger_RegisterFocus(anonymous(
‘Post Button’ of window POP_Batch_Entry of form POP_Batch_Entry),
TRIGGER_FOCUS_PRE, TRIGGER_BEFORE_ORIGINAL,
script Trigger_DeleteOurPOPBatchTempTable);
if intResult <> SY_NOERR then
warning “Trigger registration for Trigger_DeleteOurPOPBatchTempTable2 has failed.”;
end if;

Which calls a simple one-line trigger script:

{ Trigger_DeleteOurPOPBatchTempTable }
remove range table GP_POP_PostingJournal;

We can see in the finished report that the second POP Invoice has one line that only partially invoices the line from the purchase order, with a QTY Not Invoiced of 15:
FinishedReport

SETTING REPORT SECURITY (OR NOT)

In our other Report Writer Dexterity enhancement blog, we showed how to trigger off custom Report Writer functions, which meant that we physically added fields to the report.  This meant that we needed to set security to our newly modified report.

This Dexterity project is different, we are using Dexterity code to insert new lines into the Report Writer TEMP table that is used in the Body of the report.  What this means is, as far as Great Plains is concerned, this is NOT a modified report!  Which means our additional custom lines will automatically print anytime you print the POP Batch Report, and you cannot restrict the printing of the additional lines with a user security setting.

 

 

Balancing the POP RNI Report to the GL

The POP RNI (Received Not Invoiced) balance should be balanced to the General Ledger Accrued Expenses balance, for it is truly a legal liability that must be tracked and reconciled.

In our old blog on balancing GP we promised that we would cover balancing the RNI in more depth, and since then our not so old dog has learned some new tricks.  Please review our old blog for generic advice that affects balancing all subledger in all modules to the GL, and correcting the posted GL subledger AJEs (Adjusting Journal Entries):

https://dynamicsgptipsandtraps.wordpress.com/2018/08/28/basic-balancing-and-accounting-tips/

This is the MS Dynamics GP support discussion of balancing the RNI report to the GL, please review it in case Microsoft adds any suggestions we do not cover here.
https://support.microsoft.com/en-ph/help/951236/information-about-why-the-values-on-the-received-not-invoiced-report-m

CAVEATS: If you use Landed Cost or Multi-currency, you may need to take these adjustments into account.

By definition, drop-shipment transactions never appear on the RNI report because it is your customer who receives the inventory.  For direct shipments your customer has the RNI liability, not you.

PRINTING THE POP RNI REPORT

The RNI, or Received Not Invoiced report, is a PURCHASING ANALYSIS report.  You should probably use the date you began using the POP module as your report beginning date.

RNI Report

We balance our Received Not Invoiced report monthly, but in hind-sight we recommend that you try to balance this at least on a weekly basis, at least until you successfully tame your RNI beast, and keep him tamed.  You should be able to improve your business processes so this report balances to the Accrued Expenses account in the general ledger to the penny.  This is how the general ledger entries flow for the RNI, or Accrued Expenses GL Account:

RNI T Accounts

When balancing, an accrued expense will drop off this report when it is INVOICED, not when it has been POSTED.  Which means that posting as many invoice batches as you can will make this account easier to balance, and that the POP Invoice unposted batch reports will be a reconciling item.  You can build a SmartList for the unposted RNI balances you need to add back, or you can run this SQL query:

USE [TWO18]  — use your own company database
SELECT [POPRCTNM],[POPTYPE],[VNDDOCNM],[receiptdate],[BACHNUMB],[VENDORID],[VENDNAME], [SUBTOTAL],sum([SUBTOTAL]) OVER() as GrandTotal
FROM [dbo].[POP10300]

TIPS AND TRAPS WHEN CLOSING PURCHASE ORDERS

If you close a purchase order where the inventory shipments received have been partially invoiced, a journal entry will be generated for the un-invoiced items.  You should review the journal entry; the item cost may not be what you expect.

If you close a purchase order where none of the inventory shipments have been received, NO JOURNAL ENTRY will be created by Great Plains.  Great Plains says this is an intentional design decision.

RNI Closing PO

The preferable method to write off an RNI amount that will never be invoiced is to enter a Returns Transaction Entry.  This method always generates a GL adjusting journal entry and provides a better audit trail.  When you enter a RETURN, access the help screen for the window and review the FIELD documentation.  The type affects the allowable return quantity, the unit cost of the return items, and for non-US companies, the value-added Tax Schedule selected.

RNI POP Refunds

For RNI writeoffs, my preference is to select the RETURN type.  The RETURN type uses the POP inventory receiving cost, aka the original shipment receipt cost.

OTHER REASONS WHY VALID POP INVOICES DO NOT APPEAR ON THE RNI REPORT

Why would valid vendor POP Invoices not appear on the RNI Report?  The most common reason is the PO has been cancelled in error.  Unfortunately, Great Plains does not permit you to un-cancel a purchase order that has been cancelled in error.  You can only uncancel a purchase order with a SQL query.  Note that Status 4 means is the RECEIVING status, and that you will need to bring the cancelled subtotal or CANCSUB to zero, and reinstate the outstanding SUBTOTAL amount.

USE TWO18  — use your own company database
declare @PONumber varchar(17)
select @PONumber = ‘PO123456’
update dbo.pop10100
set postatus = ‘4’,statgrp = ‘1’,subtotal = cancsub where PONUMBER = @PONumber
update dbo.pop10100
set cancsub = 0 where PONUMBER = @PONumber
update dbo.pop10110
set polnesta = ‘4’ where PONUMBER = @PONumber

Over the past decade we have had several instances where the POP Invoice amount does not appear on the RNI report, but Great Plains will not let us AUTO-INVOICE the PO because it has already been invoiced!  How do you discover which POP Invoice is hiding?  You simply run a SQL query looking for any unposted POP Invoice lines that are hiding:

USE [TWO18]  — use your own company database
SELECT [POPRCTNM],[PONUMBER],[ITEMNMBR],[ITEMDESC],[UNITCOST],[EXTDCOST]
FROM [dbo].[POP10310] WHERE [POPRCTNM] NOT IN
( SELECT [POPRCTNM] FROM [dbo].[POP10300] )

This SQL query simply looks for unposted POP Invoice or POP Receiving lines that have inventory detail but no header record saved.  This can happen because the underlying Dexterity engine saves each scrolling window line immediately, but does not immediately save the header information.

If this query returns some transactions, how do you fix them?  By far the simplest solution is to enter the hiding transaction in either the POP Invoice Entry or the POP Receiving Entry form, enter the hiding POP Receipt Number.  You will see the lines with inventory items but no vendor information.  My strong advice is to simply delete the invoicing or receiving transaction and re-process the transaction.  If you delete and start over, you know your link file data will be valid.

You will often see POP10300 orphaned header records with a zero POPTYPE flag and no details.  You can delete them if you wish, they have never caused us problems.

PARTIALLY POSTED BATCHES AND OTHER POP DATA ISSUES

If the posting for your POP Invoice or Receiving batch is interrupted, usually some or all of the POP records for one transaction will be in both the work and history files.  Print the batch report to screen, enter another batch number for the problem transaction with duplicates, and post the remaining valid unposted transactions.

We always puzzled out the SQL queries needed to fix the data in the PO tables (POP10100/110), POP Receipt tables (POP10300/310/POP30300/310) and the link tables (POP10500/600), if yours is a more complex situation there may be other tables.  Our situation is we had multiple custom tables with data that was hard to duplicate.  Alternately, you can delete the unposted transaction, and delete with a SQL query any leftover records in the history and link tables.

The following rogue event happened to us: twice in the past two years, but never in the decade previous, in the middle of the day, during data entry, not during posting, all the line inventory details were deleted for SOME but not ALL unposted POP Invoice and POP Receivings.  Our sister custom detail tables were never touched, so it was unlikely that our custom Dexterity code was at fault.  Dr Google told us that several other reputable consultants have had clients that experienced this problem.  However, Great Plains support says that this is a rare event, that they do have a record of anyone opening a support reporting this issue in recent years (I know I did not).  Great Plains support told me to encourage everyone who experiences this problem to contact them.

The very best way to recover from this event is to delete the problem POP Invoices and POP Receivings that have no lines and re-enter them.  BUT, if the user tries to POST these batches with headers and no lines, they will post without warning and without lines, which will mean that the RNI Report for these transactions will no longer be accurate, until someone fixes the link files with SQL queries as best they can.

USEFULNESS OF POP CHECK LINKS

IMHO, running Check Links for the POP module is not only a waste of time, it is also taking an unnecessary risk of data corruption.  Not to mention that the POP check link procedures are glacially slow.  We ran check links on a test backup of our production data (IMHO, you should never run Check Links first on production data, though these functions are much safer now than in the earlier years of Dynamics GP).  Check Links for the POP modules verifies mostly the PO files, but not so much the POP Receipt and POP Link files, which are the real accounting transactions in GP.  We came to this conclusion after exhaustively analyzing the text file output of all our POP check link procedures for several years of work and historical.  If other consultants have differing opinions or experiences, we would be happy to hear from them.

JenKuntz.ca

Talking about Great Plains, Dexterity, SQL, and some C#

Dilbert Daily Strip

Talking about Great Plains, Dexterity, SQL, and some C#

The Dynamics GP Blogster

Talking about Great Plains, Dexterity, SQL, and some C#

Winthrop Development Consultants Blog

This blog will keep you up to date with Winthrop Development Consultants and David’s experiences, tips and tricks as a developer and consultant in the Microsoft Dynamics GP community.

Reflections on Morality, Philosophy, and History

Talking about Great Plains, Dexterity, SQL, and some C#