"The most powerful designs are always the result of a continuous process of simplification and refinement." | ||
--Kevin Mullet |
"All the good ideas never lie under one hat." | ||
--Dale Turner |
Each new version of NAV includes new features and capabilities. NAV 2015 is no exception. It is our responsibility to understand how to apply these new features, to use them intelligently in our designs, and to develop with both their strengths and limitations in mind. The new features in NAV 2015 include new ways to debug our work and new ways to deliver information to our users. Our goal in the end is not only to provide workmanlike results but, if possible, to delight our users with the quality of the experience and the ease of use of our products.
In this chapter, we will:
Now that we have a good overall picture of how we enable users to access the tools that we have created, we are ready to start creating our own NAV C/AL routines. It is important that we learn our way around the NAV C/AL code in the standard product first. You may recall the advice in a previous chapter that the new code we create should be visually and logically compatible with what already exists. If we think of our new code as a guest being hosted by the original system, we will be doing what any thoughtful guest does – fitting smoothly into the host's environment.
An equally important aspect of becoming familiar with the existing code is to increase the likelihood we can take advantage of the features and components of the standard product to address some of our application requirements. There are at least two types of existing NAV C/AL code, of which we should make use whenever appropriate.
One group is the callable functions that are used liberally throughout NAV. Once we know about these, we can use them in our logic whenever they fit. There is no documentation for most of these functions, so we must either learn about them here or by doing our homework, studying the NAV code. The second group includes the many code snippets we can copy when we face a problem similar to something the NAV developers have already addressed.
The code snippets differ from the callable functions in two ways. First, they are not structured as coherent and callable entities. Second, they are likely to serve as models - code that must be modified to fit the situation (for example, changing variable names, adding or removing constraints, and so on).
In addition to the directly usable C/AL code, we should also make liberal use of the NAV Design Patterns Repository located at: https://community.dynamics.com/nav/w/designpatterns/105.nav-design-patterns-repository.aspx
NAV Design Patterns provide common definitions of how certain types of functions are implemented in NAV. Pattern examples include:
There are many others and new pattern definitions are added often.
Most of the callable functions in NAV are designed to handle a very specific set of data or conditions and have no general-purpose use (for example, the routines for updating Check Ledger entries during a posting process are likely to apply only to that specific function). If we are making modifications to a particular application area within NAV, we may find functions that we can utilize, either as is or as models for our new functions.
There are quite a few functions within NAV that are relatively general purpose. They either act on data that is common in many different situations (such as dates) or they perform processing tasks that are common to many situations (such as providing access to an external file). We will review a few such functions in detail, then list a number of others worth studying. If nothing else, these functions are useful as guides for "here is how NAV does it". The various parameters in these explanations are named to assist with our learning and not named the same as in the NAV code (though all structures, data types, and other technical specs match the NAV code).
If we are going to use one of these functions, we must take care to clearly understand how it operates. In each case, we should study the function and test with it before assuming that we understand how it works. There is little or no documentation for most of these functions, so understanding their proper use is totally up to us. If we need a customization, that must be done by making a copy of the target function and then modifying the copy.
This codeunit is a good example of how a well designed and well written code has long term utility. If we look at the Object Designer information for this codeunit, we will see that it originated in NAV (Navision) V3.00 in 2001. That doesn't mean it is out of date; it means that it was well thought out and complete.
Codeunit 358 contains two functions we can use in our code to create filters based on the Accounting Period Calendar. The first is CreateFiscalYearFilter
. If we are calling this from an object that has Codeunit 358 defined as a Global variable named DateFilterCalc, our call would use the following syntax:
DateFilterCalc,CreateFiscalYearFilter (Filter,Name,BaseDate,NextStep)
The calling parameters are Filter
(text, length 30), Name
(text, length 30), BaseDate
(date), and NextStep
(integer).
The second such function is CreateAccountingPeriodFilter
that has the following syntax:
DateFilterCalc.CreateAccountingPeriodFilter (Filter,Name,BaseDate,NextStep)
The calling parameters are Filter
(text, length 30), Name
(text, length 30), BaseDate
(date), and NextStep
(integer).
In the following code screenshot from Page 151 – Customer Statistics, we can see how NAV calls these functions. Page 152 – Vendor Statistics, Page 223 – Resource Statistics, and a number of other Master table statistics forms also use this set of functions:
In the next code screenshot, NAV uses the filters stored in the CustDateFilter
array to constrain the calculation of a series of FlowFields for the Customer Statistics page:
When one of these functions is called, the Filter
and Name
parameters are updated within the function, so we can use them as return parameters, allowing the function to return a workable filter and a name for that filter. The filter is calculated from the BaseDate
and NextStep
we supply.
The returned filter is supplied back in the format of a range filter string, 'startdate..enddate'
(for example, 01/01/16..12/31/16). If we call CreateFiscalYear
, the Filter
will be for the range of a fiscal year, as defined by the system's Accounting
Period
table. If we call CreateAccountingPeriodFilter
, the Filter
will be for the range of a fiscal period, as defined by the same table.
The dates of the Period or Year filter returned are tied to the BaseDate
parameter, which can be any legal date. The NextStep
parameter tells which period or year to use, depending on which function is called. A NextStep
=
0
says use the period or year containing the BaseDate
, NextStep
=
1
says use the next period or year into the future, and NextStep
=
-2
says use the period or year before last (go back two periods or years).
The Name
value returned is also derived from the Accounting
Period
table. If the call is to CreateAccountingPeriodFilter
, then Name
will contain the appropriate Accounting Period Name. If the call is to CreateFiscalYearFilter
, then Name
will contain 'Fiscal
Year
yyyy'
, where yyyy
will be the four-digit numeric year.
This codeunit contains three functions that can be used for date handling. They are FindDate
, NextDate
, and CreatePeriodFormat
.
FindDate
functionSearchString
(text, length 3), Calendar
(Date table), PeriodType
(Option, integer))DateFound
BooleanFindDate(SearchString,CalendarRec,PeriodType)
This function is often used in pages to assist with the date calculation. The purpose of this function is to find a date in the virtual Date table based on the parameters passed in. The search starts with an initial record in the Calendar table. If we pass in a record that has already been initialized by positioning the table at a date, then that will be the base date, otherwise the Work Date will be used.
PeriodType
is an Option field with the option value choices of day, week, month, quarter, year, and accounting period. For ease of coding, we could call the function with the integer equivalent (0, 1, 2, 3, 4, 5) or set up our own equivalent Option variable. In general, it's a much better practice to set up an Option variable because the Option strings make the code self-documenting.
SearchString
allows us to pass in a logical control string containing =
, >
, <
, <=
, >=
, and so on. FindDate
will find the first date starting with the initialized Calendar
date that satisfies the SearchString
logic instruction and fits the PeriodType
defined. For example, if the PeriodType
is day and the date 01/25/16 is used along with the SearchString
of >
, then the date 01/26/16 will be returned in Calendar.
NextDate
functionNextStep
(integer), Calendar
(Date table), PeriodType
(Option, integer))IntegerVariable := NextDate(NextStep,CalendarRec,PeriodType)
NextDate
will find the next date record in the Calendar
table that satisfies the calling parameters. The Calendar
and PeriodType
calling parameters for FindDate
have the same definition as they do for the FindDate
function. However, for this function to be really useful, Calendar
must be initialized before calling NextDate
—otherwise, the function will calculate the appropriate next date from day 0. The NextStep
parameter allows us to define the number of periods of PeriodType
to move, so as to obtain the desired next date. For example, if we start with a Calendar
table positioned on 01/25/16, PeriodType
of quarter
(that is 3
), and NextStep
of 2
, then NextDate
will move forward two quarters and return with Calendar
focused on Quarter 7/1/16 to 9/30/16.
CreatePeriodFormat
function
CreatePeriodFormat
allows us to supply a date and specify which of its format options we want via PeriodType
. The function's return value is a ten-character formatted text value; for example, mm/dd/yy, ww/yyyy, mon/yyyy, qtr/yyyy, or yyyy.
The functions in the Format
Address
codeunit do the obvious, they format addresses in a variety of situations. The address data in any master record (Customer, Vendor, Sales Order Sell-to, Sales Order Ship-to, Employee, and so on) may contain embedded blank lines. For example, the Address 2 line may be empty. When we print out the address information on a document or report, it will look better if there are no blank lines. These functions take care of such tasks.
In addition, NAV provides setup options for multiple formats of City – Post Code – County – Country combinations. The Format
Address
functions format addresses according to what was chosen in the setup or was been defined in the Countries/Regions page for different postal areas.
There are over 60 data-specific functions in the Format
Address
codeunit. Each data-specific function allows us to pass a record parameter for the record containing the raw address data (such as a Customer record, a Vendor Record, a Sales Order, and so on) plus a parameter of a one-dimensional Text array with 8 elements of length up to 90 characters. Each function extracts the address data from its specific master record format and stores it in the array. The function then passes that data to a general-purpose function, which does the actual work of re-sequencing according to the various setup rules and compressing the data by removing blank lines.
The following are examples of function call format for the functions for Company
and the Sales
Ship-to
addresses. In each case, AddressArray
is Text, Length 90, and one-dimensional with 8 elements.
"Format Address".Company(AddressArray,CompanyRec); "Format Address".SalesHeaderShipTo(AddressArray,SalesHeaderRec);
The function's processed result is returned in the AddressArray
parameter.
In addition to the data-specific functions in the Format
Address
codeunit, we can also directly utilize the more general-purpose functions contained there. If we add a new address structure as part of an enhancement, we may want to create our own data-specific address formatting function in our custom codeunit. But we should design our function to call the general purpose functions that already exist (and are already debugged).
The primary general-purpose address formatting function (the one we are most likely to call directly) is FormatAddr
. This is the function that does most of the work in this codeunit. The syntax for the FormatAddr
function is as follows:
FormatAddr(AddressArray,Name,Name2,ContactName,Address1,Address2, City,PostCode,County,CountryCode)
The calling parameters of AddressArray
, Name
, Name2
, and ContactName
are all Text, length 90. Address1
, Address2
, City
, and County
are all Text, length 50. PostCode
and CountryCode
are data type Code, length 20 and length 10, respectively.
Our data is passed into the function in the individual Address
fields. The results are passed back in the AddressArray
parameter for us to use.
There are two other functions in the Format
Address
codeunit that are often called directly. They are FormatPostCodeCity
and GeneratePostCodeCity
. The FormatPostCodeCity
function serves the purpose of finding the applicable setup rule for PostCode
+ City
+ County
+ Country
formatting. It then calls the GeneratePostCodeCity
function, which does the actual formatting.
Accompanying the defined NAV Patterns (on the same website as the Patterns), there is a section entitled "Recipes – The NAV C/AL Cookbook". One of those recipes, "Address Integration", applies to the preceding section on address formatting and provides a presentation on this topic at https://community.dynamics.com/nav/w/designpatterns/234.address-integration.aspx.
Throughout NAV, master records (for example Customer, Vendor, Item, and so on) and activity documents (Sales Order, Purchase Order, Warehouse Transfer Orders, and so on) are controlled by the unique identification number assigned to each one. This unique identification number is assigned through a call to a function within the NoSeriesManagement
codeunit. That function is InitSeries
. The calling format for InitSeries
is as follows:
NoSeriesManagement.InitSeries(WhichNumberSeriesToUse, LastDataRecNumberSeriesCode, SeriesDateToApply, NumberToUse, NumberSeriesUsed)
The parameter WhichNumberSeriesToUse
is generally defined on a Numbers Tab in the Setup record for the applicable application area. LastDataRecNumberSeriesCode
tells the function what Number Series was used for the previous record in this table. The SeriesDateToApply
parameter allows the function to assign ID numbers in a date-dependent fashion. NumberToUse
and the NumberSeriesUsed
are return parameters.
The following screenshot shows an example for Table 18 – Customer:
The next screenshot shows a second example for Table 36 – Sales Header. In this case, the call to NoSeresMgt
has been placed in a local function:
With the exception of GetNextNo
(used in assigning unique identifying numbers to each of a series of transactions) and possibly TestManual
(used to test if manual numbering is allowed), we are not likely to use other functions in the NoSeriesManagement
codeunit. The other functions are principally used either by the InitSeries
function or other NAV routines whose job it is to maintain Number Series control information and data.
There is also a NAV Pattern defined describing the use of number series in NAV. It is titled "No. Series".
It is very helpful when we're creating new code to have a model that works which we can study (or clone). This is especially true in NAV where there is little or no development "how to" documentation available for many of the different functions we would like to use. One of the more challenging aspects of learning to develop in the NAV environment is learning how to handle issues in the "NAV way". Learning the "NAV way" is very beneficial because then our code works better, is easier to maintain, and is easier to upgrade. There is no better place to learn the strengths and subtle features of the product than to study the code written by the developers who are part of the inner circle of NAV creation.
A list of objects follows which contain functions we may find useful for use in our code or as models. We find these useful for studying how "it's" done in NAV ("it" obviously varies depending on the function's purpose).
There are over 150 codeunits with the word "Management" or "Mgt" as part of their description name (filter the codeunits using *Management*|*Mgt*
). Each of these codeunits contains functions in which the purpose is the management of some specific aspect of NAV data. Many are very specific to a narrow range of data. Some are more general, because they contain functions we can reuse in another application area (for example, Codeunit 396 – NoSeriesManagement).
When we are working on an enhancement in a particular functional area, it is extremely important to check the Management codeunits utilized in that area. We may be able to use some existing standard functions directly. This will have the benefit of reducing the code we have to create and debug. Of course, when a new version is released, we will have to check to see if the functions on which we relied have changed in a way that affects our code.
If we can't use the existing material as is, we may find functions we can use as models for tasks in the area of our enhancement. And, even if that is not true, by researching and studying the existing code, we will learn more about how data is structured and processes flow in the standard NAV system.