Merge pull request #101 from christianbraeunlich/nav-dp-formatting
NAVPatterns Formatting
This commit is contained in:
commit
9e06f72056
115 changed files with 2221 additions and 2507 deletions
|
|
@ -77,10 +77,7 @@ New table
|
|||
```AL
|
||||
TAB 50003 VAT Return Data
|
||||
PROCEDURE FillInVATReturnData@1200001(VAR VATReturnData@1200000 : Record 50003);
|
||||
```
|
||||
|
||||
****
|
||||
```AL
|
||||
VATReturnData.INIT;
|
||||
VATReturnData.NumberOfCopies := GetDefaultNumberOfCopies;
|
||||
VATReturnData.Uploaded := FALSE;
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ The [Released Entity][anchor4].
|
|||
{{< youtube O2R-fTSup1o >}}
|
||||
|
||||
[anchor0]: 2260.BlockedEntityPattern.png
|
||||
[anchor1]: /nav/w/designpatterns/247.data-driven-blocked-entity/edit
|
||||
[anchor1]: /navpatterns/1-patterns/blocked-entity/data-driven-blocked-entity/
|
||||
[anchor2]: 8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png
|
||||
[anchor3]: 3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png
|
||||
[anchor4]: /nav/w/designpatterns/115.released-entity.aspx
|
||||
[anchor4]: /navpatterns/1-patterns/released-entity/
|
||||
|
||||
|
||||
[image0]: 2260.BlockedEntityPattern.png
|
||||
|
|
|
|||
|
|
@ -59,10 +59,11 @@ All the approval workflows include a response that restricts usage of a record,
|
|||
|
||||
The code behind the "Add record restriction" workflow response:
|
||||
|
||||
RecRef.GETTABLE(Variant);
|
||||
Workflow.GET(WorkflowStepInstance."Workflow Code");
|
||||
RecordRestrictionMgt.RestrictRecordUsage(RecRef.RECORDID,STRSUBSTNO(RestrictUsageDetailsTxt,Workflow.Code,Workflow.Description));
|
||||
|
||||
```AL
|
||||
RecRef.GETTABLE(Variant);
|
||||
Workflow.GET(WorkflowStepInstance."Workflow Code");
|
||||
RecordRestrictionMgt.RestrictRecordUsage(RecRef.RECORDID,STRSUBSTNO(RestrictUsageDetailsTxt,Workflow.Code,Workflow.Description));
|
||||
```
|
||||
|
||||
The code behind the "Remove record restriction" response:
|
||||
|
||||
|
|
@ -103,7 +104,7 @@ This pattern has been introduced in Dynamics NAV 2016\.
|
|||
|
||||
|
||||
[anchor0]: attention.jpg
|
||||
[anchor1]: /nav/w/designpatterns/79.blocked-entity
|
||||
[anchor1]: /navpatterns/1-patterns/blocked-entity/
|
||||
[anchor2]: https://microsoft.sharepoint.com/teams/DynamicsNAV/Wiki/Nav%20Wiki%20Documents/NAV%20App%20Patterns/NAV%20App%20Patterns%20for%20Review/Data-Driven%20Blocked%20Entity.docx#_msocom_2
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -74,31 +74,34 @@ BEGIN
|
|||
END;
|
||||
|
||||
LOCAL PROCEDURE IdentifyCountryRegionCode@1(VAR BankDataConvBank@1002 : Record 1259;Filter@1000 : Text) : Text;
|
||||
VAR
|
||||
CompanyInformation@1001 : Record 79;
|
||||
BlankFilter@1003 : Text;
|
||||
BEGIN
|
||||
BlankFilter := '''''';
|
||||
IF Filter = BlankFilter THEN BEGIN
|
||||
CompanyInformation.GET;
|
||||
BankDataConvBank.SETFILTER("Country/Region Code",CompanyInformation."Country/Region Code");
|
||||
EXIT(BankDataConvBank.GETFILTER("Country/Region Code"));
|
||||
END;
|
||||
EXIT(Filter);
|
||||
VAR
|
||||
CompanyInformation@1001 : Record 79;
|
||||
BlankFilter@1003 : Text;
|
||||
BEGIN
|
||||
BlankFilter := '''''';
|
||||
|
||||
IF Filter = BlankFilter THEN BEGIN
|
||||
CompanyInformation.GET;
|
||||
BankDataConvBank.SETFILTER("Country/Region Code",CompanyInformation."Country/Region Code");
|
||||
EXIT(BankDataConvBank.GETFILTER("Country/Region Code"));
|
||||
END;
|
||||
|
||||
EXIT(Filter);
|
||||
END;
|
||||
|
||||
LOCAL PROCEDURE RefreshBankNamesOlderThanToday@5(CountryRegionCode@1000 : Text;ShowErrors@1002 : Boolean;Timeout@1004 : Integer);
|
||||
VAR
|
||||
BankDataConvBank@1001 : Record 1259;
|
||||
ImpBankListExtDataHndl@1003 : Codeunit 1289;
|
||||
BEGIN
|
||||
IF CountryRegionCode <\> '' THEN
|
||||
BankDataConvBank.SETFILTER("Country/Region Code",CountryRegionCode);
|
||||
VAR
|
||||
BankDataConvBank@1001 : Record 1259;
|
||||
ImpBankListExtDataHndl@1003 : Codeunit 1289;
|
||||
BEGIN
|
||||
IF CountryRegionCode <> '' THEN
|
||||
BankDataConvBank.SETFILTER("Country/Region Code",CountryRegionCode);
|
||||
BankDataConvBank.SETFILTER("Last Update Date",'<%1',TODAY);
|
||||
IF BankDataConvBank.FINDFIRST THEN
|
||||
ImpBankListExtDataHndl.GetBankListFromConversionService(ShowErrors,CountryRegionCode,Timeout);
|
||||
END;
|
||||
```
|
||||
|
||||
## NAV Usages
|
||||
|
||||
Bank name lookup on the Bank Account card for dynamically identifying the format to use to generate a bank-specific payment file.
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ The Dynamics NAV application developer can take into account using the Copy Docu
|
|||
The Copy Document pattern involves the following entities:
|
||||
|
||||
1. Source document tables for document header and line. For example,Sales Header/Line.
|
||||
2. Destination document tables for document header and line.
|
||||
2. Destination document tables for document header and line.
|
||||
**Note:** The source document header/line and destination document header/line tables do not need to be the same. For example, you can copy a Sales Shipment Header/Lines into a Sales Header/Lines.
|
||||
|
||||
**Note:**The source document header/line and destination document header/line tables do not need to be the same. For example, you can copy a Sales Shipment Header/Lines into a Sales Header/Lines.
|
||||
3. Copy Document engine: COD6620, Copy Document Mgt.
|
||||
4. Copy Document report for a specific document type. The report requires the following parameters:
|
||||
* Source Document Type
|
||||
|
|
@ -77,7 +77,7 @@ Example: Copy Sales Document for Credit Memos.
|
|||
|
||||
In the standard version of Microsoft Dynamics NAV, the Copy Document functionality is implemented in the Sales Credit Memo window as shown in the following section.
|
||||
|
||||
\*\*\*
|
||||
****
|
||||
|
||||
**Precondition**: The user enters data in PAGE44, Sales Credit Memo.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Using the patterns involves three steps.
|
|||
|
||||
1) As a first step, we must insert a record. This can be done either through C/AL code or by letting the user create a record using the **New** action.
|
||||
|
||||
2) After the record is created, we must apply the template. This is done by using the **UpdateRecord** function in the **Config. Template Management **codeunit (8612).
|
||||
2) After the record is created, we must apply the template. This is done by using the **UpdateRecord** function in the **Config. Template Management** codeunit (8612).
|
||||
|
||||
**Config. Template Lines** records reference one **Config. Template Header** record (lines pattern). The lines can be of type:
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ Using the patterns involves three steps.
|
|||
|
||||
The **UpdateRecord** function applies values to the record one line at the time. One of the requirements was to be possible to use configuration templates in different language/regional settings than the template was created in.
|
||||
|
||||
To support this scenario, when applying the**Config. Template Line** record, **GLOBALLANGUAGE** is set to the language ID of the field. This is important because the default value is stored as text, so we need to use the same formatting that NAV was running on when the template was created. Otherwise, data types, such as Boolean, Date, etc., will raise validation errors.
|
||||
To support this scenario, when applying the **Config. Template Line** record, **GLOBALLANGUAGE** is set to the language ID of the field. This is important because the default value is stored as text, so we need to use the same formatting that NAV was running on when the template was created. Otherwise, data types, such as Boolean, Date, etc., will raise validation errors.
|
||||
|
||||
Any updates to a **Config. Template Line** record will automatically update the language ID to the current one. Since lines are applied one by one, it is supported to have lines with different language IDs belonging to the same template.
|
||||
|
||||
|
|
@ -47,39 +47,40 @@ Any updates to a **Config. Template Line** record will automatically update the
|
|||
|
||||
Code example (Insert a record, apply a template, and insert the related templates):
|
||||
|
||||
**// First insert a record Customer.INSERT(TRUE);**
|
||||
|
||||
**// Apply a template RecRef.GETTABLE(Customer);**
|
||||
|
||||
```al
|
||||
// First insert a record Customer.INSERT(TRUE);
|
||||
|
||||
// Apply a template RecRef.GETTABLE(Customer);
|
||||
|
||||
ConfigTemplateMgt.UpdateRecord(ConfigTemplateHeader,RecRef);
|
||||
RecRef.SETTABLE(Customer);
|
||||
```
|
||||
|
||||
**// Insert Dimensions -- related templates**
|
||||
```al
|
||||
// Insert Dimensions -- related templates
|
||||
|
||||
MiniDimensionsTemplate.InsertDimensionsFromTemplates(ConfigTemplateHeader,Customer."No.",DATABASE::Customer);
|
||||
```
|
||||
|
||||
Code to insert related templates (dimensions):
|
||||
|
||||
```al
|
||||
FUNCTION InsertDimensionsFromTemplates(ConfigTemplateHeader : Record "Config. Template Header";MasterRecordNo : Code\[20\];TableID : Integer)
|
||||
FUNCTION InsertDimensionsFromTemplates(ConfigTemplateHeader : Record "Config. Template Header";MasterRecordNo : Code[20];TableID : Integer)
|
||||
|
||||
// There are multiple records (multiple dimensions per master record)
|
||||
// We have to set filter
|
||||
ConfigTemplateLine.SETRANGE(Type,ConfigTemplateLine.Type::"Related Template");
|
||||
ConfigTemplateLine.SETRANGE("Data Template Code",ConfigTemplateHeader.Code
|
||||
IF ConfigTemplateLine.FINDSET THEN
|
||||
REPEAT
|
||||
ConfigTemplateHeader.GET(ConfigTemplateLine."Template Code");
|
||||
// Ensure that the table where the template belongs to is Dimensions
|
||||
// We could have other related templates
|
||||
IF ConfigTemplateHeader."Table ID" = DATABASE::"Default Dimension" THEN
|
||||
InsertDimensionFromTemplate(ConfigTemplateHeader,MasterRecordNo,TableID);
|
||||
UNTIL ConfigTemplateLine.NEXT = 0;
|
||||
```
|
||||
REPEAT
|
||||
ConfigTemplateHeader.GET(ConfigTemplateLine."Template Code");
|
||||
|
||||
**// Create a new Dimensions Record and link it to the Master Record******
|
||||
```al
|
||||
FUNCTION InsertDimensionFromTemplate(ConfigTemplateHeader : Record "Config. Template Header";MasterRecordNo : Code\[20\];TableID : Integer)
|
||||
// Ensure that the table where the template belongs to is Dimensions
|
||||
// We could have other related templates
|
||||
IF ConfigTemplateHeader."Table ID" = DATABASE::"Default Dimension" THEN
|
||||
InsertDimensionFromTemplate(ConfigTemplateHeader,MasterRecordNo,TableID);
|
||||
UNTIL ConfigTemplateLine.NEXT = 0;
|
||||
|
||||
// Create a new Dimensions Record and link it to the Master Record
|
||||
FUNCTION InsertDimensionFromTemplate(ConfigTemplateHeader : Record "Config. Template Header";MasterRecordNo : Code[20];TableID : Integer)
|
||||
DefaultDimension.INIT;
|
||||
DefaultDimension."No." := MasterRecordNo;
|
||||
DefaultDimension."Table ID" := TableID;
|
||||
|
|
@ -100,7 +101,7 @@ FUNCTION InsertDimensionFromTemplate(ConfigTemplateHeader : Record "Config. Temp
|
|||
|
||||
**To view or edit templates, you have two options:**
|
||||
|
||||
1. Use the **Config. Template List** table (8620) and the **Config. Template Header Card **table (8618).
|
||||
1. Use the **Config. Template List** table (8620) and the **Config. Template Header Card** table (8618).
|
||||
|
||||
This is a generic solution that is not very usable and is error-prone (no lookups, checks for length, table relation checks, etc.) The default value is a text field of 250 characters, which might be much more than the field length, and may lead to validation errors when used. Users will most likely not be able to use this page.
|
||||
|
||||
|
|
@ -121,9 +122,9 @@ The goals of this solution were:
|
|||
* To avoid any lateral effects of doing validation on the temporary master record. Doing validation on fields, even though the record itself is temporary, could permanently modify other data in the database. For example, if you insert a new record in the **Customer** table, even in a temporary table, a Contact record is created, which will not be temporary.
|
||||
* Testability: It is easy to test through RecordRef that the template table matches the main table. We can compare field lengths, data types, table relations, etc. The test is able to detect that they are out of sync, so it is easy to prevent errors.
|
||||
|
||||
One example in the product is the **Mini Customer Template **table (1300).
|
||||
One example in the product is the **Mini Customer Template** table (1300).
|
||||
|
||||
The table itself contains very little code. OnModify, OnInsert, and OnDelete triggers update the **Configuration Header** and **Configuration Lines** tables. The following functions in the **Config. Template Management **codeunit (8612), are used for this:
|
||||
The table itself contains very little code. OnModify, OnInsert, and OnDelete triggers update the **Configuration Header** and **Configuration Lines** tables. The following functions in the **Config. Template Management** codeunit (8612), are used for this:
|
||||
|
||||
* ConfigTemplateManagement.CreateConfigTemplateAndLines
|
||||
* ConfigTemplateManagement.UpdateConfigTemplateAndLines
|
||||
|
|
@ -142,15 +143,13 @@ In C5 2014, this is the workflow:
|
|||
|
||||
The user opens the **Customers List** window and selects **New**
|
||||
|
||||
**[![ ][image3]][anchor3]**
|
||||
|
||||
**[][anchor3]**
|
||||
[![ ][image3]][anchor3]
|
||||
|
||||
From this page, the user can view the template, edit it, or create a new one. Selecting a template will populate the customer card and open a new record. From the existing record, the user has options to save as a template or opening a list of templates to maintain available templates. Selecting a template will populate the customer card and open a new record. From the existing record, the user has options to save as a template or opening a list of templates to maintain available templates.
|
||||
|
||||
[![ ][image4]][anchor4]
|
||||
|
||||
From the **Customer Card Template **window, we can invoke the **Dimensions** action, through which we can define the dimensions that will be inserted together with the template:
|
||||
From the **Customer Card Template** window, we can invoke the **Dimensions** action, through which we can define the dimensions that will be inserted together with the template:
|
||||
|
||||
[![ ][image5]][anchor5]
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ GETURL automatically handles:
|
|||
|
||||
The format is:
|
||||
|
||||
\[String :=\] GETURL(ClientType\[, Company\]\[, Object Type\]\[, Object Id\]\[, Record\])
|
||||
[String :=] GETURL(ClientType[, Company][, Object Type][, Object Id][, Record])
|
||||
|
||||
Where:
|
||||
|
||||
|
|
@ -60,228 +60,65 @@ The GETURL function can generally be used every time a URL must be created. The
|
|||
|
||||
The following are examples of calls to GETURL and their corresponding return value:
|
||||
|
||||
**Command**
|
||||
|
||||
**URL**
|
||||
```al
|
||||
GETURL(CLIENTTYPE::Win)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
|
||||
GETURL(CLIENTTYPE::Web)
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient
|
||||
|
||||
GETURL(CLIENTTYPE::OData)
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP)
|
||||
|
||||
http://MyServer:7047/DynamicsNAV71/WS/Services
|
||||
|
||||
GETURL(CLIENTTYPE::Current) ie. When running this code on a Win client session
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
|
||||
GETURL(CLIENTTYPE::Default) ie. When the Server config key DefaultClient is set to Windows
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,'')
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,'NONEXISTING Corp')
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/NONEXISTING Corp/
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME)
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient?company=CRONUS
|
||||
|
||||
GETURL(CLIENTTYPE::Web,'')
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient
|
||||
|
||||
GETURL(CLIENTTYPE::Web,'NONEXISTING Corp')
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient?company=NONEXISTING Corp
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME)
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')
|
||||
|
||||
GETURL(CLIENTTYPE::OData,'')
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData
|
||||
|
||||
GETURL(CLIENTTYPE::OData,'NONEXISTING Corp')
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData/Company('NONEXISTING Corp')
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME)
|
||||
|
||||
http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Services
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,'')
|
||||
|
||||
http://MyServer:7047/DynamicsNAV71/WS/Services
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,'NONEXISTING Corp')
|
||||
|
||||
http://MyServer:7047/DynamicsNAV71/WS/NONEXISTING Corp/Services
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Table,27)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runtable?table=27
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,27)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=27
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Report,6)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runreport?report=6
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Codeunit,5065)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runcodeunit?codeunit=5065
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Query,9150)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runquery?query=9150
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::XmlPort,5150)
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runxmlport?xmlport=5150
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27) ie. When the Web Service is published
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/PAG27Vendors
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Query,9150) ie. When the Web Service is published
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/QUE9150MyCustomers
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27)Â ie. When the Web Service is published
|
||||
|
||||
http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Page/PAG27Vendors
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Codeunit,5065) ie. When the Web Service is published
|
||||
|
||||
http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Codeunit/COD5065EmailLogging
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,27,record) List Page
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=27&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,26,record) Card Page
|
||||
|
||||
dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=26&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,27,record) List Page
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient?company=CRONUS&page=27&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,26,record) Card Page
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient?company=CRONUS&page=26&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27,record)
|
||||
|
||||
http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/PAG27Vendors('IC1030')
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,27)
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient?company=CRONUS&page=27
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Report,6)
|
||||
|
||||
https://navwebsrvr:443/DynamicsNAV71\_Instance1/Webclient?company=CRONUS&report=6
|
||||
|
||||
```
|
||||
Command | URL
|
||||
--------|------
|
||||
GETURL(CLIENTTYPE::Win) | dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
GETURL(CLIENTTYPE::Web) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient
|
||||
GETURL(CLIENTTYPE::OData) | http://MyServer:7048/DynamicsNAV71/OData
|
||||
GETURL(CLIENTTYPE::SOAP) | http://MyServer:7047/DynamicsNAV71/WS/Services
|
||||
GETURL(CLIENTTYPE::Current) ie. When running this code on a Win client session | dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
GETURL(CLIENTTYPE::Default) ie. When the Server config key DefaultClient is set to Windows | dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/
|
||||
GETURL(CLIENTTYPE::Windows,'') | dynamicsnav://MyServer:7046/DynamicsNAV71//
|
||||
GETURL(CLIENTTYPE::Windows,'NONEXISTING Corp') | dynamicsnav://MyServer:7046/DynamicsNAV71/NONEXISTING Corp/
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS
|
||||
GETURL(CLIENTTYPE::Web,'') | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient
|
||||
GETURL(CLIENTTYPE::Web,'NONEXISTING Corp') | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=NONEXISTING Corp
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME) | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')
|
||||
GETURL(CLIENTTYPE::OData,'') | http://MyServer:7048/DynamicsNAV71/OData
|
||||
GETURL(CLIENTTYPE::OData,'NONEXISTING Corp') | http://MyServer:7048/DynamicsNAV71/OData/Company('NONEXISTING Corp')
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME) | http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Services
|
||||
GETURL(CLIENTTYPE::SOAP,'') | http://MyServer:7047/DynamicsNAV71/WS/Services
|
||||
GETURL(CLIENTTYPE::SOAP,'NONEXISTING Corp') | http://MyServer:7047/DynamicsNAV71/WS/NONEXISTING Corp/Services
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Table,27) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runtable?table=27
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,27) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=27
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Report,6) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runreport?report=6
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runcodeunit?codeunit=5065
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Query,9150) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runquery?query=9150
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runxmlport?xmlport=5150
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27) ie. When the Web Service is published | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/PAG27Vendors
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Query,9150) ie. When the Web Service is published | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/QUE9150MyCustomers
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27)Â ie. When the Web Service is published | http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Page/PAG27Vendors
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Codeunit,5065) ie. When the Web Service is published | http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Codeunit/COD5065EmailLogging
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,27,record) List Page | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=27&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,26,record) Card Page | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=26&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,27,record) List Page | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&page=27&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,26,record) Card Page | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&page=26&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27,record) | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/PAG27Vendors('IC1030')
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,27) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&page=27
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Report,6) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&report=6
|
||||
|
||||
If the GETURL function is called with invalid parameters, it will return an empty string. In that case, you can find the related error text by calling the GETLASTERRORTEXT function.
|
||||
|
||||
**Function Call**
|
||||
|
||||
**Error Message**
|
||||
|
||||
```al
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Table,27)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Codeunit,5065)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Query,9150)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::XmlPort,5150)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Table,27)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27)
|
||||
|
||||
The Page object, 27, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Report,6)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Codeunit,5065)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Query,9150)
|
||||
|
||||
The Query object, 9150, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::XmlPort,5150)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Table,27)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27)
|
||||
|
||||
The Page object, 27, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Report,6)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Codeunit,5065)
|
||||
|
||||
The Codeunit object, 5065, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Query,9150)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::XmlPort,5150)
|
||||
|
||||
The specified object type parameter for the GetUrl function is not valid.
|
||||
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27,record)
|
||||
|
||||
```
|
||||
|
||||
You cannot specify a record parameter for the GetUrl function when the object type is SOAP
|
||||
Function Call | Error Message
|
||||
--------|------
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Table,27) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Query,9150) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Table,27) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27) | The Page object, 27, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Report,6) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Query,9150) | The Query object, 9150, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Table,27) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27) | The Page object, 27, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Report,6) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | The Codeunit object, 5065, that is specified for the GetUrl function has not been published in the Web Services table.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Query,9150) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | The specified object type parameter for the GetUrl function is not valid.
|
||||
GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27,record) | You cannot specify a record parameter for the GetUrl function when the object type is SOAP
|
||||
|
||||
## NAV Specific Example
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ This table is used to encapsulate the logic of the Business Chart Add-in. The ta
|
|||
* Storing chart values and conversion from .NET to C/AL and vice versa
|
||||
* Handling of captions: We must use C/AL to provide multilanguage text in add-ins. In addition, the multilanguage text must be encapsulated in a single place, because we pass/read the same dataset from/to the add-in.
|
||||
* DrillDown logic
|
||||
* Other helper data related functions, for displaying date, periods, etc.\[Bogdana1\] \[NK2\] \[NK3\]
|
||||
* Other helper data related functions, for displaying date, periods, etc.[Bogdana1] [NK2] [NK3]
|
||||
|
||||
**Note:** It is recommended that you reuse the **Business Chart Buffer **table****(485) as a buffer table or extend. It is a generic table which should cover most of the use cases. Implement a new buffer table only if this table does not meet your needs.
|
||||
**Note:** It is recommended that you reuse the **Business Chart Buffer table** (485) as a buffer table or extend. It is a generic table which should cover most of the use cases. Implement a new buffer table only if this table does not meet your needs.
|
||||
|
||||
### CardPart page
|
||||
|
||||
|
|
@ -67,10 +67,10 @@ One of the most common functionalities is personalization. If the chart can be c
|
|||
To do this, you need the following:
|
||||
|
||||
* A setup record to store the data. You can use the **Business Chart User Setup** table (487) or create a new setup table if you need to store more information.
|
||||
* A management codeunit to write/apply the settings to the chart and to encapsulate other logic. Since we should not write code on pages, the code for the actions and other logic that does not apply to the setup record****should go in this codeunit.
|
||||
* A management codeunit to write/apply the settings to the chart and to encapsulate other logic. Since we should not write code on pages, the code for the actions and other logic that does not apply to the setup record should go in this codeunit.
|
||||
* Setup pages where users can customize how the chart is shown and set different settings.
|
||||
|
||||
The relation between the components is visualized in the following diagram\[
|
||||
The relation between the components is visualized in the following diagram:
|
||||
|
||||
[![ ][image3]][anchor3]
|
||||
|
||||
|
|
@ -98,19 +98,22 @@ Implementation of the **Finance Performance Chart** page (762)
|
|||
|
||||
```al
|
||||
BusinessChart::AddInReady()
|
||||
UpdateChart(Period::" ");
|
||||
|
||||
UpdateChart(Period::" ");
|
||||
|
||||
LOCAL UpdateChart(Period : ',Next,Previous')
|
||||
MoveAndUpdateChart(Period,0);
|
||||
|
||||
MoveAndUpdateChart(Period,0);
|
||||
|
||||
LOCAL MoveAndUpdateChart(Period : ',Next,Previous';Move : Integer)
|
||||
AccSchedChartManagement.GetSetupRecordset(AccountSchedulesChartSetup,AccountSchedulesChartSetup.Name,Move);
|
||||
|
||||
AccSchedChartManagement.UpdateData(Rec,Period,AccountSchedulesChartSetup);
|
||||
AccSchedChartManagement.GetSetupRecordset(AccountSchedulesChartSetup,AccountSchedulesChartSetup.Name,Move);
|
||||
|
||||
Update(CurrPage.BusinessChart);
|
||||
AccSchedChartManagement.UpdateData(Rec,Period,AccountSchedulesChartSetup);
|
||||
|
||||
StatusText := GetCurrentSelectionText("Period Filter Start Date","Period Filter End Date");
|
||||
Update(CurrPage.BusinessChart);
|
||||
|
||||
StatusText := GetCurrentSelectionText("Period Filter Start Date","Period Filter End Date");
|
||||
```
|
||||
|
||||
In the MoveAndUpdateChart method, the AccSchedChartManagement codeunit gets a setup record and updates it if necessary. Then, it initializes the chart with setup data and sets the StatusText to show the period for which data is displayed. The same method is used by the actions to move and update the chart so that there is no code duplication.
|
||||
|
|
@ -119,8 +122,10 @@ The following code is used to implement **DataPointClicked**
|
|||
|
||||
```al
|
||||
BusinessChart::DataPointClicked(point : DotNet "Microsoft.Dynamics.Nav.Client.BusinessChart.BusinessChartDataPoint")
|
||||
SetDrillDownIndexes(point);
|
||||
AccSchedChartManagement.DrillDown(Rec,AccountSchedulesChartSetup);
|
||||
|
||||
SetDrillDownIndexes(point);
|
||||
|
||||
AccSchedChartManagement.DrillDown(Rec,AccountSchedulesChartSetup);
|
||||
```
|
||||
|
||||
SetDrillDownindexes is a method from the **Business Chart Buffer** table that maps the DotNet point variable to C/AL data, so it must be used. The next method that you must implement is the action to be performed on Drilldown.
|
||||
|
|
|
|||
|
|
@ -33,22 +33,22 @@ The pattern has four components:
|
|||
An example of this would be when we have multiple users looking at the same set of data and we want their screens to update in "real time" whenever one of them makes a change, without doing a full refresh. We will use the [Observer pattern][anchor2] to capture the change (act as the Publisher) and then create a Table to hold Subscribers and Filters (Change Observer), a Table to be the Message Queue (Change Notification), and a Codeunit to be the Message Broker and help with the polling (ObserverMgt).
|
||||
|
||||
Below are the table definitions:
|
||||
```al
|
||||
//Change Observer:
|
||||
//"Table ID" Integer "Observable Table"
|
||||
"Server ID" Integer
|
||||
"Session ID" Integer
|
||||
|
||||
//Change Notification:
|
||||
//"Table ID" Integer "Observable Table"
|
||||
"Server ID" Integer
|
||||
"Session ID" Integer
|
||||
"Entry No." Integer AutoIncrement
|
||||
"Type of Change" Option Insert,Modify,Delete,Rename
|
||||
"Record ID" RecordID
|
||||
... (other fields to indicate what has changed)
|
||||
```
|
||||
|
||||
**Change Observer:** | |
|
||||
-----|------|-----
|
||||
"Table ID" | Integer | "Observable Table"
|
||||
"Server ID" | Integer
|
||||
"Session ID" | Integer
|
||||
|
||||
**Change Notification:** | |
|
||||
-----|------|-----
|
||||
"Table ID" | Integer | "Observable Table"
|
||||
"Server ID" | Integer
|
||||
"Session ID" | Integer
|
||||
"Entry No." | Integer | AutoIncrement
|
||||
"Type of Change" | Option | Insert,Modify,Delete,Rename
|
||||
"Record ID" | RecordID
|
||||
... (other fields to indicate what has changed)
|
||||
|
||||
The Change Observer table identifies the Subscriber using Server ID and Session ID, and then in this example there is only one filter, which is the Table ID we want to listen to any changes. In this case all three fields are in the Primary Key.
|
||||
|
||||
|
|
@ -58,85 +58,89 @@ _**Note:**_ Other examples of the pattern could have very different fields to id
|
|||
|
||||
Our Message Broker Codeunit will also serve as a central place to create Subscribers (Listen and StopListening functions) and a place to Poll for Messages. Note that the Poll function deletes the Messages as it reads them.
|
||||
```al
|
||||
//Listen(TableID : Integer)**
|
||||
WITH Observer DO BEGIN
|
||||
"Table ID" := TableID;
|
||||
"Server ID" := SERVICEINSTANCEID;
|
||||
"Session ID" := SESSIONID;
|
||||
INSERT(TRUE);
|
||||
COMMIT;
|
||||
END;
|
||||
Listen(TableID : Integer)
|
||||
WITH Observer DO BEGIN
|
||||
"Table ID" := TableID;
|
||||
"Server ID" := SERVICEINSTANCEID;
|
||||
"Session ID" := SESSIONID;
|
||||
INSERT(TRUE);
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
//StopListening(TableID : Integer)
|
||||
WITH Observer DO BEGIN
|
||||
RESET;
|
||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||
SETRANGE("Session ID",SESSIONID);
|
||||
SETRANGE("Table ID",TableID);
|
||||
DELETEALL(TRUE);
|
||||
COMMIT;
|
||||
END;
|
||||
StopListening(TableID : Integer)
|
||||
WITH Observer DO BEGIN
|
||||
RESET;
|
||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||
SETRANGE("Session ID",SESSIONID);
|
||||
SETRANGE("Table ID",TableID);
|
||||
DELETEALL(TRUE);
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
//NotifyAll(ChangeNotification : Record "Change Notification")**
|
||||
WITH Observer DO BEGIN
|
||||
RESET;
|
||||
SETRANGE("Table ID",ChangeNotification."Table ID");
|
||||
IF FINDSET THEN REPEAT
|
||||
Notify(Observer,ChangeNotification);
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
NotifyAll(ChangeNotification : Record "Change Notification")
|
||||
WITH Observer DO BEGIN
|
||||
RESET;
|
||||
SETRANGE("Table ID",ChangeNotification."Table ID");
|
||||
IF FINDSET THEN REPEAT
|
||||
Notify(Observer,ChangeNotification);
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
|
||||
//Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification")**
|
||||
WITH ChangeNotification DO BEGIN
|
||||
"Server ID" := Observer."Server ID";
|
||||
"Session ID" := Observer."Session ID";
|
||||
"Entry No." := 0;
|
||||
INSERT;
|
||||
END;
|
||||
Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification")
|
||||
WITH ChangeNotification DO BEGIN
|
||||
"Server ID" := Observer."Server ID";
|
||||
"Session ID" := Observer."Session ID";
|
||||
"Entry No." := 0;
|
||||
INSERT;
|
||||
END;
|
||||
|
||||
//Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification")**
|
||||
WITH ChangeNotification DO BEGIN
|
||||
TempChangeNotification.RESET;
|
||||
TempChangeNotification.DELETEALL;
|
||||
RESET;
|
||||
SETRANGE("Table ID",TableID);
|
||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||
SETRANGE("Session ID",SESSIONID);
|
||||
IF FINDSET THEN REPEAT
|
||||
TempChangeNotification := ChangeNotification;
|
||||
TempChangeNotification.INSERT;
|
||||
MARK(TRUE);
|
||||
UNTIL NEXT = 0;
|
||||
MARKEDONLY(TRUE);
|
||||
DELETEALL;
|
||||
END;
|
||||
Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification")
|
||||
WITH ChangeNotification DO BEGIN
|
||||
TempChangeNotification.RESET;
|
||||
TempChangeNotification.DELETEALL;
|
||||
|
||||
RESET;
|
||||
SETRANGE("Table ID",TableID);
|
||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||
SETRANGE("Session ID",SESSIONID);
|
||||
|
||||
IF FINDSET THEN REPEAT
|
||||
TempChangeNotification := ChangeNotification;
|
||||
TempChangeNotification.INSERT;
|
||||
MARK(TRUE);
|
||||
UNTIL NEXT = 0;
|
||||
|
||||
MARKEDONLY(TRUE);
|
||||
DELETEALL;
|
||||
END;
|
||||
```
|
||||
|
||||
**__** The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page:
|
||||
The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page:
|
||||
|
||||
```al
|
||||
//OnQueryClosePage(CloseAction : Action None) : Boolean**
|
||||
ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking");
|
||||
|
||||
//Timer::AddInReady()**
|
||||
IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN
|
||||
CurrPage.Timer.Ping(1000);
|
||||
|
||||
//Timer::Pong()**
|
||||
CallUpdate;
|
||||
CurrPage.Timer.Ping(1000);
|
||||
|
||||
//LOCAL CallUpdate()**
|
||||
ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification);
|
||||
WITH TempChangeNotification DO BEGIN
|
||||
IF FINDSET THEN REPEAT
|
||||
OnQueryClosePage(CloseAction : Action None) : Boolean
|
||||
ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking");
|
||||
|
||||
Timer::AddInReady()
|
||||
IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN
|
||||
CurrPage.Timer.Ping(1000);
|
||||
|
||||
Timer::Pong()
|
||||
CallUpdate;
|
||||
CurrPage.Timer.Ping(1000);
|
||||
|
||||
LOCAL CallUpdate()
|
||||
ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification);
|
||||
|
||||
WITH TempChangeNotification DO BEGIN
|
||||
IF FINDSET THEN REPEAT
|
||||
IF "Type of Change" = "Type of Change"::Delete THEN BEGIN
|
||||
...
|
||||
...
|
||||
END ELSE IF RecRef.GET("Record ID") THEN BEGIN
|
||||
...
|
||||
...
|
||||
END;
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
|
@ -151,13 +155,13 @@ This pattern was originally described in the following blog:
|
|||
|
||||
Below is the Wikipedia link to the PubSub pattern
|
||||
|
||||
[https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe\_pattern][anchor1]
|
||||
[https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern][anchor1]
|
||||
|
||||
|
||||
|
||||
[anchor0]: PubSub.png
|
||||
[anchor1]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
|
||||
[anchor2]: /nav/w/designpatterns/248.observer
|
||||
[anchor2]: /navpatterns/1-patterns/observer/
|
||||
[anchor3]: https://geeknikolai.wordpress.com/2015/10/30/pubsub-pattern-in-dynamics-nav-2016/
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ There is no way in NAV to get a set of records from the database, which all have
|
|||
|
||||
* The NAV Service Tier receives and throws away data.
|
||||
|
||||
### Ending Date Problem
|
||||
## Ending Date Problem
|
||||
|
||||
Ending Date may introduce some problems of its own.
|
||||
|
||||
|
|
@ -46,15 +46,15 @@ In the following example, we have a fictive table containing: Code, Starting Dat
|
|||
You will need to create the view before you define the Table Object.
|
||||
You will need to create a view for every company in the database.
|
||||
```sql
|
||||
CREATE VIEW [dbo].[CRONUS$PriceView]
|
||||
AS
|
||||
SELECT [Code], [Starting Date], [Price]
|
||||
FROM dbo.[CRONUS$Price] AS A
|
||||
WHERE [Starting Date] =
|
||||
(SELECT MAX([Starting Date])
|
||||
FROM dbo.[CRONUS$Price] AS B
|
||||
WHERE B.[Code] = A.[Code] AND
|
||||
B.[Starting Date] <= GETDATE())
|
||||
CREATE VIEW [dbo].[CRONUS$PriceView]
|
||||
AS
|
||||
SELECT [Code], [Starting Date], [Price]
|
||||
FROM dbo.[CRONUS$Price] AS A
|
||||
WHERE [Starting Date] =
|
||||
(SELECT MAX([Starting Date])
|
||||
FROM dbo.[CRONUS$Price] AS B
|
||||
WHERE B.[Code] = A.[Code] AND
|
||||
B.[Starting Date] <= GETDATE())
|
||||
```
|
||||
|
||||
Test the view to ensure that you get the correct result. It is much easier to test now than later.
|
||||
|
|
@ -66,7 +66,7 @@ Remember to set the link table property before you save it.
|
|||
### 3. Implement the code
|
||||
|
||||
```al
|
||||
IF PriceView.FINDSET THEN // You have them
|
||||
IF PriceView.FINDSET THEN // You have them
|
||||
```
|
||||
|
||||
### 4. Create a deployment codeunit
|
||||
|
|
@ -134,7 +134,6 @@ The pattern does not exist in NAV (yet J). We have used it several times in our
|
|||
|
||||
## Ideas for improvement
|
||||
|
||||
|
||||
Query Object should be able to handle sub-selects and Unions. A simple solution could be to allow the NAV developer to specify the actual Select statement inside the query Object in clear text. Opening up for writing your own queries and map the projection to the Query-defined fields will make the query Object very versatile and remove the pressure from Microsoft trying to create all the different permutations that a select statement can have. Microsoft and others have all tried to create wizards that can create SQL select statement. They all end up having a clear text option.
|
||||
|
||||
An alternative would be better support for linked table objects, specifically views. The current implementation is very fragile.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ This pattern is describing how you can migrate data using the Data Migration Fa
|
|||
|
||||
## Problem:
|
||||
|
||||
Writing migration code from an external source, such as a product from a competitor, can be time consuming, as you need to tackle the problems of what to migrate, in which order, exception handling... and can result in code that is fragile due to deep dependencies on the NAV data model (high coupling). Any change to the NAV data model can easily break this code. ****
|
||||
Writing migration code from an external source, such as a product from a competitor, can be time consuming, as you need to tackle the problems of what to migrate, in which order, exception handling... and can result in code that is fragile due to deep dependencies on the NAV data model (high coupling). Any change to the NAV data model can easily break this code.
|
||||
|
||||
## Solution:
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ The façade framework has the following components:
|
|||
|
||||
* A page where you can view the status and progress of the migration. It shows one line for each master data entity (item, customer, vendor, general ledger account) that was chosen for migration. You can also stop a migration by choosing the **Stop Migration** action.
|
||||
|
||||
* * **Data Migration Overview** (page 1799)
|
||||
* **Data Migration Overview** (page 1799)
|
||||
|
||||
## Usage:
|
||||
|
||||
|
|
@ -47,10 +47,10 @@ There are the following use cases:
|
|||
|
||||
To initialize and start the data migration, you must call the following procedures:
|
||||
|
||||
* **"Data Migration Status Façade".InitStatusLine('My Migration Type',Database::Item,42000,0,Codeunit::"My extension Item migration codeunit") **
|
||||
* **"Data Migration Status Façade".InitStatusLine('My Migration Type',Database::Item,42000,0,Codeunit::"My extension Item migration codeunit")**
|
||||
This deletes existing status lines for migrating Items for 'My Migration Type', and initializes a new status line with 0 records migrated out of 42000\.
|
||||
|
||||
**"Data Migration Façade".StartMigration('My Migration Type',false) **
|
||||
**"Data Migration Façade".StartMigration('My Migration Type',false)**
|
||||
This starts the migration. False means this is not a retry. A re-try is when you migrate one or more records from the **Show Errors** page, which is described later in this document. Retry = true is only used by the **Show Errors** page and should not be used from extensions.
|
||||
|
||||
### Usage without staging tables:
|
||||
|
|
@ -70,56 +70,61 @@ The overall workflow is:
|
|||
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
_Figure 1: sequence diagram of the data migration without staging tables _
|
||||
_Figure 1: sequence diagram of the data migration without staging tables_
|
||||
|
||||
The following example shows how to migrate items without staging tables:
|
||||
```al
|
||||
trigger OnRun();
|
||||
var
|
||||
ItemDataMigrationFacade: Codeunit "Item Data Migration Facade";
|
||||
ItemNumber: Integer;
|
||||
ItemJson: Text;
|
||||
begin
|
||||
// loop on items retrieved through a web service for example
|
||||
for ItemNumber := 1 to ExternalWebService.GetItemCount do begin
|
||||
ExternalWebService.GetItem(ItemNumber,ItemJson);
|
||||
// create item using the facade
|
||||
if not ItemDataMigrationFacade.CreateItemIfNeeded(ItemJson.ItemNumber,ItemJson.ItemName1,
|
||||
ItemJson.ItemName2,ConvertItemType(ItemJson.ItemType)) then
|
||||
exit; // item already exists
|
||||
// set some fields using the facade
|
||||
ItemDataMigrationFacade.SetVendorItemNo(ItemJson.VendItemNumber);
|
||||
ItemDataMigrationFacade.SetUnitVolume(ItemJson.Volume);
|
||||
ItemDataMigrationFacade.SetAlternativeItemNo(ItemJson.AltItemNumber);
|
||||
if ItemJson.PrimaryVendor <\> '' then
|
||||
ItemDataMigrationFacade.SetVendorNo(ItemJson.PrimaryVendor);
|
||||
// migrate dependencies
|
||||
MigrateItemUnitOfMeasure(ItemDataMigrationFacade,ItemJson);
|
||||
// modify the item (+run trigger) to save the changes made by setters
|
||||
ItemDataMigrationFacade.ModifyItem(true);
|
||||
// update the status in the migration dashboard
|
||||
DataMigrationStatusFacade.IncrementMigratedRecordCount('My Migration Type',
|
||||
Database::Item,1);
|
||||
end;
|
||||
end;
|
||||
procedure MigrateItemUnitOfMeasure(ItemDataMigrationFacade : Codeunit "Item Data Migration Facade";
|
||||
ItemJson : Text);
|
||||
var
|
||||
MyUnitCodeStagingTable: Record "My Unit Code Staging Table";
|
||||
DataMigrationStatusFacade: Codeunit "Data Migration Status Facade";
|
||||
DescriptionToSet: Text\[10\];
|
||||
UnitCodeJson: Text;
|
||||
begin
|
||||
if ItemJson.UnitCode = '' then
|
||||
// log an error using the Data migration façade
|
||||
DataMigrationStatusFacade.RegisterErrorNoStagingTablesCase(
|
||||
'My Migration Type',Database::Item,'Unit of measure is empty.');
|
||||
if ExternalWebService.GetUnitCode(ItemJson.UnitCode,UnitCodeJson) then
|
||||
DescriptionToSet := UnitCodeJson.Description;
|
||||
ItemDataMigrationFacade.CreateUnitOfMeasureIfNeeded(ItemJson.UnitCode, DescriptionToSet);
|
||||
// set the unit of measure on the item
|
||||
ItemDataMigrationFacade.SetBaseUnitOfMeasure(ItemJson.UnitCode);
|
||||
end;
|
||||
trigger OnRun();
|
||||
var
|
||||
ItemDataMigrationFacade: Codeunit "Item Data Migration Facade";
|
||||
ItemNumber: Integer;
|
||||
ItemJson: Text;
|
||||
begin
|
||||
// loop on items retrieved through a web service for example
|
||||
for ItemNumber := 1 to ExternalWebService.GetItemCount do begin
|
||||
ExternalWebService.GetItem(ItemNumber,ItemJson);
|
||||
|
||||
// create item using the facade
|
||||
if not ItemDataMigrationFacade.CreateItemIfNeeded(ItemJson.ItemNumber,ItemJson.ItemName1,
|
||||
ItemJson.ItemName2,ConvertItemType(ItemJson.ItemType)) then
|
||||
exit; // item already exists
|
||||
|
||||
// set some fields using the facade
|
||||
ItemDataMigrationFacade.SetVendorItemNo(ItemJson.VendItemNumber);
|
||||
ItemDataMigrationFacade.SetUnitVolume(ItemJson.Volume);
|
||||
ItemDataMigrationFacade.SetAlternativeItemNo(ItemJson.AltItemNumber);
|
||||
if ItemJson.PrimaryVendor <> '' then
|
||||
ItemDataMigrationFacade.SetVendorNo(ItemJson.PrimaryVendor);
|
||||
|
||||
// migrate dependencies
|
||||
MigrateItemUnitOfMeasure(ItemDataMigrationFacade,ItemJson);
|
||||
|
||||
// modify the item (+run trigger) to save the changes made by setters
|
||||
ItemDataMigrationFacade.ModifyItem(true);
|
||||
// update the status in the migration dashboard
|
||||
DataMigrationStatusFacade.IncrementMigratedRecordCount('My Migration Type',Database::Item,1);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure MigrateItemUnitOfMeasure(ItemDataMigrationFacade : Codeunit "Item Data Migration Facade"; ItemJson : Text);
|
||||
var
|
||||
MyUnitCodeStagingTable: Record "My Unit Code Staging Table";
|
||||
DataMigrationStatusFacade: Codeunit "Data Migration Status Facade";
|
||||
DescriptionToSet: Text[10];
|
||||
UnitCodeJson: Text;
|
||||
begin
|
||||
if ItemJson.UnitCode = '' then
|
||||
// log an error using the Data migration façade
|
||||
DataMigrationStatusFacade.RegisterErrorNoStagingTablesCase(
|
||||
'My Migration Type',Database::Item,'Unit of measure is empty.');
|
||||
|
||||
if ExternalWebService.GetUnitCode(ItemJson.UnitCode,UnitCodeJson) then
|
||||
DescriptionToSet := UnitCodeJson.Description;
|
||||
ItemDataMigrationFacade.CreateUnitOfMeasureIfNeeded(ItemJson.UnitCode, DescriptionToSet);
|
||||
|
||||
// set the unit of measure on the item
|
||||
ItemDataMigrationFacade.SetBaseUnitOfMeasure(ItemJson.UnitCode);
|
||||
end;
|
||||
```
|
||||
|
||||
_Figure 2: Example of Item and Item Unit of Measure migration without staging tables_
|
||||
|
|
@ -136,7 +141,7 @@ The overall workflow is:
|
|||
* Subscribe to the events to migrate entities and their dependencies.
|
||||
* From the event subscribers, call the façade procedures to create entities and set their field values:
|
||||
|
||||
* * **"Item Data Migration Façade".CreateItemIfNeeded('ITEM042','My Item Description';'My Item Description 2';ItemTypeToSet::Inventory)**
|
||||
* **"Item Data Migration Façade".CreateItemIfNeeded('ITEM042','My Item Description';'My Item Description 2';ItemTypeToSet::Inventory)**
|
||||
* **"Item Data Migration Façade".SetBaseUnitOfMeasure('BOX')**
|
||||
* **"Item Data Migration Façade".ModifyItem(true)**
|
||||
|
||||
|
|
@ -146,66 +151,72 @@ _Figure 3: Simplified sequence diagram of the data migration with staging tables
|
|||
|
||||
Below is a simplified example showing how to create an item:
|
||||
```al
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItem', '', true, true)]
|
||||
procedure OnMigrateItem(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
||||
var
|
||||
MyItemStagingTable : Record "My Item Staging Table";
|
||||
begin
|
||||
// handle the event if it targets this extension's staging table
|
||||
if RecordIdToMigrate.TableNo <\> Database::"My Item Staging Table" then
|
||||
exit;
|
||||
MyItemStagingTable.Get(RecordIdToMigrate);
|
||||
// create item using the facade
|
||||
if not Sender.CreateItemIfNeeded(MyItemStagingTable.ItemNumber,MyItemStagingTable.ItemName1,
|
||||
MyItemStagingTable.ItemName2,ConvertItemType(MyItemStagingTable.ItemType)) then
|
||||
exit; // item already exists
|
||||
// set some fields using the facade
|
||||
Sender.SetVendorItemNo(MyItemStagingTable.VendItemNumber);
|
||||
Sender.SetUnitVolume(MyItemStagingTable.Volume);
|
||||
Sender.SetAlternativeItemNo(MyItemStagingTable.AltItemNumber);
|
||||
if MyItemStagingTable.PrimaryVendor <\> '' then
|
||||
Sender.SetVendorNo(MyItemStagingTable.PrimaryVendor);
|
||||
// modify the item (+run trigger) to save the changes made by setters
|
||||
Sender.ModifyItem(true);
|
||||
end;
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItem', '', true, true)]
|
||||
procedure OnMigrateItem(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
||||
var
|
||||
MyItemStagingTable : Record "My Item Staging Table";
|
||||
begin
|
||||
// handle the event if it targets this extension's staging table
|
||||
if RecordIdToMigrate.TableNo <> Database::"My Item Staging Table" then
|
||||
exit;
|
||||
MyItemStagingTable.Get(RecordIdToMigrate);
|
||||
|
||||
// create item using the facade
|
||||
if not Sender.CreateItemIfNeeded(MyItemStagingTable.ItemNumber,MyItemStagingTable.ItemName1,
|
||||
MyItemStagingTable.ItemName2,ConvertItemType(MyItemStagingTable.ItemType)) then
|
||||
exit; // item already exists
|
||||
|
||||
// set some fields using the facade
|
||||
Sender.SetVendorItemNo(MyItemStagingTable.VendItemNumber);
|
||||
Sender.SetUnitVolume(MyItemStagingTable.Volume);
|
||||
Sender.SetAlternativeItemNo(MyItemStagingTable.AltItemNumber);
|
||||
if MyItemStagingTable.PrimaryVendor <> '' then
|
||||
Sender.SetVendorNo(MyItemStagingTable.PrimaryVendor);
|
||||
|
||||
// modify the item (+run trigger) to save the changes made by setters
|
||||
Sender.ModifyItem(true);
|
||||
end;
|
||||
```
|
||||
|
||||
_Figure 4: Example of event subscriber for Item migration_
|
||||
|
||||
|
||||
Below is another example showing how to use additional events to set fields that reference other tables, here the unit of measure:
|
||||
|
||||
```al
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItemUnitOfMeasure', '', true, true)]
|
||||
procedure OnMigrateItemUnitOfMeasure(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
||||
var
|
||||
MyItemStagingTable : Record "My Item Staging Table";
|
||||
MyUnitCodeStagingTable : Record "My Unit Code Staging Table";
|
||||
DescriptionToSet: Text[10];
|
||||
begin
|
||||
// handle the event if it targets this extension's staging table
|
||||
if RecordIdToMigrate.TableNo <\> Database::"My Item Staging Table" then
|
||||
exit;
|
||||
MyItemStagingTable.Get(RecordIdToMigrate);
|
||||
if MyItemStagingTable.UnitCode = '' then
|
||||
error('Unit of measure is empty.');
|
||||
MyUnitCodeStagingTable.SetRange(UnitCode,MyItemStagingTable.UnitCode);
|
||||
if MyUnitCodeStagingTable.FindFirst then
|
||||
DescriptionToSet := MyUnitCodeStagingTable.Description;
|
||||
// create the unit of measure through the facade
|
||||
Sender.CreateUnitOfMeasureIfNeeded(MyItemStagingTable.UnitCode, DescriptionToSet);
|
||||
// set the unit of measure on the item
|
||||
Sender.SetBaseUnitOfMeasure(MyItemStagingTable.UnitCode);
|
||||
// modify the item to save the changes made by setter
|
||||
Sender.ModifyItem(false);
|
||||
end;
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItemUnitOfMeasure', '', true, true)]
|
||||
procedure OnMigrateItemUnitOfMeasure(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
||||
var
|
||||
MyItemStagingTable : Record "My Item Staging Table";
|
||||
MyUnitCodeStagingTable : Record "My Unit Code Staging Table";
|
||||
DescriptionToSet: Text[10];
|
||||
begin
|
||||
// handle the event if it targets this extension's staging table
|
||||
if RecordIdToMigrate.TableNo <> Database::"My Item Staging Table" then
|
||||
exit;
|
||||
|
||||
MyItemStagingTable.Get(RecordIdToMigrate);
|
||||
if MyItemStagingTable.UnitCode = '' then
|
||||
error('Unit of measure is empty.');
|
||||
MyUnitCodeStagingTable.SetRange(UnitCode,MyItemStagingTable.UnitCode);
|
||||
if MyUnitCodeStagingTable.FindFirst then
|
||||
DescriptionToSet := MyUnitCodeStagingTable.Description;
|
||||
|
||||
// create the unit of measure through the facade
|
||||
Sender.CreateUnitOfMeasureIfNeeded(MyItemStagingTable.UnitCode, DescriptionToSet);
|
||||
|
||||
// set the unit of measure on the item
|
||||
Sender.SetBaseUnitOfMeasure(MyItemStagingTable.UnitCode);
|
||||
|
||||
// modify the item to save the changes made by setter
|
||||
Sender.ModifyItem(false);
|
||||
end;
|
||||
```
|
||||
|
||||
_Figure 5: Example of event subscriber for Item Unit of Measure migration_
|
||||
|
||||
### Combining both approaches:
|
||||
|
||||
If you want to migrate additional entities, the **Data Migration **framework lets you initialize the migration with entities other than master data. In this case, the **Data Migration Overview** page will show additional lines. Item, vendor, customer, an general ledger accounts are migrated with an event driven approach, and the additional entities are migrated by calling an extension codeunit **OnRun** method.
|
||||
If you want to migrate additional entities, the **Data Migration** framework lets you initialize the migration with entities other than master data. In this case, the **Data Migration Overview** page will show additional lines. Item, vendor, customer, an general ledger accounts are migrated with an event driven approach, and the additional entities are migrated by calling an extension codeunit **OnRun** method.
|
||||
|
||||
## Error handling with staging tables:
|
||||
|
||||
|
|
@ -215,17 +226,13 @@ The migration starts by calling **RUN** on the façade codeunit. Errors thrown d
|
|||
|
||||
_Figure 6: List of errors shown when clicking **Show Errors** on the **Data Migration Overview** page_
|
||||
|
||||
__
|
||||
|
||||
The **Edit Record** action opens a view of the staging table, where you can edit fields to fix errors. Figure 4 shows an example of a page for the vendor staging table in a migration from C5\.
|
||||
|
||||
[![ ][image4]][anchor4]
|
||||
|
||||
_Figure 7: Edit a staging table record_
|
||||
|
||||
__
|
||||
|
||||
The **Staging Table ID** determines the page to open, so it is important that the page ID is equal to the **Staging Table ID**, at least for the master data staging tables, for example, for**G/L Accounts, Items, Customers** and **Vendor**. You should ensure that pages to edit related entities are linked on this page by means of new actions. For example, Figure 4 uses the **C5 Purchaser** action.
|
||||
The **Staging Table ID** determines the page to open, so it is important that the page ID is equal to the **Staging Table ID**, at least for the master data staging tables, for example, for **G/L Accounts, Items, Customers** and **Vendor**. You should ensure that pages to edit related entities are linked on this page by means of new actions. For example, Figure 4 uses the **C5 Purchaser** action.
|
||||
|
||||
After you fix the staging table record, you can choose the **Migrate** action to mark the selected records as records to retry, and then run **StartMigration** with the **Retry** flag set to true. This is the only place where the retry flag should be set to true in the **StartMigration** procedure.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,15 @@ The pattern is most easily described when you look at an example. This example i
|
|||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
The goal of this functionality is to:
|
||||
• List all the different connections to external services,
|
||||
• Have a central place to navigate to the corresponding setup of the service.
|
||||
The goal of this functionality is to:
|
||||
|
||||
* List all the different connections to external services,
|
||||
* Have a central place to navigate to the corresponding setup of the service.
|
||||
|
||||
The functionality (**Service Connections**) itself, is not aware of the state nor setup nor any context of all the different services in the list. All it does is:
|
||||
• It raises an event as an opportunity for all services within the NAV application to subscribe to,
|
||||
• It has a public function **InsertServiceConnection** that the subscribers can use to register itself at the Service Connection.
|
||||
The functionality (**Service Connections**) itself, is not aware of the state nor setup nor any context of all the different services in the list. All it does is:
|
||||
|
||||
* It raises an event as an opportunity for all services within the NAV application to subscribe to,
|
||||
* It has a public function **InsertServiceConnection** that the subscribers can use to register itself at the Service Connection.
|
||||
|
||||
The event **OnRegisterServiceConnection** is raised when the page (1279 - **Service Connections**) is opened.
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ Create the page for "Document Header" Table with the wizard by selecting the Doc
|
|||
|
||||
Then simply add your subpage as new line in the designer, and adjust the property "SubFormPerLink" with "Document No.=FIELD(No.)" to link header and lines :
|
||||
|
||||
**[![ ][image1]][anchor1]**
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
**Page "Document List":** Use the Page wizard to create a List page based on the Document table and add fields, FactBox (RecordLinks, Notes...), etc.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ To guide users, we can prompt them with a dialog on which they can update the se
|
|||
Define two functions in the setup or supplementary table: One for verifying if the needed information is available, and another for exposing the page that contains the fields that the user must update.
|
||||
|
||||
Call the code. For example:
|
||||
```AL
|
||||
```
|
||||
Local IsXAvailable : Boolean
|
||||
If field X <\> '' then
|
||||
If field X <> '' then
|
||||
Exit(True)
|
||||
Exit(false)
|
||||
|
||||
|
|
@ -31,9 +31,11 @@ Open the card page in edit mode
|
|||
|
||||
If not IsXAvailable then
|
||||
Error(Field X is missing a value. Please correct it.)
|
||||
```
|
||||
|
||||
The calling code
|
||||
|
||||
```
|
||||
..
|
||||
|
||||
SetupTable.VerifyAndSetX
|
||||
|
|
@ -42,21 +44,21 @@ SetupTable.VerifyAndSetX
|
|||
```
|
||||
## NAV Specific Example
|
||||
|
||||
****
|
||||
|
||||
In the **Sales & Receivables Setup** table (311) for the DK version, the following procedures have been added:
|
||||
|
||||
```al
|
||||
Local Procedure IsOIOUBLPathAvailable(...)
|
||||
|
||||
Procedure VerifyAndSetOIOUBLPath(...)
|
||||
```
|
||||
|
||||
The code in the **Sales & Receivables Setup** table can now be called directly from the related processing codeunit, such as the **Sales-Post + Print** codeunit (82).
|
||||
|
||||
Were the code is called:
|
||||
```AL
|
||||
IF ("EAN No." <\> '') THEN
|
||||
|
||||
SalesSetup.VerifyAndSetOIOUBLPathSetup(SalesHeader."Document Type");
|
||||
```al
|
||||
IF ("EAN No." <> '') THEN
|
||||
SalesSetup.VerifyAndSetOIOUBLPathSetup(SalesHeader."Document Type");
|
||||
```
|
||||
|
||||
If the setup is not updated properly, the user is prompted to update it as follows.
|
||||
|
|
|
|||
|
|
@ -53,13 +53,14 @@ When the processing is complete, you can check if any error messages of type "Er
|
|||
The code below is an example of how the error message component was used in one part of the before mentioned Mexican feature. This code iterates over all G/L Accounts and pipes information out into an XML file. While doing so, it is validated that all mandatory fields have values and meet certain conditions. And only if that is the case, is the XML document actually exported. Also notice, that an error message is logged, in case no G/L Accounts are found given the provided filters. That way, the user can be guided to setup the system correctly.
|
||||
|
||||
```AL
|
||||
PROCEDURE ExportChartOfAccounts@1(Year@1000 : Integer;Month@1001 : Integer);
|
||||
VAR
|
||||
TempErrorMessage@1003 : TEMPORARY Record 700;
|
||||
BEGIN
|
||||
PROCEDURE ExportChartOfAccounts@1(Year@1000 : Integer;Month@1001 : Integer);
|
||||
VAR
|
||||
TempErrorMessage@1003 : TEMPORARY Record 700;
|
||||
BEGIN
|
||||
TempErrorMessage.ClearLog; // only necessary if variable is global
|
||||
...
|
||||
CreateXMLHeader(Document,RootNode,CatalogoNodeTxt,Namespace,Year,Month,'1.1');
|
||||
|
||||
IF GLAccount.FINDSET THEN BEGIN
|
||||
REPEAT
|
||||
TempErrorMessage.LogIfEmpty (GLAccount,GLAccount.FIELDNO(Name),TempErrorMessage."Message Type"::Error);
|
||||
|
|
@ -73,18 +74,18 @@ The code below is an example of how the error message component was used in one
|
|||
GLAccount."Debit/Credit"::Credit:
|
||||
XMLDOMManagement.AddAttribute(Node,'Natur','A');
|
||||
ELSE
|
||||
TempErrorMessage.LogMessage(
|
||||
GLAccount,GLAccount.FIELDNO("Debit/Credit"),TempErrorMessage."Message Type"::Error,
|
||||
STRSUBSTNO(GLAccountTypeErr,GLAccount."Debit/Credit",GLAccount.RECORDID));
|
||||
TempErrorMessage.LogMessage(
|
||||
GLAccount,GLAccount.FIELDNO("Debit/Credit"),TempErrorMessage."Message Type"::Error,
|
||||
STRSUBSTNO(GLAccountTypeErr,GLAccount."Debit/Credit",GLAccount.RECORDID));
|
||||
END;
|
||||
UNTIL GLAccount.NEXT = 0;
|
||||
END ELSE
|
||||
TempErrorMessage.LogSimpleMessage(TempErrorMessage."Message Type"::Error,NoSATAccountDefinedErr);
|
||||
|
||||
|
||||
IF NOT TempErrorMessage.HasErrors(TRUE) THEN
|
||||
SaveXMLToClient(Document,Year,Month,'CT');
|
||||
TempErrorMessage.ShowErrorMessages(FALSE);
|
||||
END;
|
||||
TempErrorMessage.ShowErrorMessages(FALSE);
|
||||
END;
|
||||
```
|
||||
|
||||
One could also do pre-processing in a function of its own, and only if the pre-processing results in no error messages of type "Error" would the processing continue.
|
||||
|
|
@ -95,9 +96,9 @@ By using this easy to use component, we have the possibility to extend this func
|
|||
|
||||
|
||||
|
||||
[anchor0]: /nav/w/designpatterns/124.journal-error-processing.aspx
|
||||
[anchor0]: /navpatterns/1-patterns/journal-error-processing/
|
||||
[anchor1]: image001.png
|
||||
[anchor2]: /nav/w/designpatterns/104.easy-update-of-setup-or-supplementary-information.aspx
|
||||
[anchor2]: /navpatterns/1-patterns/easy-update-of-setup-or-supplementary-information/
|
||||
[anchor3]: image003.png
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ weight = 560
|
|||
+++
|
||||
_By David Bastide at Microsoft Development Center Copenhagen_
|
||||
|
||||
_[![ ][image0]][anchor0]_
|
||||
|
||||
__
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
## Context
|
||||
|
||||
|
|
@ -19,14 +17,9 @@ This document provides an elegant and extensible pattern about how to extend the
|
|||
|
||||
The Dynamics 365 Business Central release (April 2018) introduces a new HeadlinePart page type. This page type defines a page that rotates a display of several headlines after another, in the web client. A user can also click to switch to another headline. Headlines can also include a drilldown action that will be invoked when the user clicks the headline Text of the payload can be emphasized.
|
||||
Headlines are divided in 2 parts: the qualifier, and the payload as you can see in the figure below.
|
||||
_
|
||||
_
|
||||
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
< --\[if gte mso 9\]\>< \[endif\]--\>< --\[if gte mso 9\]\>< \[endif\]--\>< --\[if gte mso 9\]\>< \[endif\]--\>< --\[if gte mso 10\]\>
|
||||
< \[endif\]--\>
|
||||
|
||||
_Figure 1: Qualifier, Payload and emphasized text._
|
||||
|
||||
## Usage
|
||||
|
|
@ -61,89 +54,88 @@ To format headlines, you should use Codeunit 1439 Headline Management functions:
|
|||
|
||||
### Examples:
|
||||
|
||||
#### 1\. Extending the page with a new headline:
|
||||
|
||||
group(LargestSale)
|
||||
{
|
||||
#### 1. Extending the page with a new headline:
|
||||
```al
|
||||
group(LargestSale)
|
||||
{
|
||||
Visible = LargestSaleVisible;
|
||||
ShowCaption=false;
|
||||
Editable=false;
|
||||
|
||||
field(LargestSaleText;LargestSaleText)
|
||||
{
|
||||
ApplicationArea = Basic, Suite;
|
||||
DrillDown=true;
|
||||
|
||||
trigger OnDrillDown()
|
||||
var
|
||||
EssentialBusHeadlineMgt: Codeunit "Essential Bus. Headline Mgt.";
|
||||
begin
|
||||
EssentialBusHeadlineMgt.OnDrillDownLargestSale();
|
||||
end;
|
||||
ApplicationArea = Basic, Suite;
|
||||
DrillDown=true;
|
||||
|
||||
trigger OnDrillDown()
|
||||
var
|
||||
EssentialBusHeadlineMgt: Codeunit "Essential Bus. Headline Mgt.";
|
||||
begin
|
||||
EssentialBusHeadlineMgt.OnDrillDownLargestSale();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
####
|
||||
|
||||
#### 2\. Subscribing to the OnComputeHeadlines event, and computing headlines
|
||||
|
||||
\[EventSubscriber(ObjectType::Codeunit, Codeunit::"Headline RC Business Manager", 'OnComputeHeadlines', '', true, true)\]
|
||||
#### 2. Subscribing to the OnComputeHeadlines event, and computing headlines
|
||||
```al
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Headline RC Business Manager", 'OnComputeHeadlines', '', true, true)]
|
||||
procedure OnComputeHeadlinesBusinessManager()
|
||||
begin
|
||||
// \[...\] compute headline, and init the EssentialBusinessHeadline record
|
||||
// [...] compute headline, and init the EssentialBusinessHeadline record
|
||||
if not ShowHeadline then
|
||||
exit; // not enough data to compute headline
|
||||
exit; // not enough data to compute headline
|
||||
|
||||
if not HeadlineManagement.GetHeadlineText(
|
||||
'Insight from last week',
|
||||
StrSubstNo('The largest posted sales invoice was for %1',
|
||||
HeadlineManagement.Emphasize(Format(CustomerLedgerEntry.Amount, 0, TypeHelper.GetAmountFormatWithUserLocale('$'))))
|
||||
EssentialBusinessHeadline."Headline Text")
|
||||
'Insight from last week',
|
||||
StrSubstNo('The largest posted sales invoice was for %1',
|
||||
HeadlineManagement.Emphasize(Format(CustomerLedgerEntry.Amount, 0, TypeHelper.GetAmountFormatWithUserLocale('$'))))
|
||||
EssentialBusinessHeadline."Headline Text")
|
||||
then
|
||||
exit;
|
||||
exit;
|
||||
|
||||
EssentialBusinessHeadline.Validate("Headline Visible", true);
|
||||
EssentialBusinessHeadline.Modify();
|
||||
end;
|
||||
|
||||
end;
|
||||
```
|
||||
|
||||
####
|
||||
#### 3. Subscribing to the OnIsAnyExtensionHeadlineVisible event
|
||||
|
||||
#### 3\. Subscribing to the OnIsAnyExtensionHeadlineVisible event
|
||||
|
||||
\[EventSubscriber(ObjectType::Page, Page::"Headline RC Business Manager", 'OnIsAnyExtensionHeadlineVisible', '', true, true)\]
|
||||
procedure OnIsAnyExtensionHeadlineVisible(var ExtensionHeadlinesVisible: Boolean)
|
||||
var
|
||||
```al
|
||||
[EventSubscriber(ObjectType::Page, Page::"Headline RC Business Manager", 'OnIsAnyExtensionHeadlineVisible', '', true, true)]
|
||||
procedure OnIsAnyExtensionHeadlineVisible(var ExtensionHeadlinesVisible: Boolean)
|
||||
var
|
||||
EssentialBusinessHeadline: Record "Essential Business Headline";
|
||||
AtLeastOneHeadlineVisible: Boolean;
|
||||
begin
|
||||
begin
|
||||
EssentialBusinessHeadline.SetRange("Headline Visible", true);
|
||||
EssentialBusinessHeadline.SetFilter("Headline Name",'%1|%2|%3|%4|%5',
|
||||
EssentialBusinessHeadline."Headline Name"::LargestOrder,
|
||||
EssentialBusinessHeadline."Headline Name"::LargestSale,
|
||||
EssentialBusinessHeadline."Headline Name"::BusiestResource,
|
||||
EssentialBusinessHeadline."Headline Name"::MostPopularItem,
|
||||
EssentialBusinessHeadline."Headline Name"::SalesIncrease,
|
||||
EssentialBusinessHeadline."Headline Name"::TopCustomer);
|
||||
EssentialBusinessHeadline."Headline Name"::LargestOrder,
|
||||
EssentialBusinessHeadline."Headline Name"::LargestSale,
|
||||
EssentialBusinessHeadline."Headline Name"::BusiestResource,
|
||||
EssentialBusinessHeadline."Headline Name"::MostPopularItem,
|
||||
EssentialBusinessHeadline."Headline Name"::SalesIncrease,
|
||||
EssentialBusinessHeadline."Headline Name"::TopCustomer);
|
||||
|
||||
AtLeastOneHeadlineVisible := not EssentialBusinessHeadline.IsEmpty();
|
||||
// only modify the var if this extension is making some headlines visible, setting to false could override some other extensions setting the value to true
|
||||
if AtLeastOneHeadlineVisible then
|
||||
ExtensionHeadlinesVisible := true;
|
||||
end;
|
||||
|
||||
ExtensionHeadlinesVisible := true;
|
||||
end;
|
||||
```
|
||||
####
|
||||
|
||||
#### 4\. Setting the headline text on the page
|
||||
#### 4. Setting the headline text on the page
|
||||
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
```al
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
EssentialBusinessHeadline.GetHeadline(EssentialBusinessHeadline."Headline Name"::LargestSale);
|
||||
LargestSaleVisible := EssentialBusinessHeadline."Headline Visible";
|
||||
LargestSaleText := EssentialBusinessHeadline."Headline Text";
|
||||
end;
|
||||
end;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
[![ ][image2]][anchor2]
|
||||
|
||||
_Figure 2: Sequence diagram of headline usage_
|
||||
|
|
@ -152,10 +144,6 @@ _Figure 2: Sequence diagram of headline usage_
|
|||
|
||||
* Essential Business Headlines extension
|
||||
|
||||
< --\[if gte mso 9\]\>< \[endif\]--\>< --\[if gte mso 9\]\>< \[endif\]--\>< --\[if gte mso 9\]\>< \[endif\]--\>< --\[if supportAnnotations\]--\>< --\[endif\]--\>< --\[if gte mso 10\]\>
|
||||
< \[endif\]--\>
|
||||
|
||||
|
||||
|
||||
[anchor0]: 3733.logo.png
|
||||
[anchor1]: Headline.png
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ REPEAT
|
|||
|
||||
VendorBillLine.TESTFIELD("Document Type",VendorBillLine."Document Type"::Invoice);
|
||||
|
||||
IF ((VendorBillLine."Vendor No." <\> PrevVendorBillLine."Vendor No.") OR (VendorBillLine."Vendor Bank Acc. No." <\> PrevVendorBillLine."Vendor Bank Acc. No.")) THEN BEGIN InsertTempGenJnlLine(TempGenJnlLine,VendorBillHeader,PrevVendorBillLine,CumulativeAmount);
|
||||
IF ((VendorBillLine."Vendor No." <> PrevVendorBillLine."Vendor No.") OR (VendorBillLine."Vendor Bank Acc. No." <> PrevVendorBillLine."Vendor Bank Acc. No.")) THEN BEGIN InsertTempGenJnlLine(TempGenJnlLine,VendorBillHeader,PrevVendorBillLine,CumulativeAmount);
|
||||
|
||||
CumulativeAmount := VendorBillLine."Amount to Pay";
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ I recommend to use this concept on:
|
|||
**Step 1** - if it doesn't exist yet - you create your Hook Codeunit. As the name assumes .. this is always a codeunit. We apply the following rules to it:
|
||||
|
||||
* One Hook always hooks into one object. Which basically means that I will only declare this new codeunit in one other object (which is its parent object)
|
||||
* The naming convention is: "The\_Original\_Object\_Name Hook". Naming conventions are important, just to find your mapped object, and also to be able to group the Hooks.
|
||||
* The naming convention is: "The_Original_Object_Name Hook". Naming conventions are important, just to find your mapped object, and also to be able to group the Hooks.
|
||||
|
||||
**Step 2**, you create the hook, which is basically a method (function) in your codeunit. The naming is important:
|
||||
|
||||
|
|
@ -72,13 +72,13 @@ Suppose, we want to add business logic just before posting a sales document. In
|
|||
This pattern can be used in all cases to put business logic. But I see three possible approaches
|
||||
|
||||
1. You only declare the most important and most used hooks
|
||||
* This way, you limit the amount of objects and hooks to only a few in the default product
|
||||
* For example, only the OnBeforePostSalesHeader, OnBeforeReleaseSalesDocument, .. And no field validation or such...
|
||||
* This way, you limit the amount of objects and hooks to only a few in the default product
|
||||
* For example, only the OnBeforePostSalesHeader, OnBeforeReleaseSalesDocument, .. And no field validation or such...
|
||||
2. Only at objects of the default application which you are customizing.
|
||||
* This way, you don't want to create hooks for your own objects, only default existing objects.
|
||||
* This way, you don't want to create hooks for your own objects, only default existing objects.
|
||||
3. (recommended) You create hooks on all places you don't want to write business logic, and on all existing objects which you would like to customize.
|
||||
* This is a very consistent way of working, as in any case, your business logic ends up in either a hook, or in its corresponding objects from a specific design pattern. But you know that the entry point is always a hook.
|
||||
* You know what to expect in any case, both changed business logic in existing code and business logic in newly created code is entered from a hook.
|
||||
* This is a very consistent way of working, as in any case, your business logic ends up in either a hook, or in its corresponding objects from a specific design pattern. But you know that the entry point is always a hook.
|
||||
* You know what to expect in any case, both changed business logic in existing code and business logic in newly created code is entered from a hook.
|
||||
|
||||
## Related Topics
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ weight = 630
|
|||
+++
|
||||
_By [Soren Klemmensen][anchor0], [_Partner-Ready-Software_ ][anchor1] & [360 Visibility][anchor2]_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
This Pattern is meant to create generic & reusable links between tables. The goal is to have an easy generic way to link a generically designed sub table to a record on a main table which can be used for other links too.
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ To minimize the impact of customizations and to keep modules as generic and reus
|
|||
* To create a generic and reusable auto generated link (A Surrogate Key), Immune to natural key data & requirement changes, on a main table with minimum impact on the table.
|
||||
* To create generic and reusable sub tables that effortless can be reused anywhere in the application.
|
||||
|
||||
## **Description**
|
||||
## Description
|
||||
|
||||
Over years of development many things are repeated across different implementation and even inside the same application. A typical example could be adding comments to an area just as it is done in Microsoft Dynamics NAV multiple times. There can be reasons for doing this again and again, but not only does this need to be maintained and upgraded over the years, but all the implementations of comments also needs to be tested separately. If a standard and generic comment could be developed and a generic way of connecting it to a main table this could be resolved. This is exactly what this pattern is trying accomplish.
|
||||
|
||||
|
|
@ -21,25 +21,19 @@ Over years of development many things are repeated across different implementati
|
|||
|
||||
_Figure 1: Table structure for linking a Document Header and Line Table with a Document Comment Table._
|
||||
|
||||
__
|
||||
|
||||
_[![ ][image1]][anchor4]_
|
||||
[![ ][image1]][anchor4]
|
||||
|
||||
_Figure 2: Table Structure for linking a Master Data Table with a Master Data Comment Table_
|
||||
|
||||
__
|
||||
|
||||
A typical way of linking a table to master data or to a document has been to use the primary key of the table being linked to. This causes some issues as the linked table now is designed specifically for the main table and it functionality cannot be reused. In case of renames the linked table needs to be renamed too which is costly in processing. Code also needs to be added on the delete trigger of the table to ensure that the attached records get removed if needed which increases the over all footprint of any change.
|
||||
|
||||
_[![ ][image2]][anchor5]_
|
||||
[![ ][image2]][anchor5]
|
||||
|
||||
_Figure 3: A Generic Way of creating a Comment table and linking it in a generic way to the main table no matter what this table might be. The Unique Record Identifier on the main tables is an Integer with AutoIncrement set to yes._
|
||||
|
||||
__
|
||||
|
||||
It is recommended using this pattern in all tables which need sub tables unless specific reasons exists for not doing this.
|
||||
|
||||
## **Usage**
|
||||
## Usage
|
||||
|
||||
**Step 1**: Create a generic Unique Record Identifier in the main table
|
||||
|
||||
|
|
@ -77,13 +71,10 @@ Let's assume we would like to create comment for a larger number of very differe
|
|||
|
||||
**Update the Customer table (Step 1)**: Go to table 18 Customer and add Field 50000 Unique Record Identifier. Set the Property Data Type to Integer, Editable to No & AutoIncrement to Yes. Save the changes.
|
||||
|
||||
_[![ ][image3]][anchor6]_
|
||||
[![ ][image3]][anchor6]
|
||||
|
||||
__
|
||||
_[![ ][image4]][anchor7]
|
||||
|
||||
_[![ ][image4]][anchor7]_
|
||||
|
||||
__
|
||||
|
||||
**Create Comment table (Step 2)**: Create a new table called Comment. Add 3 fields Table No., Unique Record Identifier & Line No. all of Data Type Integer. Make these 3 fields into the primary key for the table. Add a 4 field called Comment with Data Type Text(80). Save the changes.
|
||||
|
||||
|
|
@ -91,34 +82,24 @@ __
|
|||
|
||||
**Create the Comments page (Step 3)**: Start the page wizard and create a Page based on table comment created above as a List only showing the Comment field. Set AutoSplitKey property to Yes on the page.
|
||||
|
||||
__
|
||||
|
||||
_[![ ][image6]][anchor9]_
|
||||
|
||||
__
|
||||
|
||||
**Update the Customer Card (Step 4)**: Add an action to the Customer Card to open the Comments. Go to Actions and create an action called Smart Comment. Populate the properties RunObject, RunPageView and RunPageLink as see in the picture below.
|
||||
|
||||
[![ ][image7]][anchor10]
|
||||
|
||||
__
|
||||
|
||||
**Create a hook OnDatabaseDelete (Step 5)**:
|
||||
|
||||
Create a codeunit 50000 called "ApplicationManagement Hook" with one function called OnAfterOnDatabaseDelete taking the parameter RecRef of Data Type RecordRef. Add it as a global variable to Codeunit 1 ApplicationManagement and call the function as the last line in OnDatabaseDelete. Please read about the hook pattern before implementing it.
|
||||
|
||||
[![ ][image8]][anchor11]
|
||||
|
||||
__
|
||||
|
||||
**Create the code needed to delete comments linked to a deleted record (Step6)**:
|
||||
|
||||
Create a DeleteComments function taking the RecRef of Data Type RecordRef and add the code as shown blow.
|
||||
|
||||
[![ ][image9]][anchor12]
|
||||
|
||||
__
|
||||
|
||||
It is assumed in this example for simplicity that the Field 50000 is reserved across the entire application for the Unique Record Identifier as defined in Step 1\.
|
||||
|
||||
The comments are now fully working if we look away from the fact that we did not deal with a few things specific to Sales orders like RecreateSalesLines function, Archiving and Copy Document. All of which can easily be dealt with.
|
||||
|
|
@ -133,24 +114,22 @@ Upgrade wise there can be an impact if data is moved out of tables to be reinser
|
|||
|
||||
Other risks could be if Transfer Fields are used and data is being copied unintentionally.
|
||||
|
||||
## **NAV Usages**
|
||||
## NAV Usages
|
||||
|
||||
This is a new pattern not currently used in Microsoft Dynamics NAV.
|
||||
|
||||
## **Ideas for improvement**
|
||||
## Ideas for improvement
|
||||
|
||||
The reason for doing it this way is that you now can reuse your code again and again with only creating the same link on other tables instead of building it from scratch each time reducing testing needed and improving the quality of the overall product.
|
||||
|
||||
## **Consequences (When it should not be used)**
|
||||
## Consequences (When it should not be used)
|
||||
|
||||
This can be used on any table when linking anything to it that can be considered a generic module which can be reused. That said it should not be used in cases where there is a risk of tables growing so much that performance could be impacted. There are ways to reduce performance impact when using high volume tables, but that is outside the scope of this pattern.
|
||||
|
||||
## **Related Topics**
|
||||
## Related Topics
|
||||
|
||||
This is related to the hook pattern as far as they both try to reduce the footprint changes have on the standard application, by creating reusable ways to interact with the standard code. That said the hook pattern is more about hooking the functionality to existing while this pattern is more about creating reusable ways of creating functionality.
|
||||
|
||||
__
|
||||
|
||||
|
||||
|
||||
[anchor0]: http://mvp.microsoft.com/en-us/mvp/Soren%20Klemmensen-5001002 "Soren Klemmensen"
|
||||
|
|
|
|||
|
|
@ -74,19 +74,27 @@ Important parts:
|
|||
|
||||
Example of the embed code:
|
||||
|
||||
```js
|
||||
<iframe width="560" height="315" src="//www.youtube.com/embed/7SGp9pA9cAY" frameborder="0" allowfullscreen\></iframe\>
|
||||
```
|
||||
|
||||
You must assign **src attribute** to src of the embed code, for example:
|
||||
|
||||
```al
|
||||
VideoPlayer.SetFrameAttribute('src', 'https://www.youtube.com/embed/7SGp9pA9cAY');
|
||||
```
|
||||
|
||||
Without this, the video will not play. You can use the same function to assign other attributes, for example to remove frame border use:
|
||||
|
||||
```al
|
||||
VideoPlayer.SetFrameAttribute(' frameborder', '0');
|
||||
```
|
||||
|
||||
Height and width should be set by using the following functions, since they ensure that the video will be centered on the page.
|
||||
|
||||
```al
|
||||
VideoPlayerAddIn.SetHeight(Height) and VideoPlayerAddIn.SetWidth(Width);
|
||||
```
|
||||
|
||||
If you would like to reuse the **Mini Video Player Page** page (1395), then use:
|
||||
|
||||
|
|
|
|||
|
|
@ -46,61 +46,31 @@ When the second payment journal line is selected, the FactBox shows errors for t
|
|||
|
||||
In the following table, the Generic Object column contains the objects that you can use as a base for your implementation.
|
||||
|
||||
Generic Object
|
||||
Generic Object | Description | Sample W1 implementation of SEPA Credit Transfer
|
||||
------|------|------
|
||||
Journal Page | This is the journal list page where the user invokes the processing action. | Payment Journal
|
||||
Action on Page | The processing action invoked by the user on the journal list page. | Export Payments to File
|
||||
Errors Page List Part | A FactBox that displays any journal line validation errors.
|
||||
To improve user experience, the developer can highlight the lines with errors in red and conveniently sort the lines with errors at the top. | Payment Journal Errors Part
|
||||
Validation codeunit | Contains code that checks that the journal line contains correct, complete, and coherent data and that the line is ready for whatever process must be done next. | SEPA CT-Check Line
|
||||
Processing codeunit | Executes the processing of the journal lines. | SEPA CT-Export File
|
||||
|
||||
Description
|
||||
**Generic Object:** Journal Error Text Table
|
||||
|
||||
Sample W1 implementation of SEPA Credit Transfer\*
|
||||
|
||||
Journal Page
|
||||
|
||||
This is the journal list page where the user invokes the processing action.
|
||||
|
||||
Payment Journal
|
||||
|
||||
Action on Page
|
||||
|
||||
The processing action invoked by the user on the journal list page.
|
||||
|
||||
Export Payments to File
|
||||
|
||||
Errors Page List Part
|
||||
|
||||
A FactBox that displays any journal line validation errors.
|
||||
|
||||
To improve user experience, the developer can highlight the lines with errors in red and conveniently sort the lines with errors at the top.
|
||||
|
||||
Payment Journal Errors Part
|
||||
|
||||
Validation codeunit
|
||||
|
||||
Contains code that checks that the journal line contains correct, complete, and coherent data and that the line is ready for whatever process must be done next.
|
||||
|
||||
SEPA CT-Check Line
|
||||
|
||||
Processing codeunit
|
||||
|
||||
Executes the processing of the journal lines.
|
||||
|
||||
SEPA CT-Export File
|
||||
|
||||
Journal Error Text Table
|
||||
|
||||
Contains
|
||||
**Description:**
|
||||
|
||||
Contains:
|
||||
* The error messages
|
||||
|
||||
* Link information about where the error messages belong. For example, in table 1228, Payment Jnl. Export Error Text, the error is linked uniquely to a journal line by the following fields:
|
||||
|
||||
* Journal Template Name, with TableRelation="Gen. Journal Template"
|
||||
* Journal Batch Name, with TableRelation="Gen. Journal Batch".Name WHERE (Journal Template Name=FIELD(Journal Template Name))
|
||||
* Link information about where the error messages belong. For example, in table 1228, Payment Jnl. Export Error Text, the error is linked uniquely to a journal line by the following fields:
|
||||
* Journal Template Name, with TableRelation="Gen. Journal Template"
|
||||
* Journal Batch Name, with TableRelation="Gen. Journal Batch".Name WHERE (Journal Template Name=FIELD(Journal Template Name))
|
||||
* Journal Line No.
|
||||
|
||||
Other related information can be added, such as document number of the original source document, if the current journal line originates from a document.
|
||||
|
||||
An extra improvement would be to add a drilldown or a link to the page where the user can fix the error. This would significantly simplify the scenario by excluding manual navigation and investigation by the user to find the page where the error can be fixed.
|
||||
|
||||
Payment Jnl. Export Error Text
|
||||
**Sample W1 implementation of SEPA Credit Transfer:** Payment Jnl. Export Error Text
|
||||
|
||||
\* The W1 implementation of file export for SEPA Credit Transfer contains the generic SEPA functionality. However, due to differences in data models and user scenarios in various country implementations, the selected local versions contain adaptations of the generic functionality.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The role of a journal line is to temporarily hold transaction data until the tra
|
|||
|
||||
Journal templates are used to specify the underlying journal structure and to provide the default information for the journal batches. Journal batches usually serve to group journal lines, such as lines created by two different users.
|
||||
|
||||
_[![ ][image0]][anchor0]_
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
## Description
|
||||
|
||||
|
|
@ -28,31 +28,13 @@ The journal templates are located on the Journal Template page. A Journal Templa
|
|||
|
||||
The Journal Template table stores the relevant attributes that define the nature and behavior of the journal templates, for example:
|
||||
|
||||
Journal Template Table Field
|
||||
|
||||
Description
|
||||
|
||||
Test Report ID
|
||||
|
||||
The journals offer the possibility of running test reports3. The role of a test report is to simulate the posting process. The verification criteria for the journal lines is ran, and the report can be displayed, all without doing the actual posting. This helps finding and correcting any errors that might exist in the data.
|
||||
|
||||
The name of the test report is the same with the name of the corresponding journal, plus the suffix " - Test". For example, the General Journal has the associated test report named General Journal - Test.
|
||||
|
||||
Posting Report ID
|
||||
|
||||
This report is printed when a user selects Post and Print4.
|
||||
|
||||
Page ID
|
||||
|
||||
For some journals, more UI objects are required. For example, the General Journals have a special page for bank and cash.
|
||||
|
||||
Source Code
|
||||
|
||||
Here you can enter a Trail Code for all the postings done through this Journal4.
|
||||
|
||||
Recurring
|
||||
|
||||
Whenever you post lines from a recurring journal, new lines are automatically created with a posting date defined in the recurring date formula.
|
||||
Journal Template Table Field | Description
|
||||
-----|-----
|
||||
Test Report ID | The journals offer the possibility of running test reports3. The role of a test report is to simulate the posting process. The verification criteria for the journal lines is ran, and the report can be displayed, all without doing the actual posting. This helps finding and correcting any errors that might exist in the data. The name of the test report is the same with the name of the corresponding journal, plus the suffix " - Test". For example, the General Journal has the associated test report named General Journal - Test.
|
||||
Posting Report ID | This report is printed when a user selects Post and Print4.
|
||||
Page ID | For some journals, more UI objects are required. For example, the General Journals have a special page for bank and cash.
|
||||
Source Code | Here you can enter a Trail Code for all the postings done through this Journal4.
|
||||
Recurring | Whenever you post lines from a recurring journal, new lines are automatically created with a posting date defined in the recurring date formula.
|
||||
|
||||
Each journal template defines a default value of those attributes. The values that are defined in a template will be inherited by the journal batches, which will be created from a journal template.
|
||||
|
||||
|
|
@ -72,7 +54,7 @@ During posting, only the information from the journal lines is needed. However,
|
|||
|
||||
Posting creates ledger entries from the temporary content that is stored in the journal lines. Ledger entries are not created directly. Instead, they are posted from journal lines.
|
||||
|
||||
_[![ ][image1]][anchor1]_
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
### Aggregation
|
||||
|
||||
|
|
@ -119,30 +101,35 @@ The General Journals Batches page (251) is linked to the Gen. Journal Batch sour
|
|||
|
||||
Some of the fields in the Gen. Journal Batch table are not editable. Instead, their value is automatically calculated from the parent Gen. Journal Template table. For example, the Recurring field (22) is a FlowField with the following calculation formula:
|
||||
|
||||
Lookup("Gen. Journal Template".Recurring WHERE (Name=FIELD(Journal Template Name)))
|
||||
|
||||
```al
|
||||
Lookup("Gen. Journal Template".Recurring WHERE (Name=FIELD(Journal Template Name)))
|
||||
```
|
||||
|
||||
Similarly, the Template Type field is a FlowField that gets its value from the parent table:
|
||||
|
||||
Lookup("Gen. Journal Template".Type WHERE (Name=FIELD(Journal Template Name)))
|
||||
```al
|
||||
Lookup("Gen. Journal Template".Type WHERE (Name=FIELD(Journal Template Name)))
|
||||
```
|
||||
|
||||
### Setting up a New Batch
|
||||
|
||||
When the user creates a new batch, the following field values are transferred from the Gen. Journal Template table to the Gen. Journal Batch table:
|
||||
|
||||
"Bal. Account Type" := GenJnlTemplate."Bal. Account Type";
|
||||
```al
|
||||
"Bal. Account Type" := GenJnlTemplate."Bal. Account Type";
|
||||
|
||||
"Bal. Account No." := GenJnlTemplate."Bal. Account No.";
|
||||
"Bal. Account No." := GenJnlTemplate."Bal. Account No.";
|
||||
|
||||
"No. Series" := GenJnlTemplate."No. Series";
|
||||
"No. Series" := GenJnlTemplate."No. Series";
|
||||
|
||||
"Posting No. Series" := GenJnlTemplate."Posting No. Series";
|
||||
"Posting No. Series" := GenJnlTemplate."Posting No. Series";
|
||||
|
||||
"Reason Code" := GenJnlTemplate."Reason Code";
|
||||
"Reason Code" := GenJnlTemplate."Reason Code";
|
||||
|
||||
"Copy VAT Setup to Jnl. Lines" := GenJnlTemplate."Copy VAT Setup to Jnl. Lines";
|
||||
"Copy VAT Setup to Jnl. Lines" := GenJnlTemplate."Copy VAT Setup to Jnl. Lines";
|
||||
|
||||
"Allow VAT Difference" := GenJnlTemplate."Allow VAT Difference";
|
||||
"Allow VAT Difference" := GenJnlTemplate."Allow VAT Difference";
|
||||
```
|
||||
|
||||
### Cascade record deletion
|
||||
|
||||
|
|
|
|||
|
|
@ -18,99 +18,74 @@ Examples of Master Data tables are:
|
|||
|
||||
Business Data:
|
||||
|
||||
Table 18: Customer
|
||||
|
||||
Table 23: Vendor
|
||||
|
||||
Table 270: Bank Account
|
||||
|
||||
Table 5050: Contact
|
||||
* Table 18: Customer
|
||||
* Table 23: Vendor
|
||||
* Table 270: Bank Account
|
||||
* Table 5050: Contact
|
||||
|
||||
Product Data:
|
||||
|
||||
Table 15 G/L Account
|
||||
|
||||
Table 27: Item
|
||||
|
||||
Table 156: Resource
|
||||
|
||||
Table 5600: Fixed Asset
|
||||
|
||||
Table 5718: Nonstock Item
|
||||
|
||||
Table 5913: Loaner
|
||||
* Table 15 G/L Account
|
||||
* Table 27: Item
|
||||
* Table 156: Resource
|
||||
* Table 5600: Fixed Asset
|
||||
* Table 5718: Nonstock Item
|
||||
* Table 5913: Loaner
|
||||
|
||||
Master Data has attributes such as Location, Countries & Item Vendors. These tables are closely related to Master Data tables and are called Supplemental and Subsidiary tables.
|
||||
|
||||
Supplemental:
|
||||
|
||||
Table 9: Country/Region
|
||||
|
||||
Table 14: Location
|
||||
* Table 9: Country/Region
|
||||
* Table 14: Location
|
||||
|
||||
Subsidiary:
|
||||
|
||||
Table 99: Item Vendor
|
||||
* Table 99: Item Vendor
|
||||
|
||||
Master Data is among other used in journals, posting routines and historic data like ledger entries.
|
||||
|
||||
**Naming & Conventions**
|
||||
|
||||
****
|
||||
|
||||
Table & Card Page
|
||||
|
||||
Singular
|
||||
* Singular
|
||||
|
||||
Examples: Customer, Vendor & Item
|
||||
* Examples: Customer, Vendor & Item
|
||||
|
||||
List Page
|
||||
|
||||
Plural if Editable = TRUE.
|
||||
* Plural if Editable = TRUE.
|
||||
|
||||
Singular + "List" if Editable = FALSE.
|
||||
* Singular + "List" if Editable = FALSE.
|
||||
|
||||
# Example
|
||||
|
||||
The data entity has a single primary key field with the following properties:
|
||||
|
||||
Field No.
|
||||
|
||||
1
|
||||
|
||||
Name
|
||||
|
||||
"No."
|
||||
|
||||
Date Type
|
||||
|
||||
Code 20
|
||||
|
|
||||
----------|---------
|
||||
Field No. | 1
|
||||
Name | "No."
|
||||
Date Type | Code 20
|
||||
|
||||
Each master data entity has a field which gives a name tag to the data entity carrying the natural name of the entity. This field is called Name if the data entity refers to a living person or an organization, such as a customer or a vendor; it is called Description instead if the data entity does not refer to a person or organization, such as an item. The field has the following properties:
|
||||
|
||||
Name
|
||||
|
||||
Name or Description
|
||||
|
||||
Date Type
|
||||
|
||||
Text 50
|
||||
|
|
||||
----------|---------
|
||||
Name | Name or Description
|
||||
Date Type | Text 50
|
||||
|
||||
An added benefit of using the Name or Description field naming is that this will be part of the dropdown when looking up based on the table relation.
|
||||
|
||||
The table properties of a master data table contain the following entries:
|
||||
|
||||
LookupPageID
|
||||
|
|
||||
------------------|---------
|
||||
LookupPageID | A ListType Page referring to the table which is not editable.
|
||||
DrillDownPageID | The same page as defined in the LookupPageID
|
||||
DataCaptionFields | The primary key plus the field which provides the primary name tag for the record. This will typically be the Name or Description field defined earlier.
|
||||
|
||||
A ListType Page referring to the table which is not editable.
|
||||
|
||||
DrillDownPageID
|
||||
|
||||
The same page as defined in the LookupPageID
|
||||
|
||||
DataCaptionFields
|
||||
|
||||
The primary key plus the field which provides the primary name tag for the record. This will typically be the Name or Description field defined earlier.
|
||||
|
||||
# NAV Specific Example
|
||||
|
||||
|
|
@ -148,27 +123,19 @@ You can see this pattern used in the following tables & pages:
|
|||
|
||||
Business Data:
|
||||
|
||||
Table 18: Customer, Page 21 Customer Card & Page 22 Customer List
|
||||
|
||||
Table 23: Vendor, Page 26 Vendor Card & Page 27 Vendor List
|
||||
|
||||
Table 270: Bank Account, Page 370 Bank Account Card & Page 371 Bank Account List
|
||||
|
||||
Table 5050: Contact, Page 5050 Contact Card & Page 5052 Contact List
|
||||
* Table 18: Customer, Page 21 Customer Card & Page 22 Customer List
|
||||
* Table 23: Vendor, Page 26 Vendor Card & Page 27 Vendor List
|
||||
* Table 270: Bank Account, Page 370 Bank Account Card & Page 371 Bank Account List
|
||||
* Table 5050: Contact, Page 5050 Contact Card & Page 5052 Contact List
|
||||
|
||||
Product Data:
|
||||
|
||||
Table 15 G/L Account, Page 17 G/L Account Card & Page 18 G/L Account List
|
||||
|
||||
Table 27: Item, Page 30 Item Card & Page 31 Item List
|
||||
|
||||
Table 156: Resource, Page 76 Resource Card & Page 77 Resource List
|
||||
|
||||
Table 5600: Fixed Asset, Page 5600 Fixed Asset Card & Page 5601 Fixed Asset List
|
||||
|
||||
Table 5718: Nonstock Item, Page 5725 Nonstock Item Card & Page 5726 Nonstock Item List
|
||||
|
||||
Table 5913: Loaner, Page 5922 Loaner Card & Page 5923 Loaner List
|
||||
* Table 15 G/L Account, Page 17 G/L Account Card & Page 18 G/L Account List
|
||||
* Table 27: Item, Page 30 Item Card & Page 31 Item List
|
||||
* Table 156: Resource, Page 76 Resource Card & Page 77 Resource List
|
||||
* Table 5600: Fixed Asset, Page 5600 Fixed Asset Card & Page 5601 Fixed Asset List
|
||||
* Table 5718: Nonstock Item, Page 5725 Nonstock Item Card & Page 5726 Nonstock Item List
|
||||
* Table 5913: Loaner, Page 5922 Loaner Card & Page 5923 Loaner List
|
||||
|
||||
# Consequences
|
||||
|
||||
|
|
|
|||
|
|
@ -26,40 +26,43 @@ The second step is the actually download of file(s). For the Web client this con
|
|||
|
||||
Code 1: File loop shows an example implementation of this pattern. ServerFileName is generated at the beginning of the report/codeunit, and is the base for GetSeriesFilename. The file that is actually written to during data processing is stored in another variable which holds the output from GetSeriesFilename on the current file number. Note; the example code will only create a ZIP file if there in fact are multiple files to be downloaded.
|
||||
|
||||
IF FileName = '' THEN
|
||||
```al
|
||||
IF FileName = '' THEN
|
||||
ERROR(SupplyFileNameErr);
|
||||
IF ZipMultipleFiles AND (GetTotalNumberOfFiles \> 1) AND FileManagement.IsWebClient
|
||||
THEN BEGIN
|
||||
|
||||
IF ZipMultipleFiles AND (GetTotalNumberOfFiles \> 1) AND FileManagement.IsWebClient
|
||||
THEN BEGIN
|
||||
Basename := FileManagement.GetFileName(FileName);
|
||||
ZipFileName := FileManagement.CreateZipArchiveObject;
|
||||
FOR FileNo := 1 TO GetTotalNumberOfFiles DO
|
||||
FileManagement.AddFileToZipArchive(
|
||||
GetSeriesFilename(ServerFileName,FileNo),GetSeriesFilename(Basename,FileNo));
|
||||
FileManagement.AddFileToZipArchive(
|
||||
GetSeriesFilename(ServerFileName,FileNo),GetSeriesFilename(Basename,FileNo));
|
||||
FileManagement.DownloadHandler(ZipFileName,'','','',
|
||||
STRSUBSTNO('%1.zip',FileManagement.GetFileNameWithoutExtension(FileName)))
|
||||
END ELSE BEGIN
|
||||
STRSUBSTNO('%1.zip',FileManagement.GetFileNameWithoutExtension(FileName)))
|
||||
END ELSE BEGIN
|
||||
IF FileManagement.IsWebClient THEN BEGIN
|
||||
IF GetTotalNumberOfFile \> 1 THEN
|
||||
ERROR(MultipleFilesWebClientErr);
|
||||
FileManagementDownloadHandler(GetSeriesFilename(ServerFileName,1),'','','',
|
||||
FileManagement.GetFileName(FileName));
|
||||
IF GetTotalNumberOfFile > 1 THEN
|
||||
ERROR(MultipleFilesWebClientErr);
|
||||
FileManagementDownloadHandler(GetSeriesFilename(ServerFileName,1),'','','',
|
||||
FileManagement.GetFileName(FileName));
|
||||
END ELSE
|
||||
FOR FileNo := 1 TO GetTotalNumberOfFiles DO
|
||||
FileManagement.DownloadToFile(GetSeriesFilieName(ServerFileName,FileNo),
|
||||
GetSeriesFilename(FileName,FileNo));
|
||||
END;
|
||||
FOR FileNo := 1 TO GetTotalNumberOfFiles DO
|
||||
FileManagement.DownloadToFile(GetSeriesFilieName(ServerFileName,FileNo),
|
||||
GetSeriesFilename(FileName,FileNo));
|
||||
END;
|
||||
```
|
||||
|
||||
_Code 1: File loop_
|
||||
|
||||
__
|
||||
|
||||
The following code is an example implementation of the GetSeriesFilename function. It needs to support the case where no directory is given, only a filename, in order to add files to the root of the ZIP archive. The example will add a file number right before the extension, e.g. C:\\directory\\file.txt will become C:\\directory\\file1.txt etc.
|
||||
|
||||
LOCAL GetSeriesFilename(FileName : Text;FileNo : Integer) : Text
|
||||
IF STRPOS(FileName,'\\') <\> 0 THEN
|
||||
```al
|
||||
LOCAL GetSeriesFilename(FileName : Text;FileNo : Integer) : Text
|
||||
IF STRPOS(FileName,'\\') <> 0 THEN
|
||||
Directory := FileMgt.GetDirectoryName(FileName) + '\\';
|
||||
EXIT(Directory + FileMgt.GetFileNameWithoutExtension(FileName) + FORMAT(FileNo) + '.' +
|
||||
EXIT(Directory + FileMgt.GetFileNameWithoutExtension(FileName) + FORMAT(FileNo) + '.' +
|
||||
FileMgt.GetExtension(FileName));
|
||||
```
|
||||
|
||||
#### _Code 2: GetSeriesFilename_
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@ weight = 810
|
|||
+++
|
||||
_By Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
__
|
||||
|
||||
### **Abstract**
|
||||
### Abstract
|
||||
|
||||
This pattern describes how to open a related document (or card) page from a list page, for the case when there can be more than one pages associated to the rows of the list page.
|
||||
|
||||
### **Description**
|
||||
### Description
|
||||
|
||||
The example below illustrates the connection of a List Page with multiple Document Pages, while the second example links the List Page with Card Pages.
|
||||
|
||||
|
|
@ -23,34 +21,28 @@ The records contained in a list page have an associated page, which is specified
|
|||
|
||||
However, there are situations when rows of the list page can correspond to different pages each (either cards or documents). For example, consider a list page containing 3 rows, requiring the following behaviour:
|
||||
|
||||
Row1
|
||||
|
||||
Opens page ID 1
|
||||
|
||||
Row2
|
||||
|
||||
Opens page ID 2
|
||||
|
||||
Row3
|
||||
|
||||
Opens page ID 3
|
||||
|
|
||||
-----|----------------
|
||||
Row1 | Opens page ID 1
|
||||
Row2 | Opens page ID 2
|
||||
Row3 | Opens page ID 3
|
||||
|
||||
This situation is not handled automatically by NAV. There is no possibility to specify more than one CardPageID in the list page properties. Furthermore, there is no possibility to specify one or more document page IDs on the list page. Therefore, those cases need to be handled explicitly by the C/AL developer.
|
||||
|
||||
### **Usage**
|
||||
### Usage
|
||||
|
||||
The solution used in NAV implementations is at the list page level, as following:
|
||||
|
||||
1. The property CardPageID of the list page remains undefined.
|
||||
2. An action named "Show Document" or "Card" is created on the Navigate tab, with the properties:
|
||||
|
||||
* Image = EditLines
|
||||
* Promoted = Yes
|
||||
* ShortCutKey = Shift+F7
|
||||
* Image = EditLines
|
||||
* Promoted = Yes
|
||||
* ShortCutKey = Shift+F7
|
||||
|
||||
1. The OnAction trigger for the Card action, contains explicit logic to run the targeted card page. It can, for example, be a CASE statement, which invokes PAGE.RUN(...) based on an enumeration field of the current row.
|
||||
3. The OnAction trigger for the Card action, contains explicit logic to run the targeted card page. It can, for example, be a CASE statement, which invokes PAGE.RUN(...) based on an enumeration field of the current row.
|
||||
|
||||
### **NAV Specific Example**
|
||||
### NAV Specific Example
|
||||
|
||||
For example, the NAV page Sales List (page ID 45), which displays the Sales Header Table (table ID 36), chooses which card to open, based on the Document Type field. This is an option field, which can have the following values: Quote, Order, Invoice, Credit Memo, Blanket Order, Return Order. For each document type, the related card page must be opened.
|
||||
|
||||
|
|
@ -58,36 +50,24 @@ For example, the NAV page Sales List (page ID 45), which displays the Sales Head
|
|||
|
||||
For this purpose, a new action ("Card") is added to the Sales List page. The OnAction trigger of this new action contains the page selection logic:
|
||||
|
||||
CASE "Document Type" OF
|
||||
|
||||
```al
|
||||
CASE "Document Type" OF
|
||||
"Document Type"::Quote:
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Quote",Rec);
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Quote",Rec);
|
||||
"Document Type"::Order:
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Order",Rec);
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Order",Rec);
|
||||
"Document Type"::Invoice:
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Invoice",Rec);
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Invoice",Rec);
|
||||
"Document Type"::"Return Order":
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Return Order",Rec);
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Return Order",Rec);
|
||||
"Document Type"::"Credit Memo":
|
||||
PAGE.RUN(PAGE::"Sales Credit Memo",Rec);
|
||||
"Document Type"::"Blanket Order":
|
||||
PAGE.RUN(PAGE::"Blanket Sales Order",Rec);
|
||||
END;
|
||||
```
|
||||
|
||||
PAGE.RUN(PAGE::"Sales Credit Memo",Rec);
|
||||
|
||||
"Document Type"::"Blanket Order":[
|
||||
][anchor2]
|
||||
|
||||
PAGE.RUN(PAGE::"Blanket Sales Order",Rec);
|
||||
|
||||
END;
|
||||
|
||||
### **NAV Usages**[ ][anchor3]
|
||||
### NAV Usages
|
||||
|
||||
Some of the NAV implementations of this pattern can be found in the following pages:
|
||||
|
||||
|
|
|
|||
|
|
@ -47,14 +47,11 @@ After those changes, when the customer (in this case the Danish "Lauritzen Konto
|
|||
Named "<Entity\> Translation" table, where <Entity\> is replaced with the name of the actual object being translated. For the Item example above, this table will be named "Item Translation".
|
||||
|
||||
The table definition contains at a minimum:
|
||||
**Field**
|
||||
**Description**
|
||||
Entity ID field
|
||||
For example, Item No.
|
||||
Language Code
|
||||
Identifies the language of this translation string (for example, "DAN" (Danish), "BGR"(Bulgarian). This is one of the language codes defined in the Languages table.
|
||||
Translation
|
||||
The translated string.
|
||||
Field | Description
|
||||
---------|---------
|
||||
Entity ID field | For example, Item No.
|
||||
Language Code | Identifies the language of this translation string (for example, "DAN" (Danish), "BGR"(Bulgarian). This is one of the language codes defined in the Languages table.
|
||||
Translation | The translated string.
|
||||
|
||||
The table above has a key composed of the first two fields.
|
||||
|
||||
|
|
@ -74,18 +71,13 @@ Some of the NAV implementations of this pattern are:
|
|||
|
||||
## Related Topics
|
||||
|
||||
The **Extended Text** pattern is a more powerful version of the **Multilanguage application data** pattern presented in this section. The main differences are: **Pattern**
|
||||
**Multilanguage Application Data**
|
||||
**Extended Text**
|
||||
**Supports translation of application data**
|
||||
Yes
|
||||
Yes
|
||||
**Format**
|
||||
Single- or multi-line
|
||||
Single-line
|
||||
**Applies to document type**
|
||||
Can choose which document types are affected.
|
||||
All document types are affected.
|
||||
The **Extended Text** pattern is a more powerful version of the **Multilanguage application data** pattern presented in this section. The main differences are:
|
||||
|
||||
Pattern | Multilanguage Application Data | Extended Text
|
||||
--------|--------------------------------|--------------
|
||||
Supports translation of application data | Yes | Yes
|
||||
Format | Single- or multi-line | Single-line
|
||||
Applies to document type | Can choose which document types are affected. | All document types are affected.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,30 +48,40 @@ For instance, if a failure to establish a connection to the web service due to l
|
|||
|
||||
The "External Data Handling Codeunit" is a consumer of **COD1290 Web Service Mgt.** codeunit. If a runtime exception occurs, it gets handled as follows:
|
||||
|
||||
LOCAL PROCEDURE SendDataToConversionService@1(VAR PaymentFileTempBlob@1003 : Record 99008535;BodyTempBlob@1004 : Record 99008535;PostingExch@1007 : Record 1220);
|
||||
VAR
|
||||
```al
|
||||
LOCAL PROCEDURE SendDataToConversionService@1(VAR PaymentFileTempBlob@1003 : Record 99008535;BodyTempBlob@1004 : Record 99008535;PostingExch@1007 : Record 1220);
|
||||
VAR
|
||||
BankDataConvServiceSetup@1000 : Record 1260;
|
||||
WebServiceRequestMgt@1001 : Codeunit 1290;
|
||||
BodyInStream@1005 : InStream;
|
||||
ResponseInStream@1002 : InStream;
|
||||
BEGIN
|
||||
BEGIN
|
||||
IF NOT BodyTempBlob.Blob.HASVALUE THEN
|
||||
ERROR(NoRequestBodyErr);
|
||||
ERROR(NoRequestBodyErr);
|
||||
|
||||
PrepareSOAPRequestBody(BodyTempBlob);
|
||||
|
||||
COMMIT;
|
||||
|
||||
BankDataConvServiceSetup.GET;
|
||||
BodyTempBlob.Blob.CREATEINSTREAM(BodyInStream);
|
||||
WebServiceRequestMgt.SetGlobals(BodyInStream,
|
||||
BankDataConvServiceSetup."Service URL",BankDataConvServiceSetup."User Name",BankDataConvServiceSetup.GetPassword);
|
||||
IF NOT WebServiceRequestMgt.RUN THEN
|
||||
WebServiceRequestMgt.ProcessFaultResponse;
|
||||
WebServiceRequestMgt.GetResponseContent(ResponseInStream);
|
||||
CheckIfErrorsOccurred(ResponseInStream,PostingExch);
|
||||
ReadContentFromResponse(PaymentFileTempBlob,ResponseInStream);
|
||||
END;
|
||||
BankDataConvServiceSetup."Service URL",BankDataConvServiceSetup."User Name",BankDataConvServiceSetup.GetPassword);
|
||||
|
||||
PROCEDURE ProcessFaultResponse@15();
|
||||
VAR
|
||||
IF NOT WebServiceRequestMgt.RUN THEN
|
||||
WebServiceRequestMgt.ProcessFaultResponse;
|
||||
|
||||
WebServiceRequestMgt.GetResponseContent(ResponseInStream);
|
||||
|
||||
CheckIfErrorsOccurred(ResponseInStream,PostingExch);
|
||||
|
||||
ReadContentFromResponse(PaymentFileTempBlob,ResponseInStream);
|
||||
END;
|
||||
```
|
||||
|
||||
```al
|
||||
PROCEDURE ProcessFaultResponse@15();
|
||||
VAR
|
||||
XMLDOMMgt@1006 : Codeunit 6224;
|
||||
DotNetExceptionHandler@1000 : Codeunit 1291;
|
||||
WebException@1005 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.WebException";
|
||||
|
|
@ -80,77 +90,96 @@ The "External Data Handling Codeunit" is a consumer of **COD1290 Web Service Mgt
|
|||
HttpWebResponseError@1007 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpWebResponse";
|
||||
HttpStatusCode@1008 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpStatusCode";
|
||||
ResponseInputStream@1002 : InStream;
|
||||
BEGIN
|
||||
BEGIN
|
||||
DotNetExceptionHandler.Collect;
|
||||
|
||||
IF NOT DotNetExceptionHandler.CastToType(WebException,GETDOTNETTYPE(WebException)) THEN
|
||||
DotNetExceptionHandler.Rethrow;
|
||||
DotNetExceptionHandler.Rethrow;
|
||||
|
||||
IF NOT WebException.Status.Equals(WebExceptionStatus.ProtocolError) THEN
|
||||
ERROR(WebException.Message);
|
||||
ERROR(WebException.Message);
|
||||
|
||||
ResponseInputStream := WebException.Response.GetResponseStream;
|
||||
DebugLogStreamToTempFile(ResponseInputStream,'WebExceptionResponse',TempDebugLogTempBlob);
|
||||
|
||||
HttpWebResponseError := WebException.Response;
|
||||
IF NOT (HttpWebResponseError.StatusCode.Equals(HttpStatusCode.Found) OR
|
||||
HttpWebResponseError.StatusCode.Equals(HttpStatusCode.InternalServerError))
|
||||
HttpWebResponseError.StatusCode.Equals(HttpStatusCode.InternalServerError))
|
||||
THEN
|
||||
ERROR(WebException.Message);
|
||||
ERROR(WebException.Message);
|
||||
|
||||
XmlDoc := XmlDoc.XmlDocument;
|
||||
XmlDoc.Load(ResponseInputStream);
|
||||
ERROR(XMLDOMMgt.FindNodeTextWithNamespace(XmlDoc.DocumentElement,FaultStringXmlPathTxt,'soap',SoapNamespaceTxt));
|
||||
END;
|
||||
|
||||
OBJECT Codeunit 1291 DotNet Exception Handler
|
||||
{
|
||||
ERROR(XMLDOMMgt.FindNodeTextWithNamespace(XmlDoc.DocumentElement,FaultStringXmlPathTxt,'soap',SoapNamespaceTxt));
|
||||
END;
|
||||
```
|
||||
|
||||
```al
|
||||
OBJECT Codeunit 1291 DotNet Exception Handler
|
||||
{
|
||||
OBJECT-PROPERTIES
|
||||
{
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
}
|
||||
PROPERTIES
|
||||
{
|
||||
OnRun=BEGIN
|
||||
END;
|
||||
OnRun=BEGIN
|
||||
END;
|
||||
|
||||
}
|
||||
CODE
|
||||
{
|
||||
VAR
|
||||
OuterException@1000 : DotNet "'mscorlib'.System.Exception";
|
||||
PROCEDURE Catch@3(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type");
|
||||
BEGIN
|
||||
Collect;
|
||||
IF NOT CastToType(Exception,Type) THEN
|
||||
Rethrow;
|
||||
END;
|
||||
PROCEDURE Collect@1();
|
||||
BEGIN
|
||||
OuterException := GETLASTERROROBJECT;
|
||||
END;
|
||||
PROCEDURE TryCastToType@5(Type@1000 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
VAR
|
||||
Exception@1001 : DotNet "'mscorlib'.System.FormatException";
|
||||
BEGIN
|
||||
EXIT(CastToType(Exception,Type));
|
||||
END;
|
||||
PROCEDURE CastToType@2(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
BEGIN
|
||||
Exception := OuterException;
|
||||
REPEAT
|
||||
IF Type.Equals(Exception.GetType()) THEN
|
||||
EXIT(TRUE);
|
||||
Exception := Exception.InnerException;
|
||||
UNTIL ISNULL(Exception);
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
PROCEDURE Rethrow@4();
|
||||
BEGIN
|
||||
IF NOT ISNULL(OuterException.InnerException) THEN
|
||||
ERROR(OuterException.InnerException.Message);
|
||||
ERROR(OuterException.Message);
|
||||
END;
|
||||
BEGIN
|
||||
END.
|
||||
}
|
||||
VAR
|
||||
OuterException@1000 : DotNet "'mscorlib'.System.Exception";
|
||||
|
||||
PROCEDURE Catch@3(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type");
|
||||
BEGIN
|
||||
Collect;
|
||||
IF NOT CastToType(Exception,Type) THEN
|
||||
Rethrow;
|
||||
END;
|
||||
|
||||
PROCEDURE Collect@1();
|
||||
BEGIN
|
||||
OuterException := GETLASTERROROBJECT;
|
||||
END;
|
||||
|
||||
PROCEDURE TryCastToType@5(Type@1000 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
VAR
|
||||
Exception@1001 : DotNet "'mscorlib'.System.FormatException";
|
||||
BEGIN
|
||||
EXIT(CastToType(Exception,Type));
|
||||
END;
|
||||
|
||||
PROCEDURE CastToType@2(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
BEGIN
|
||||
Exception := OuterException;
|
||||
|
||||
REPEAT
|
||||
IF Type.Equals(Exception.GetType()) THEN
|
||||
EXIT(TRUE);
|
||||
Exception := Exception.InnerException;
|
||||
UNTIL ISNULL(Exception);
|
||||
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
|
||||
PROCEDURE Rethrow@4();
|
||||
BEGIN
|
||||
IF NOT ISNULL(OuterException.InnerException) THEN
|
||||
ERROR(OuterException.InnerException.Message);
|
||||
|
||||
ERROR(OuterException.Message);
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
END.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## NAV Usages
|
||||
|
||||
|
|
|
|||
|
|
@ -48,48 +48,66 @@ For instance, if a failure to establish a connection to the web service due to l
|
|||
|
||||
The "External Data Handling Codeunit" is a consumer of **COD1290 Web Service Mgt.** codeunit. If a runtime exception occurs, it gets handled as follows:
|
||||
|
||||
\[TryFunction\]
|
||||
PROCEDURE SendRequestToWebService@17();
|
||||
VAR
|
||||
```al
|
||||
[TryFunction]
|
||||
PROCEDURE SendRequestToWebService@17();
|
||||
VAR
|
||||
WebRequestHelper@1000 : Codeunit 1299;
|
||||
HttpWebRequest@1007 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpWebRequest";
|
||||
HttpStatusCode@1002 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpStatusCode";
|
||||
ResponseHeaders@1001 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Collections.Specialized.NameValueCollection";
|
||||
ResponseInStream@1006 : InStream;
|
||||
BEGIN
|
||||
BEGIN
|
||||
CheckGlobals;
|
||||
|
||||
BuildWebRequest(GlobalURL,HttpWebRequest);
|
||||
|
||||
ResponseInStreamTempBlob.INIT;
|
||||
ResponseInStreamTempBlob.Blob.CREATEINSTREAM(ResponseInStream);
|
||||
CreateSoapRequest(HttpWebRequest.GetRequestStream,GlobalRequestBodyInStream,GlobalUsername,GlobalPassword);
|
||||
WebRequestHelper.GetWebResponse(HttpWebRequest,HttpWebResponse,ResponseInStream,HttpStatusCode,ResponseHeaders,GlobalProgressDialogEnabled);
|
||||
ExtractContentFromResponse(ResponseInStream,ResponseBodyTempBlob);
|
||||
END;
|
||||
|
||||
LOCAL PROCEDURE SendDataToConversionService@1(VAR PaymentFileTempBlob@1003 : Record 99008535;BodyTempBlob@1004 : Record 99008535;PostingExch@1007 : Record 1220);
|
||||
VAR
|
||||
CreateSoapRequest(HttpWebRequest.GetRequestStream,GlobalRequestBodyInStream,GlobalUsername,GlobalPassword);
|
||||
|
||||
WebRequestHelper.GetWebResponse(HttpWebRequest,HttpWebResponse,ResponseInStream,
|
||||
HttpStatusCode,ResponseHeaders,GlobalProgressDialogEnabled);
|
||||
|
||||
ExtractContentFromResponse(ResponseInStream,ResponseBodyTempBlob);
|
||||
END;
|
||||
```
|
||||
|
||||
```al
|
||||
LOCAL PROCEDURE SendDataToConversionService@1(VAR PaymentFileTempBlob@1003 : Record 99008535;BodyTempBlob@1004 : Record 99008535;PostingExch@1007 : Record 1220);
|
||||
VAR
|
||||
BankDataConvServiceSetup@1000 : Record 1260;
|
||||
WebServiceRequestMgt@1001 : Codeunit 1290;
|
||||
BodyInStream@1005 : InStream;
|
||||
ResponseInStream@1002 : InStream;
|
||||
BEGIN
|
||||
BEGIN
|
||||
IF NOT BodyTempBlob.Blob.HASVALUE THEN
|
||||
ERROR(NoRequestBodyErr);
|
||||
ERROR(NoRequestBodyErr);
|
||||
|
||||
PrepareSOAPRequestBody(BodyTempBlob);
|
||||
|
||||
COMMIT;
|
||||
|
||||
BankDataConvServiceSetup.GET;
|
||||
BodyTempBlob.Blob.CREATEINSTREAM(BodyInStream);
|
||||
WebServiceRequestMgt.SetGlobals(BodyInStream,
|
||||
BankDataConvServiceSetup."Service URL",BankDataConvServiceSetup."User Name",BankDataConvServiceSetup.GetPassword);
|
||||
IF NOT WebServiceRequestMgt.SendRequestToWebService THEN
|
||||
WebServiceRequestMgt.ProcessFaultResponse;
|
||||
WebServiceRequestMgt.GetResponseContent(ResponseInStream);
|
||||
CheckIfErrorsOccurred(ResponseInStream,PostingExch);
|
||||
ReadContentFromResponse(PaymentFileTempBlob,ResponseInStream);
|
||||
END;
|
||||
WebServiceRequestMgt.SetGlobals(BodyInStream,BankDataConvServiceSetup."Service URL",
|
||||
BankDataConvServiceSetup."User Name",BankDataConvServiceSetup.GetPassword);
|
||||
|
||||
PROCEDURE ProcessFaultResponse@15();
|
||||
VAR
|
||||
IF NOT WebServiceRequestMgt.SendRequestToWebService THEN
|
||||
WebServiceRequestMgt.ProcessFaultResponse;
|
||||
|
||||
WebServiceRequestMgt.GetResponseContent(ResponseInStream);
|
||||
|
||||
CheckIfErrorsOccurred(ResponseInStream,PostingExch);
|
||||
|
||||
ReadContentFromResponse(PaymentFileTempBlob,ResponseInStream);
|
||||
END;
|
||||
```
|
||||
|
||||
```al
|
||||
PROCEDURE ProcessFaultResponse@15();
|
||||
VAR
|
||||
XMLDOMMgt@1006 : Codeunit 6224;
|
||||
DotNetExceptionHandler@1000 : Codeunit 1291;
|
||||
WebException@1005 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.WebException";
|
||||
|
|
@ -98,77 +116,95 @@ The "External Data Handling Codeunit" is a consumer of **COD1290 Web Service Mgt
|
|||
HttpWebResponseError@1007 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpWebResponse";
|
||||
HttpStatusCode@1008 : DotNet "'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpStatusCode";
|
||||
ResponseInputStream@1002 : InStream;
|
||||
BEGIN
|
||||
BEGIN
|
||||
DotNetExceptionHandler.Collect;
|
||||
|
||||
IF NOT DotNetExceptionHandler.CastToType(WebException,GETDOTNETTYPE(WebException)) THEN
|
||||
DotNetExceptionHandler.Rethrow;
|
||||
DotNetExceptionHandler.Rethrow;
|
||||
|
||||
IF NOT WebException.Status.Equals(WebExceptionStatus.ProtocolError) THEN
|
||||
ERROR(WebException.Message);
|
||||
ERROR(WebException.Message);
|
||||
|
||||
ResponseInputStream := WebException.Response.GetResponseStream;
|
||||
DebugLogStreamToTempFile(ResponseInputStream,'WebExceptionResponse',TempDebugLogTempBlob);
|
||||
|
||||
HttpWebResponseError := WebException.Response;
|
||||
IF NOT (HttpWebResponseError.StatusCode.Equals(HttpStatusCode.Found) OR
|
||||
HttpWebResponseError.StatusCode.Equals(HttpStatusCode.InternalServerError))
|
||||
HttpWebResponseError.StatusCode.Equals(HttpStatusCode.InternalServerError))
|
||||
THEN
|
||||
ERROR(WebException.Message);
|
||||
ERROR(WebException.Message);
|
||||
|
||||
XmlDoc := XmlDoc.XmlDocument;
|
||||
XmlDoc.Load(ResponseInputStream);
|
||||
ERROR(XMLDOMMgt.FindNodeTextWithNamespace(XmlDoc.DocumentElement,FaultStringXmlPathTxt,'soap',SoapNamespaceTxt));
|
||||
END;
|
||||
|
||||
OBJECT Codeunit 1291 DotNet Exception Handler
|
||||
{
|
||||
ERROR(XMLDOMMgt.FindNodeTextWithNamespace(XmlDoc.DocumentElement,FaultStringXmlPathTxt,'soap',SoapNamespaceTxt));
|
||||
END;
|
||||
```
|
||||
|
||||
```al
|
||||
OBJECT Codeunit 1291 DotNet Exception Handler
|
||||
{
|
||||
OBJECT-PROPERTIES
|
||||
{
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
}
|
||||
PROPERTIES
|
||||
{
|
||||
OnRun=BEGIN
|
||||
END;
|
||||
OnRun=BEGIN
|
||||
END;
|
||||
}
|
||||
CODE
|
||||
{
|
||||
VAR
|
||||
OuterException@1000 : DotNet "'mscorlib'.System.Exception";
|
||||
PROCEDURE Catch@3(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type");
|
||||
BEGIN
|
||||
Collect;
|
||||
IF NOT CastToType(Exception,Type) THEN
|
||||
Rethrow;
|
||||
END;
|
||||
PROCEDURE Collect@1();
|
||||
BEGIN
|
||||
OuterException := GETLASTERROROBJECT;
|
||||
END;
|
||||
PROCEDURE TryCastToType@5(Type@1000 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
VAR
|
||||
Exception@1001 : DotNet "'mscorlib'.System.FormatException";
|
||||
BEGIN
|
||||
EXIT(CastToType(Exception,Type));
|
||||
END;
|
||||
PROCEDURE CastToType@2(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
BEGIN
|
||||
Exception := OuterException;
|
||||
REPEAT
|
||||
IF Type.Equals(Exception.GetType()) THEN
|
||||
EXIT(TRUE);
|
||||
Exception := Exception.InnerException;
|
||||
UNTIL ISNULL(Exception);
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
PROCEDURE Rethrow@4();
|
||||
BEGIN
|
||||
IF NOT ISNULL(OuterException.InnerException) THEN
|
||||
ERROR(OuterException.InnerException.Message);
|
||||
ERROR(OuterException.Message);
|
||||
END;
|
||||
BEGIN
|
||||
END.
|
||||
}
|
||||
VAR
|
||||
OuterException@1000 : DotNet "'mscorlib'.System.Exception";
|
||||
|
||||
PROCEDURE Catch@3(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type");
|
||||
BEGIN
|
||||
Collect;
|
||||
IF NOT CastToType(Exception,Type) THEN
|
||||
Rethrow;
|
||||
END;
|
||||
|
||||
PROCEDURE Collect@1();
|
||||
BEGIN
|
||||
OuterException := GETLASTERROROBJECT;
|
||||
END;
|
||||
|
||||
PROCEDURE TryCastToType@5(Type@1000 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
VAR
|
||||
Exception@1001 : DotNet "'mscorlib'.System.FormatException";
|
||||
BEGIN
|
||||
EXIT(CastToType(Exception,Type));
|
||||
END;
|
||||
|
||||
PROCEDURE CastToType@2(VAR Exception@1002 : DotNet "'mscorlib'.System.FormatException";Type@1007 : DotNet "'mscorlib'.System.Type") : Boolean;
|
||||
BEGIN
|
||||
Exception := OuterException;
|
||||
|
||||
REPEAT
|
||||
IF Type.Equals(Exception.GetType()) THEN
|
||||
EXIT(TRUE);
|
||||
Exception := Exception.InnerException;
|
||||
UNTIL ISNULL(Exception);
|
||||
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
|
||||
PROCEDURE Rethrow@4();
|
||||
BEGIN
|
||||
IF NOT ISNULL(OuterException.InnerException) THEN
|
||||
ERROR(OuterException.InnerException.Message);
|
||||
|
||||
ERROR(OuterException.Message);
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
END.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## NAV Usages
|
||||
|
||||
|
|
|
|||
|
|
@ -26,30 +26,12 @@ From the implementation point of view, a number series is a record in the table
|
|||
[![ ][image0]][anchor0]
|
||||
|
||||
The most relevant fields are:
|
||||
|
||||
Code
|
||||
|
||||
Code 10
|
||||
|
||||
Used to identify the number series in further places where it will be used.
|
||||
|
||||
Default Nos.
|
||||
|
||||
Boolean
|
||||
|
||||
The boolean **Default Nos.** decides whether this number series can use automatic numbering. If false, then the user is expected to manually look up the No. field and select it from the number series.
|
||||
|
||||
Manual Nos.
|
||||
|
||||
Boolean
|
||||
|
||||
If the **Manual Nos.** is Yes, then the used will be allowed to manually type the value of the next number ("No.") field.
|
||||
|
||||
Date Order
|
||||
|
||||
Boolean
|
||||
|
||||
**Date Order** is used to decide whether or not the numbers from the range are assigned chronologically.
|
||||
|
|
||||
----|----|----
|
||||
Code | Code 10 | Used to identify the number series in further places where it will be used.
|
||||
Default Nos. | Boolean | The boolean **Default Nos.** decides whether this number series can use automatic numbering. If false, then the user is expected to manually look up the No. field and select it from the number series.
|
||||
Manual Nos. | Boolean | If the **Manual Nos.** is Yes, then the used will be allowed to manually type the value of the next number ("No.") field.
|
||||
Date Order | Boolean | **Date Order** is used to decide whether or not the numbers from the range are assigned chronologically.
|
||||
|
||||
### Number Series sequence
|
||||
|
||||
|
|
@ -61,47 +43,15 @@ The sequence definition is found in table 309 - No. Series Line. Contains the ex
|
|||
|
||||
The most relevant fields are:
|
||||
|
||||
Series Code
|
||||
|
||||
Code 10
|
||||
|
||||
Links it to the number series defined in table 308\.
|
||||
|
||||
Starting No.
|
||||
|
||||
Integer
|
||||
|
||||
This is the first number in the sequence, for a book indexing application it can be BOOK0001\.
|
||||
|
||||
Ending No.
|
||||
|
||||
Integer
|
||||
|
||||
The last number in the sequence, for example BOOK5000\.
|
||||
|
||||
Warning No.
|
||||
|
||||
Integer
|
||||
|
||||
When this number is reached, the user will see a warning message stating that the number series is running out of assignable numbers.
|
||||
|
||||
Increment-by No.
|
||||
|
||||
Integer
|
||||
|
||||
The value for incrementing the numeric part of the series.
|
||||
|
||||
Last No. Used
|
||||
|
||||
Code 20
|
||||
|
||||
The last number from the sequence that was assigned.
|
||||
|
||||
Last Date Used
|
||||
|
||||
Date
|
||||
|
||||
Stating when the last number was assigned.
|
||||
|
|
||||
----|----|----
|
||||
Series Code | Code 10 | Links it to the number series defined in table 308\.
|
||||
Starting No. | Integer | This is the first number in the sequence, for a book indexing application it can be BOOK0001\.
|
||||
Ending No. | Integer | The last number in the sequence, for example BOOK5000\.
|
||||
Warning No. | Integer | When this number is reached, the user will see a warning message stating that the number series is running out of assignable numbers.
|
||||
Increment-by No. | Integer | The value for incrementing the numeric part of the series.
|
||||
Last No. Used | Code 20 | The last number from the sequence that was assigned.
|
||||
Last Date Used | Date | Stating when the last number was assigned.
|
||||
|
||||
### Add the default number series to the setup
|
||||
|
||||
|
|
@ -113,41 +63,41 @@ The default number series for a certain application area is typically stored in
|
|||
|
||||
The table which will host the number from the number series, needs the following fields:
|
||||
|
||||
No.
|
||||
|
||||
Code 20
|
||||
|
||||
Contains the auto-generated sequential number.
|
||||
|
||||
No. Series
|
||||
|
||||
Code 10
|
||||
|
||||
The number series definition, which decides what the next No. will be.
|
||||
|
|
||||
----|----|----
|
||||
No. | Code 20 | Contains the auto-generated sequential number.
|
||||
No. Series | Code 10 | The number series definition, which decides what the next No. will be.
|
||||
|
||||
And the code to make the number series alive:
|
||||
|
||||
**OnInsert**
|
||||
|
||||
```al
|
||||
OnInsert()
|
||||
|
||||
IF "No." = '' THEN
|
||||
|
||||
NoSeriesMgt.InitSeries(DefaultNoSeriesCode,OldNoSeriesCode,NewDate,NewNo,NewNoSeriesCode);
|
||||
NoSeriesMgt.InitSeries(DefaultNoSeriesCode,OldNoSeriesCode,NewDate,NewNo,NewNoSeriesCode);
|
||||
```
|
||||
|
||||
**Field "No."**
|
||||
|
||||
```al
|
||||
No. - OnValidate()
|
||||
|
||||
IF "No." <\> xRec."No." THEN BEGIN // Validate that "No." corresponds to the current No. Series rules NoSeriesMgt.TestManual(DefaultNoSeriesCode); "No. Series" := ''; END;
|
||||
IF "No." <> xRec."No." THEN BEGIN // Validate that "No." corresponds to the current No. Series rules
|
||||
NoSeriesMgt.TestManual(DefaultNoSeriesCode);
|
||||
"No. Series" := '';
|
||||
END;
|
||||
```
|
||||
|
||||
**AssistEdit**
|
||||
|
||||
```al
|
||||
AssistEdit() : Boolean
|
||||
|
||||
IF "No." = '' THEN
|
||||
|
||||
NoSeriesMgt.InitSeries(DefaultNoSeriesCode,OldNoSeriesCode,NewDate,NewNo,NewNoSeriesCode);
|
||||
NoSeriesMgt.InitSeries(DefaultNoSeriesCode,OldNoSeriesCode,NewDate,NewNo,NewNoSeriesCode);
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
|
|
@ -166,93 +116,80 @@ The setup table 311 (Sales & Receivables Setup) contains the default number seri
|
|||
|
||||
The default number series defined in the setup is then used in the individual tables. For example, table 18 - Customer, has
|
||||
|
||||
###
|
||||
Field "No."
|
||||
### Field "No."
|
||||
|
||||
```al
|
||||
{ 1 ; ;No. ;Code20 ;AltSearchField=Search Name;
|
||||
|
||||
OnValidate=
|
||||
|
||||
BEGIN
|
||||
IF "No." <> xRec."No." THEN BEGIN
|
||||
SalesSetup.GET;
|
||||
|
||||
IF "No." <\> xRec."No." THEN BEGIN
|
||||
NoSeriesMgt.TestManual(SalesSetup."Customer Nos.");
|
||||
|
||||
SalesSetup.GET;
|
||||
|
||||
NoSeriesMgt.TestManual(SalesSetup."Customer Nos.");
|
||||
|
||||
"No. Series" := '';
|
||||
"No. Series" := '';
|
||||
END;
|
||||
|
||||
IF "Invoice Disc. Code" = '' THEN
|
||||
"Invoice Disc. Code" := "No.";
|
||||
END;
|
||||
|
||||
IF "Invoice Disc. Code" = '' THEN
|
||||
|
||||
"Invoice Disc. Code" := "No.";
|
||||
|
||||
END;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Field "No. Series"
|
||||
|
||||
```al
|
||||
{ 107 ; ;No. Series ;Code10 ;TableRelation="No. Series"; Editable=No }
|
||||
```
|
||||
|
||||
### AssistEdit
|
||||
|
||||
```al
|
||||
PROCEDURE AssistEdit@2(OldCust@1000 : Record 18) : Boolean;
|
||||
|
||||
VAR Cust@1001 : Record 18;
|
||||
|
||||
VAR
|
||||
Cust@1001 : Record 18;
|
||||
BEGIN
|
||||
WITH Cust DO BEGIN
|
||||
Cust := Rec;
|
||||
SalesSetup.GET;
|
||||
|
||||
WITH Cust DO BEGIN
|
||||
|
||||
Cust := Rec;
|
||||
|
||||
SalesSetup.GET;
|
||||
|
||||
SalesSetup.TESTFIELD("Customer Nos.");
|
||||
|
||||
IF NoSeriesMgt.SelectSeries(SalesSetup."Customer Nos.",OldCust."No. Series","No. Series") THEN BEGIN
|
||||
|
||||
NoSeriesMgt.SetSeries("No.");
|
||||
|
||||
Rec := Cust;
|
||||
|
||||
EXIT(TRUE);
|
||||
|
||||
END;
|
||||
|
||||
END;
|
||||
SalesSetup.TESTFIELD("Customer Nos.");
|
||||
|
||||
IF NoSeriesMgt.SelectSeries(SalesSetup."Customer Nos.",OldCust."No. Series","No. Series") THEN BEGIN
|
||||
NoSeriesMgt.SetSeries("No.");
|
||||
Rec := Cust;
|
||||
EXIT(TRUE);
|
||||
END;
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
### OnInsert
|
||||
|
||||
```al
|
||||
OnInsert=
|
||||
BEGIN
|
||||
BEGIN
|
||||
IF "No." = '' THEN BEGIN
|
||||
SalesSetup.GET;
|
||||
|
||||
IF "No." = '' THEN BEGIN
|
||||
SalesSetup.TESTFIELD("Customer Nos.");
|
||||
|
||||
SalesSetup.GET;
|
||||
|
||||
SalesSetup.TESTFIELD("Customer Nos.");
|
||||
|
||||
NoSeriesMgt.InitSeries(SalesSetup."Customer Nos.",xRec."No. Series",0D,"No.","No. Series");
|
||||
|
||||
END;
|
||||
|
||||
...
|
||||
|
||||
END
|
||||
NoSeriesMgt.InitSeries(SalesSetup."Customer Nos.",xRec."No. Series",0D,"No.","No. Series");
|
||||
END;
|
||||
...
|
||||
END
|
||||
```
|
||||
|
||||
To run the AssistEdit procedure, include this code on the No. - OnAssistEdit() trigger of the Page:
|
||||
|
||||
### No. - OnAssistEdit()
|
||||
|
||||
```al
|
||||
IF AssistEdit(xRec) THEN
|
||||
|
||||
CurrPage.UPDATE;
|
||||
CurrPage.UPDATE;
|
||||
```
|
||||
|
||||
{{< youtube 1lG9rY_dmM4>}}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ The ID is an optional parameter for the notification object that tracks the obje
|
|||
|
||||
**GetItemAvailabilityNotificationId**
|
||||
|
||||
EXIT('2712AD06-C48B-4C20-820E-347A60C9AD00');****
|
||||
EXIT('2712AD06-C48B-4C20-820E-347A60C9AD00');
|
||||
|
||||
Figure 5\. Uniquely identifying a notification
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ You may check that the call to this function is made almost as the first step in
|
|||
|
||||
The ability to turn notifications on or off is not required. If skipped, the notification is always shown when the condition that triggers it is met, and a user cannot turn it off.
|
||||
|
||||
# NAV specific usages
|
||||
## NAV specific usages
|
||||
|
||||
For examples of how these objects are used in Dynamics NAV, look at the code for the following objects:
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ For examples of how these objects are used in Dynamics NAV, look at the code for
|
|||
4. Codeunit 1854 Item Sales Forecast Notifier (in SalesAndInventoryForecast extension)
|
||||
5. Codeunit 1852 Item Sales Forecast Scheduler (in SalesAndInventoryForecast extension)
|
||||
|
||||
# Best practices
|
||||
## Best practices
|
||||
|
||||
The following list summarizes best practices for creating notifications:
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ This pattern is about sending notifications in Dynamics NAV, starting with versi
|
|||
|
||||
Notifications are easy to use in a wide range of cases. Instead of using notifications in a fire-and-forget way, we need to track them so that we can recall them if we need to.
|
||||
|
||||
If we can have only one notification on a given page, an easy and efficient solution is to use a predefined Notification ID, as suggested in the "Using In-context Notifications" pattern.
|
||||
If we can have only one notification on a given page, an easy and efficient solution is to use a predefined Notification ID, as suggested in the ["Using In-context Notifications"][anchor7] pattern.
|
||||
|
||||
However, some cases can be more complicated. For example, when you are adding lines to a table, what if several lines raise individual notifications? Using the same notification ID for each notification will no longer work because the latest notification overwrites the previous ones. Only one notification for a given notification ID can exist, and only the notification message would be updated. This is illustrated in Figure 1\.
|
||||
|
||||
|
|
@ -32,41 +32,48 @@ _Figure 2: You add a second item that is not in stock. the notification is fired
|
|||
|
||||
Here is the code for this behavior:
|
||||
|
||||
LOCAL PROCEDURE CreateAndSendNotification@23(UnitOfMeasureCode@1010 : Code\[20\];InventoryQty@1009 : Decimal;GrossReq@1008 : Decimal;ReservedReq@1007 : Decimal;SchedRcpt@1006 : Decimal;ReservedRcpt@1005 : Decimal;CurrentQuantity@1004 : Decimal;CurrentReservedQty@1003 : Decimal;TotalQuantity@1002 : Decimal;EarliestAvailDate@1001 : Date) : Boolean;
|
||||
VAR
|
||||
ItemAvailabilityCheck@1011 : Page 1872;
|
||||
AvailabilityCheckNotification@1000 : Notification;
|
||||
BEGIN
|
||||
**AvailabilityCheckNotification.ID(GetItemAvailabilityNotificationId);**
|
||||
AvailabilityCheckNotification.MESSAGE(STRSUBSTNO(NotificationMsg,ItemNo));
|
||||
AvailabilityCheckNotification.SCOPE(NOTIFICATIONSCOPE::LocalScope);
|
||||
AvailabilityCheckNotification.ADDACTION(DetailsTxt,CODEUNIT::"Item-Check Avail.",'ShowNotificationDetails');
|
||||
ItemAvailabilityCheck.PopulateDataOnNotification(AvailabilityCheckNotification,ItemNo,UnitOfMeasureCode,InventoryQty,GrossReq,ReservedReq,SchedRcpt,ReservedRcpt,CurrentQuantity,CurrentReservedQty,TotalQuantity,EarliestAvailDate);
|
||||
AvailabilityCheckNotification.SEND;
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
LOCAL PROCEDURE **GetItemAvailabilityNotificationId**@27() : GUID;
|
||||
BEGIN
|
||||
**EXIT('2712AD06-C48B-4C20-820E-347A60C9AD00');**
|
||||
END;
|
||||
```al
|
||||
LOCAL PROCEDURE CreateAndSendNotification@23(UnitOfMeasureCode@1010 : Code[20];InventoryQty@1009 : Decimal;GrossReq@1008 : Decimal;ReservedReq@1007 : Decimal;SchedRcpt@1006 : Decimal;ReservedRcpt@1005 : Decimal;CurrentQuantity@1004 : Decimal;CurrentReservedQty@1003 : Decimal;TotalQuantity@1002 : Decimal;EarliestAvailDate@1001 : Date) : Boolean;
|
||||
VAR
|
||||
ItemAvailabilityCheck@1011 : Page 1872;
|
||||
AvailabilityCheckNotification@1000 : Notification;
|
||||
BEGIN
|
||||
AvailabilityCheckNotification.ID(GetItemAvailabilityNotificationId);
|
||||
AvailabilityCheckNotification.MESSAGE(STRSUBSTNO(NotificationMsg,ItemNo));
|
||||
AvailabilityCheckNotification.SCOPE(NOTIFICATIONSCOPE::LocalScope);
|
||||
AvailabilityCheckNotification.ADDACTION(DetailsTxt,CODEUNIT::"Item-Check Avail.",'ShowNotificationDetails');
|
||||
|
||||
ItemAvailabilityCheck.PopulateDataOnNotification(AvailabilityCheckNotification,ItemNo,UnitOfMeasureCode,InventoryQty,GrossReq,ReservedReq,SchedRcpt,ReservedRcpt,CurrentQuantity,CurrentReservedQty,TotalQuantity,EarliestAvailDate);
|
||||
AvailabilityCheckNotification.SEND;
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
|
||||
LOCAL PROCEDURE **GetItemAvailabilityNotificationId**@27() : GUID;
|
||||
BEGIN
|
||||
EXIT('2712AD06-C48B-4C20-820E-347A60C9AD00');
|
||||
END;
|
||||
```
|
||||
|
||||
An easy fix would be to dynamically generate the notification ID. However, what if you fix the issue that triggered the notification?
|
||||
|
||||
Here is the code for this possible fix:
|
||||
|
||||
LOCAL PROCEDURE CreateAndSendNotification@23(UnitOfMeasureCode@1010 : Code\[20\];InventoryQty@1009 : Decimal;GrossReq@1008 : Decimal;ReservedReq@1007 : Decimal;SchedRcpt@1006 : Decimal;ReservedRcpt@1005 : Decimal;CurrentQuantity@1004 : Decimal;CurrentReservedQty@1003 : Decimal;TotalQuantity@1002 : Decimal;EarliestAvailDate@1001 : Date) : Boolean;
|
||||
VAR
|
||||
ItemAvailabilityCheck@1011 : Page 1872;
|
||||
AvailabilityCheckNotification@1000 : Notification;
|
||||
BEGIN
|
||||
**AvailabilityCheckNotification.ID(CREATEGUID);**
|
||||
AvailabilityCheckNotification.MESSAGE(STRSUBSTNO(NotificationMsg,ItemNo));
|
||||
AvailabilityCheckNotification.SCOPE(NOTIFICATIONSCOPE::LocalScope);
|
||||
AvailabilityCheckNotification.ADDACTION(DetailsTxt,CODEUNIT::"Item-Check Avail.",'ShowNotificationDetails');
|
||||
ItemAvailabilityCheck.PopulateDataOnNotification(AvailabilityCheckNotification,ItemNo,UnitOfMeasureCode,InventoryQty,GrossReq,ReservedReq,SchedRcpt,ReservedRcpt,CurrentQuantity,CurrentReservedQty,TotalQuantity,EarliestAvailDate);
|
||||
AvailabilityCheckNotification.SEND;
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
```al
|
||||
LOCAL PROCEDURE CreateAndSendNotification@23(UnitOfMeasureCode@1010 : Code[20];InventoryQty@1009 : Decimal;GrossReq@1008 : Decimal;ReservedReq@1007 : Decimal;SchedRcpt@1006 : Decimal;ReservedRcpt@1005 : Decimal;CurrentQuantity@1004 : Decimal;CurrentReservedQty@1003 : Decimal;TotalQuantity@1002 : Decimal;EarliestAvailDate@1001 : Date) : Boolean;
|
||||
VAR
|
||||
ItemAvailabilityCheck@1011 : Page 1872;
|
||||
AvailabilityCheckNotification@1000 : Notification;
|
||||
BEGIN
|
||||
AvailabilityCheckNotification.ID(CREATEGUID);
|
||||
AvailabilityCheckNotification.MESSAGE(STRSUBSTNO(NotificationMsg,ItemNo));
|
||||
AvailabilityCheckNotification.SCOPE(NOTIFICATIONSCOPE::LocalScope);
|
||||
AvailabilityCheckNotification.ADDACTION(DetailsTxt,CODEUNIT::"Item-Check Avail.",'ShowNotificationDetails');
|
||||
|
||||
ItemAvailabilityCheck.PopulateDataOnNotification(AvailabilityCheckNotification,ItemNo,UnitOfMeasureCode,InventoryQty,GrossReq,ReservedReq,SchedRcpt,ReservedRcpt,CurrentQuantity,CurrentReservedQty,TotalQuantity,EarliestAvailDate);
|
||||
AvailabilityCheckNotification.SEND;
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
```
|
||||
|
||||
Now, notifications do not replace each other, but we cannot recall them because we do not track each notification ID.
|
||||
|
||||
|
|
@ -107,7 +114,7 @@ The main functions provided by codeunit 1511 are:
|
|||
* Sends a notification and keeps track of it with additional information. For example, a GUID that represents the context in which the notification was sent, and an item with insufficient inventory.
|
||||
* **RecallNotificationsForRecord**(RecId : RecordID;HandleDelayedInsert : Boolean)
|
||||
|
||||
* * Recalls all notifications that were sent by a given record ID. The HandleDelayedInsert flag should be TRUE if it is possible that the record ID provided is from a record that was not yet in the database (TRUE unless we recall notifications after deletion of a record).
|
||||
* Recalls all notifications that were sent by a given record ID. The HandleDelayedInsert flag should be TRUE if it is possible that the record ID provided is from a record that was not yet in the database (TRUE unless we recall notifications after deletion of a record).
|
||||
* **RecallNotificationsForRecordWithAdditionalContext**(RecId : RecordID;AdditionalContextId : GUID;HandleDelayedInsert : Boolean)
|
||||
* Recalls the notification that was sent by a given Record ID in a particular context. The HandleDelayedInsert flag should be TRUE if it is possible that the Record ID provided is from a record that was not yet in the database (TRUE unless we recall notifications after deleting a record).
|
||||
* **SetRecordID**(RecId : RecordID)
|
||||
|
|
@ -136,8 +143,6 @@ _Figure 4: without additional context_
|
|||
|
||||
_Figure 5: with additional context_
|
||||
|
||||
__
|
||||
|
||||
However, delayed insert means that the simple case seen above doesn't happen very often. The issue is that when we call **SendNotification**, we provide the cause object's record ID. If this object has not been inserted yet, which is often the case when the user creates a new invoice, a new line, and so on, the record ID is incomplete. When the object is inserted the record ID is completed, but if we call **RecallNotificationsForRecord** at a later point, the record ID will be different from the incomplete record ID we used when sending the notification. The solution is to detect that the object is not yet inserted when we send the notification, and at a later point, set the record ID when the cause object is inserted.
|
||||
|
||||
|
||||
|
|
@ -166,10 +171,9 @@ COD1508 (Notification Lifecycle Handler)
|
|||
|
||||
**Related Patterns:**
|
||||
|
||||
[In-context notifications][anchor7] (https://community.dynamics.com/nav/w/designpatterns/284.using-in-context-notifications)
|
||||
|
||||
[Singleton codeunit][anchor8] (https://community.dynamics.com/nav/w/designpatterns/283.singleton-codeunit)
|
||||
[In-context notifications][anchor7]
|
||||
|
||||
[Singleton codeunit][anchor8]
|
||||
|
||||
|
||||
[anchor0]: 6138.logo.png
|
||||
|
|
@ -179,8 +183,8 @@ COD1508 (Notification Lifecycle Handler)
|
|||
[anchor4]: sequence1.png
|
||||
[anchor5]: sequence2.png
|
||||
[anchor6]: sequence3.png
|
||||
[anchor7]: /nav/w/designpatterns/284.using-in-context-notifications
|
||||
[anchor8]: /nav/w/designpatterns/283.singleton-codeunit
|
||||
[anchor7]: /navpatterns/1-patterns/notifications/in-context-notifications/
|
||||
[anchor8]: /navpatterns/1-patterns/singleton/singleton-codeunit/
|
||||
|
||||
|
||||
[image0]: 6138.logo.png
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ title = "Observer"
|
|||
weight = 900
|
||||
+++
|
||||
_By Nikolai L'Estrange, from TVision Technology Ltd. in the UK_
|
||||
_
|
||||
|
||||
### Abstract
|
||||
|
||||
|
|
@ -21,39 +20,46 @@ Codeunit 1 Application Management contains the triggers OnDatabaseInsert, OnData
|
|||
|
||||
In order to define which tables we are interested in we can create a new table with the following fields:
|
||||
|
||||
**Observable Table:**
|
||||
"Table ID" Integer Object.ID WHERE (Type=CONST(Table))
|
||||
TrackInsert Boolean \[optional\]
|
||||
TrackModify Boolean \[optional\]
|
||||
TrackDelete Boolean \[optional\]
|
||||
TrackRename Boolean \[optional\]
|
||||
**Observable Table:**
|
||||
|
||||
"Table ID" | Integer | Object.ID WHERE (Type=CONST(Table))
|
||||
----|----|----
|
||||
TrackInsert | Boolean | [optional]
|
||||
TrackModify | Boolean | [optional]
|
||||
TrackDelete | Boolean | [optional]
|
||||
TrackRename | Boolean | [optional]
|
||||
|
||||
Then we can create a Codeunit that will set the Table Trigger Setup parameters and also subscribe to the OnDatabase triggers.
|
||||
|
||||
**LOCAL \[EventSubscriber\] GetTableTriggerSetup(TableId : Integer;VAR OnDatabaseInsert : Boolean;VAR OnDatabaseModify : Boolean;VAR OnDatabaseDelete : Boolean;VAR OnDatabaseRename : Boolean)**
|
||||
IF Observable.GET(TableId) THEN BEGIN
|
||||
```al
|
||||
LOCAL [EventSubscriber] GetTableTriggerSetup(TableId : Integer;VAR OnDatabaseInsert : Boolean;VAR OnDatabaseModify : Boolean;VAR OnDatabaseDelete : Boolean;VAR OnDatabaseRename : Boolean)
|
||||
IF Observable.GET(TableId) THEN BEGIN
|
||||
IF Observable.TrackInsert THEN
|
||||
OnDatabaseInsert := TRUE;
|
||||
OnDatabaseInsert := TRUE;
|
||||
IF Observable.TrackModify THEN
|
||||
OnDatabaseModify := TRUE;
|
||||
OnDatabaseModify := TRUE;
|
||||
IF Observable.TrackDelete THEN
|
||||
OnDatabaseDelete := TRUE;
|
||||
OnDatabaseDelete := TRUE;
|
||||
IF Observable.TrackRename THEN
|
||||
OnDatabaseRename := TRUE;
|
||||
END;
|
||||
**LOCAL \[EventSubscriber\] OnDatabaseInsert(RecRef : RecordRef)**
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackInsert THEN
|
||||
OnDatabaseRename := TRUE;
|
||||
END;
|
||||
|
||||
LOCAL [EventSubscriber] OnDatabaseInsert(RecRef : RecordRef)
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackInsert THEN
|
||||
//do something
|
||||
**LOCAL \[EventSubscriber\] OnDatabaseModify(RecRef : RecordRef)**
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackModify THEN
|
||||
|
||||
LOCAL [EventSubscriber] OnDatabaseModify(RecRef : RecordRef)
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackModify THEN
|
||||
//do something
|
||||
**LOCAL \[EventSubscriber\] OnDatabaseDelete(RecRef : RecordRef)**
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackDelete THEN
|
||||
|
||||
LOCAL [EventSubscriber] OnDatabaseDelete(RecRef : RecordRef)
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackDelete THEN
|
||||
//do something
|
||||
**LOCAL \[EventSubscriber\] OnDatabaseRename(RecRef : RecordRef;xRecRef : RecordRef)**
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackRename THEN
|
||||
|
||||
LOCAL [EventSubscriber] OnDatabaseRename(RecRef : RecordRef;xRecRef : RecordRef)
|
||||
IF Observable.Get(RecRef.NUMBER) AND Observable.TrackRename THEN
|
||||
//do something
|
||||
|
||||
```
|
||||
|
||||
_**Note:**_ In NAV2016 all these functions can be EventSubscribers that subscribe to the functions in Codeunit 1 as per above, in earlier versions of NAV these functions will need to be Global and called explicitly from within the Codeunit 1 functions.
|
||||
|
||||
|
|
|
|||
|
|
@ -36,11 +36,13 @@ Or in case of the Post&Print, the selection codeunit is different:
|
|||
|
||||
Next, the user is able to select the options with an STRMENU, which results in filling in the fields above, like:
|
||||
|
||||
```al
|
||||
Selection := STRMENU(Text000,3);
|
||||
IF Selection = 0 THEN
|
||||
EXIT;
|
||||
Ship := Selection IN \[1,3\];
|
||||
Invoice := Selection IN \[2,3\];
|
||||
EXIT;
|
||||
Ship := Selection IN [1,3];
|
||||
Invoice := Selection IN [2,3];
|
||||
```
|
||||
|
||||
The processing codeunit is being called after these options were set.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,28 +4,23 @@ weight = 1040
|
|||
+++
|
||||
_By Bogdana Botez, at Microsoft Development Center Copenhagen_
|
||||
|
||||
### **Abstract**
|
||||
### Abstract
|
||||
|
||||
This pattern explains how to perform SELECT DISTINCT by using queries in Dynamics NAV.
|
||||
|
||||
****
|
||||
|
||||
**Description**
|
||||
|
||||
When working with tables, sometimes a developer needs to perform a SELECT DISTINCT (also known as SELECT UNIQUE) from a table. As NAV does not provide this out of the box, we present below a way to select unique records by using queries.
|
||||
|
||||
****
|
||||
|
||||
**Problem statement**
|
||||
|
||||
Let's consider the **VAT Entry** table as below: [
|
||||
][anchor0]
|
||||
Let's consider the **VAT Entry** table as below: [ ][anchor0]
|
||||
|
||||
[![ ][image0]][anchor1]
|
||||
|
||||
The goal is to select one line for each separate document that produced VAT Entries. In other words, we want records grouped by **Type, Document Type** and **Document No.**. However, if there are multiple lines with the same value of the triad **Type, Document Type** and **Document No.** in the **VAT Entry** table, we only want to see one of them.
|
||||
|
||||
#### **Solution**
|
||||
#### Solution
|
||||
|
||||
Create a new query object **VAT Entry Distinct Document No.**, with a single DataItem sourced from **VAT Entry** table. Add the three desired group-by fields **Type, Document Type** and **Document No.** as columns.
|
||||
|
||||
|
|
@ -41,7 +36,7 @@ Running the query yields a single record per document. You can notice in the sec
|
|||
|
||||
[![ ][image3]][anchor4]
|
||||
|
||||
### **Example**
|
||||
### Example
|
||||
|
||||
One thing is to be noted: there is a limitation to how much information you can take out from the records. For example, if we need to extract more information than just the one we already have in the columns, then the following apply: adding one more column of **Method Type** = **None** will indeed show more information, but it _might_ affect the grouping. More details below.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,37 +4,37 @@ weight = 1340
|
|||
+++
|
||||
_Originally by Abshishek Ghosh and Bogdan Sturzoiu at Microsoft Development Center Copenhagen_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
This pattern uses queries to create an efficient way to detect duplicate entries in a table. This is, for example, useful when trying to find out which customers or contacts have the same names, so we can merge them later.
|
||||
|
||||
## **Description**
|
||||
## Description
|
||||
|
||||
Duplicate detection has several requirements in Microsoft Dynamics NAV. One method to eliminate duplication is by defining the relevant field as the primary key. However, this method is not always practical either due to the size of the field or due to business requirements that dictate how duplicates are detected but not necessarily how they are eliminated. An example of this method is to detect contacts with the same name and take action to merge them if they are.
|
||||
|
||||
Before Dynamics NAV 2013, the only possibility was to iterate through the table in a loop and then create a sub-loop where another instance of the same table is filtered to check for duplicates. For example, to check for duplicate names in the Customer table, the code would look like this:
|
||||
|
||||
PROCEDURE HasDuplicateCustomers@26() : Boolean;
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE HasDuplicateCustomers@26() : Boolean;
|
||||
VAR
|
||||
Customer@1000 : Record 18;
|
||||
Customer2@1001 : Record 18;
|
||||
BEGIN
|
||||
BEGIN
|
||||
IF Customer.FINDSET THEN
|
||||
REPEAT
|
||||
Customer2.SETRANGE(Name,Customer.Name);
|
||||
IF Customer2.COUNT \> 1 THEN
|
||||
EXIT(TRUE);
|
||||
UNTIL Customer.NEXT = 0;
|
||||
Customer2.SETRANGE(Name,Customer.Name);
|
||||
IF Customer2.COUNT \> 1 THEN
|
||||
EXIT(TRUE);
|
||||
UNTIL Customer.NEXT = 0;
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
|
||||
####
|
||||
END;
|
||||
```
|
||||
|
||||
This code would involve setting filters on the **Customer** table as many times as there are records in the table. This is an expensive operation.
|
||||
|
||||
Starting with Dynamics NAV 2013, we can use queries to create a more efficient implementation of the same logic.
|
||||
|
||||
## **Usage**
|
||||
## Usage
|
||||
|
||||
The solution involves that you create a query to return duplicates, and then invoke it from a method that would test the value of the query to identify if duplicates were found.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ The solution involves that you create a query to return duplicates, and then inv
|
|||
|
||||
* The query must be created with the table we want to search in as the dataitem.
|
||||
* The field we want to search for must be created as a grouped field.
|
||||
* Create a totaling field on the count, and add a filter for Count \> 1\. This ensures that only records with more than one instance of the field that we selected in the previous step are included in the query result.
|
||||
* Create a totaling field on the count, and add a filter for Count > 1. This ensures that only records with more than one instance of the field that we selected in the previous step are included in the query result.
|
||||
|
||||
Continuing with our Customer Name example, here is how the query would look:
|
||||
|
||||
|
|
@ -57,29 +57,26 @@ Continuing with our Customer Name example, here is how the query would look:
|
|||
{ 2 ;1 ;Column ; ;
|
||||
DataSource=Name }
|
||||
{ 3 ;1 ;Column ; ;
|
||||
ColumnFilter=Count\_=FILTER(\>1);
|
||||
ColumnFilter=Count_=FILTER(\>1);
|
||||
MethodType=Totals;
|
||||
Method=Count }
|
||||
}
|
||||
|
||||
**
|
||||
**
|
||||
|
||||
**Step 2 -- Invoking the Query to Check for Duplicates**
|
||||
|
||||
Now that the query is created, all we need to do is to invoke the query and check if any records are returned, which would mean that there are duplicates.
|
||||
|
||||
Here is an alternate implementation of the **HasDuplicateCustomers** method using the query that we created:
|
||||
|
||||
PROCEDURE HasDuplicateCustomersWithQuery@27() : Boolean;
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE HasDuplicateCustomersWithQuery@27() : Boolean;
|
||||
VAR
|
||||
CustomerDuplicate@1000 : Query 70000;
|
||||
BEGIN
|
||||
BEGIN
|
||||
CustomerDuplicate.OPEN;
|
||||
EXIT(CustomerDuplicate.READ);
|
||||
END;
|
||||
|
||||
####
|
||||
END;
|
||||
```
|
||||
|
||||
**Examples**
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,23 @@ weight = 1350
|
|||
+++
|
||||
_Originally by Bogdan Sturzoiu, Microsoft Development Center Copenhagen_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
This pattern shows how the new query object type introduced in NAV 2013 allows you to replace costly loops when inspecting data from two or more tables.
|
||||
|
||||
### **Description**
|
||||
### Description
|
||||
|
||||
One of the core operations in a relational database is joining two or more tables. For example, you might need to extract all sales lines in the database together with information regarding the related sales header. This requires joining the Sales Header and Sales Line tables using Sales Header No. as the connecting field.;
|
||||
|
||||
The join operation has traditionally been done in C/AL by record looping. When NAV 2013 introduced the query object, it allowed us to produce a data set that is the result of a join operation between two or more tables. This simplifies the problem of finding related records in two tables linked through a foreign key.
|
||||
|
||||
#### **Pattern Elements**
|
||||
#### Pattern Elements
|
||||
|
||||
1. Two or more tables that contain records linked through a foreign key: Table 1, Table 2, Table n.
|
||||
2. A query object Query X, that joins Table 1, Table 2, etc. based on the connecting key.
|
||||
3. A processing codeunit that loops through the query records (or any other code-bearing object).
|
||||
|
||||
#### **Pattern Steps**
|
||||
#### Pattern Steps
|
||||
|
||||
1. Run the query on the connected tables.
|
||||
2. Loop through the records returned by the query.
|
||||
|
|
@ -30,13 +30,13 @@ The join operation has traditionally been done in C/AL by record looping. When N
|
|||
|
||||
_Figure 1\. The pattern elements_
|
||||
|
||||
### **Usage**
|
||||
### Usage
|
||||
|
||||
#### **Sample Problem**
|
||||
#### Sample Problem
|
||||
|
||||
The Bank Acc. Reconciliation Line table (274) and the Bank Account Ledger Entry table (271) are connected through the Bank Account No. field. Identify the matching pairs of records based on having the same remaining amount and transaction date.
|
||||
|
||||
#### **Solution Using Nested Loops**
|
||||
#### Solution Using Nested Loops
|
||||
|
||||
The classic C/AL approach is to:
|
||||
|
||||
|
|
@ -45,32 +45,34 @@ The classic C/AL approach is to:
|
|||
3. For each record in the filter, find the related records in the right table (table 271) and set the required filters on it.
|
||||
4. For each pair of records from the left and right table, decide if they are a solution and if so, apply them to each other.
|
||||
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
BankAccRecLine@1005 : Record 274;
|
||||
BankAccLedgerEntry@1006 : Record 271;
|
||||
BankAccEntrySetReconNo@1007 : Codeunit 375;
|
||||
BEGIN
|
||||
BEGIN
|
||||
BankAccRecLine.SETRANGE("Bank Account No.",BankAccReconciliation."Bank Account No.");
|
||||
BankAccRecLine.SETRANGE("Statement No.",BankAccReconciliation."Statement No.");
|
||||
BankAccRecLine.SETFILTER(Difference,'<\>%1',0);
|
||||
BankAccRecLine.SETFILTER(Difference,'<>%1',0);
|
||||
BankAccRecLine.SETRANGE(Type,BankAccRecLine.Type::"Bank Account Ledger Entry");
|
||||
IF BankAccRecLine.FINDSET THEN
|
||||
REPEAT
|
||||
BankAccLedgerEntry.SETRANGE("Bank Account No.",BankAccRecLine."Bank Account No.");
|
||||
BankAccLedgerEntry.SETRANGE(Open,TRUE);
|
||||
BankAccLedgerEntry.SETRANGE("Statement Status",BankAccLedgerEntry."Statement Status"::Open);
|
||||
BankAccLedgerEntry.SETFILTER("Remaining Amount",'<\>%1',0);
|
||||
IF BankAccLedgerEntry.FINDSET THEN
|
||||
REPEAT
|
||||
IF (BankAccRecLine.Difference = BankAccLedgerEntry."Remaining Amount") AND
|
||||
(BankAccRecLine."Transaction Date" = BankAccLedgerEntry."Posting Date") THEN
|
||||
BankAccEntrySetReconNo.ApplyEntries(
|
||||
BankAccRecLine,BankAccLedgerEntry, Relation::"One-to-One");
|
||||
UNTIL BankAccLedgerEntry.NEXT = 0;
|
||||
UNTIL BankAccRecLine.NEXT = 0;
|
||||
END;
|
||||
|
||||
IF BankAccRecLine.FINDSET THEN
|
||||
REPEAT
|
||||
BankAccLedgerEntry.SETRANGE("Bank Account No.",BankAccRecLine."Bank Account No.");
|
||||
BankAccLedgerEntry.SETRANGE(Open,TRUE);
|
||||
BankAccLedgerEntry.SETRANGE("Statement Status",BankAccLedgerEntry."Statement Status"::Open);
|
||||
BankAccLedgerEntry.SETFILTER("Remaining Amount",'<>%1',0);
|
||||
IF BankAccLedgerEntry.FINDSET THEN
|
||||
REPEAT
|
||||
IF (BankAccRecLine.Difference = BankAccLedgerEntry."Remaining Amount") AND
|
||||
(BankAccRecLine."Transaction Date" = BankAccLedgerEntry."Posting Date") THEN
|
||||
BankAccEntrySetReconNo.ApplyEntries(
|
||||
BankAccRecLine,BankAccLedgerEntry, Relation::"One-to-One");
|
||||
UNTIL BankAccLedgerEntry.NEXT = 0;
|
||||
UNTIL BankAccRecLine.NEXT = 0;
|
||||
END;
|
||||
```
|
||||
|
||||
**Solution Using Query**
|
||||
|
||||
|
|
@ -80,86 +82,90 @@ The new query-based approach involves:
|
|||
2. Loop through the records returned by the query.
|
||||
3. For each query record, decide if it represents a solution and then connect the two table records that formed it through an application.
|
||||
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE MatchSingle@5(BankAccReconciliation@1003 : Record 273);
|
||||
VAR
|
||||
BankRecMatchCandidates@1001 : Query 1252;
|
||||
BankAccEntrySetReconNo@1007 : Codeunit 375;
|
||||
BEGIN
|
||||
BankRecMatchCandidates.SETRANGE(Rec\_Line\_Bank\_Account\_No,
|
||||
BankAccReconciliation."Bank Account No.");
|
||||
BankRecMatchCandidates.SETRANGE(Rec\_Line\_Statement\_No,
|
||||
BankAccReconciliation."Statement No.");
|
||||
IF NOT BankRecMatchCandidates.OPEN THEN
|
||||
EXIT;
|
||||
WHILE BankRecMatchCandidates.READ DO BEGIN
|
||||
BankAccLedgerEntry.GET(BankRecMatchCandidates.Entry\_No);
|
||||
BankAccRecLine.GET(BankAccRecLine."Statement Type"::"Bank Reconciliation",
|
||||
BankRecMatchCandidates.Rec\_Line\_Bank\_Account\_No,
|
||||
BankRecMatchCandidates.Rec\_Line\_Statement\_No,
|
||||
BankRecMatchCandidates.Rec\_Line\_Statement\_Line\_No);
|
||||
BankAccEntrySetReconNo.ApplyEntries(BankAccRecLine,BankAccLedgerEntry,
|
||||
Relation::"One-to-One");
|
||||
END;
|
||||
END;
|
||||
BEGIN
|
||||
BankRecMatchCandidates.SETRANGE(Rec_Line_Bank_Account_No,
|
||||
BankAccReconciliation."Bank Account No.");
|
||||
BankRecMatchCandidates.SETRANGE(Rec_Line_Statement_No,
|
||||
BankAccReconciliation."Statement No.");
|
||||
|
||||
IF NOT BankRecMatchCandidates.OPEN THEN
|
||||
EXIT;
|
||||
|
||||
WHILE BankRecMatchCandidates.READ DO BEGIN
|
||||
BankAccLedgerEntry.GET(BankRecMatchCandidates.Entry_No);
|
||||
BankAccRecLine.GET(BankAccRecLine."Statement Type"::"Bank Reconciliation",
|
||||
BankRecMatchCandidates.Rec_Line_Bank_Account_No,
|
||||
BankRecMatchCandidates.Rec_Line_Statement_No,
|
||||
BankRecMatchCandidates.Rec_Line_Statement_Line_No);
|
||||
BankAccEntrySetReconNo.ApplyEntries(BankAccRecLine,BankAccLedgerEntry,
|
||||
Relation::"One-to-One");
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
where the query 1252 is defined as:
|
||||
|
||||
OBJECT Query 1252 Bank Rec. Match Candidates
|
||||
{
|
||||
```al
|
||||
OBJECT Query 1252 Bank Rec. Match Candidates
|
||||
{
|
||||
OBJECT-PROPERTIES
|
||||
{
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
Date=;
|
||||
Time=;
|
||||
Version List=;
|
||||
}
|
||||
PROPERTIES
|
||||
{
|
||||
}
|
||||
ELEMENTS
|
||||
{
|
||||
{ 1 ; ;DataItem; ;
|
||||
DataItemTable=Table274;
|
||||
DataItemTableFilter=Difference=FILTER(<\>0),
|
||||
Type=FILTER(=Bank Account Ledger Entry) }
|
||||
{ 2 ;1 ;Column ;Rec\_Line\_Bank\_Account\_No;
|
||||
DataSource=Bank Account No. }
|
||||
{ 3 ;1 ;Column ;Rec\_Line\_Statement\_No;
|
||||
DataSource=Statement No. }
|
||||
{ 4 ;1 ;Column ;Rec\_Line\_Statement\_Line\_No;
|
||||
DataSource=Statement Line No. }
|
||||
{ 5 ;1 ;Column ;Rec\_Line\_Transaction\_Date;
|
||||
DataSource=Transaction Date }
|
||||
{ 6 ;1 ;Column ;Rec\_Line\_Difference ;
|
||||
DataSource=Difference }
|
||||
{ 7 ;1 ;DataItem; ;
|
||||
DataItemTable=Table271;
|
||||
DataItemTableFilter=Remaining Amount=FILTER(<\>0),
|
||||
Open=CONST(Yes),
|
||||
Statement Status=FILTER(Open);
|
||||
DataItemLink=Bank Account No.=Bank\_Acc\_Reconciliation\_Line."Bank Account o.",
|
||||
Remaining Amount=Bank\_Acc\_Reconciliation\_Line.Difference,
|
||||
Posting Date=Bank\_Acc\_Reconciliation\_Line."Transaction Date" }
|
||||
{ 8 ;2 ;Column ; ;
|
||||
DataSource=Entry No. }
|
||||
{ 9 ;2 ;Column ;Bank\_Account\_No ;
|
||||
DataSource=Bank Account No. }
|
||||
{ 10 ;2 ;Column ; ;
|
||||
DataSource=Posting Date }
|
||||
{ 11 ;2 ;Column ; ;
|
||||
DataSource=Remaining Amount }
|
||||
{ 12 ;2 ;Column ;Bank\_Ledger\_Entry\_Open;
|
||||
DataSource=Open }
|
||||
{ 13 ;2 ;Column ; ;
|
||||
DataSource=Statement Status }
|
||||
{ 1 ; ;DataItem; ;
|
||||
DataItemTable=Table274;
|
||||
DataItemTableFilter=Difference=FILTER(<>0),
|
||||
Type=FILTER(=Bank Account Ledger Entry) }
|
||||
{ 2 ;1 ;Column ;Rec_Line_Bank_Account_No;
|
||||
DataSource=Bank Account No. }
|
||||
{ 3 ;1 ;Column ;Rec_Line_Statement_No;
|
||||
DataSource=Statement No. }
|
||||
{ 4 ;1 ;Column ;Rec_Line_Statement_Line_No;
|
||||
DataSource=Statement Line No. }
|
||||
{ 5 ;1 ;Column ;Rec_Line_Transaction_Date;
|
||||
DataSource=Transaction Date }
|
||||
{ 6 ;1 ;Column ;Rec_Line_Difference ;
|
||||
DataSource=Difference }
|
||||
{ 7 ;1 ;DataItem; ;
|
||||
DataItemTable=Table271;
|
||||
DataItemTableFilter=Remaining Amount=FILTER(<>0),
|
||||
Open=CONST(Yes),
|
||||
Statement Status=FILTER(Open);
|
||||
DataItemLink=Bank Account No.=Bank_Acc_Reconciliation_Line."Bank Account o.",
|
||||
Remaining Amount=Bank_Acc_Reconciliation_Line.Difference,
|
||||
Posting Date=Bank_Acc_Reconciliation_Line."Transaction Date" }
|
||||
{ 8 ;2 ;Column ; ;
|
||||
DataSource=Entry No. }
|
||||
{ 9 ;2 ;Column ;Bank_Account_No ;
|
||||
DataSource=Bank Account No. }
|
||||
{ 10 ;2 ;Column ; ;
|
||||
DataSource=Posting Date }
|
||||
{ 11 ;2 ;Column ; ;
|
||||
DataSource=Remaining Amount }
|
||||
{ 12 ;2 ;Column ;Bank_Ledger_Entry_Open;
|
||||
DataSource=Open }
|
||||
{ 13 ;2 ;Column ; ;
|
||||
DataSource=Statement Status }
|
||||
}
|
||||
CODE
|
||||
{
|
||||
BEGIN
|
||||
END.
|
||||
BEGIN
|
||||
END.
|
||||
}
|
||||
```
|
||||
|
||||
####
|
||||
When comparing the two implementations, we notice the following advantages of using a query instead of two loops:Comparison
|
||||
|
||||
1. A query produces the Cartesian product of tables 1 and 2 faster than by looping through both of them. The advantage grows as there are more tables linked.
|
||||
|
|
@ -170,11 +176,11 @@ The query object leverages the power of SQL Server (as it basically executes a S
|
|||
2. Using a query only requires one loop, whereas joining two or more tables requires multiple code loops that quickly become difficult to read and follow.
|
||||
3. Queries are easy to create and maintain and generally provide a cleaner design.
|
||||
|
||||
### **NAV Specific Examples**
|
||||
### NAV Specific Examples
|
||||
|
||||
In Microsoft Dynamics NAV 2013 R2, we can see the query object used in the bank account reconciliation matching algorithm. The object is query Bank Rec. Match Candidates query (1252), and it is called by the matching engine in the Match Bank Rec. Lines codeunit (1252).
|
||||
|
||||
### **Ideas for Improvement**
|
||||
### Ideas for Improvement
|
||||
|
||||
The query object type could be improved to allow the passing of parameters at runtime, or, in general, being built dynamically at runtime. This will remove the need for multiple static definitions of the same base query used in slightly different contexts.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@ weight = 970
|
|||
+++
|
||||
_By Tim Grant_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
One time query and validation of a setup table.
|
||||
|
||||
****
|
||||
|
||||
## **Description**
|
||||
## Description
|
||||
|
||||
Firstly identifies if a setup table record is in memory, removing the need to execute and validate the re-query again within a code unit. (From NAV 2013+ service tier database caching exists).
|
||||
|
||||
|
|
@ -18,7 +16,7 @@ If you need to query a setup table, but not sure if the table is yet in memory f
|
|||
|
||||
Allows a central function to validate critical fields before processing automated code. **TESTFIELD** calls are added to the function and not the INIT call in order to ensure that critical functions do not run without this human check in place as these may rely on key configuration. If you have a design where specific field values are critical (custom or standard) for integrity, then INIT INSERT is a dangerous default which could result in the corruption of processes. This is separate automated INIT INSERT from a setup page.
|
||||
|
||||
## **NAV Specific Example**
|
||||
## NAV Specific Example
|
||||
|
||||
A global variable record and a global status indicator, but within the context of a local function.
|
||||
|
||||
|
|
@ -28,7 +26,7 @@ A global variable record and a global status indicator, but within the context o
|
|||
|
||||
![ ][image2]
|
||||
|
||||
## **NAV Usages**
|
||||
## NAV Usages
|
||||
|
||||
A similar example of this is in Codeunit 80, but without any validation.
|
||||
|
||||
|
|
@ -40,9 +38,7 @@ E.g. If there is a bespoke change, then this existing **GetGLSetup** can be call
|
|||
|
||||
This means that by the time the code reaches the original **GetGLSetup** code requirement, this record is already in memory as the status flag is set to True, eliminating another check. For such Patterns the setup record should never be cleared, it should be called once globally, initiated and left in memory for future checks. It should **not** be declared locally at the same time, review your code for matching local variable usage before implementing this.
|
||||
|
||||
****
|
||||
|
||||
## **Ideas for improvement**
|
||||
## Ideas for improvement
|
||||
|
||||
Merge **Singleton Table** with dynamic validation field meta configuration. This removes the coding of **TESTFIELD** to a central setup check.
|
||||
|
||||
|
|
@ -54,13 +50,11 @@ Example with the **Sales & Receivables Setup**
|
|||
|
||||
[![ ][image7]][anchor7]
|
||||
|
||||
****
|
||||
|
||||
## **Consequences**
|
||||
## Consequences
|
||||
|
||||
If in the circumstance that a field value in the setup table is expected to change within the life of the code component instance. e.g. If a CHANGECOMPANY is required to cross reference another Setup table, use a separate variable.
|
||||
|
||||
## **Related Topics**
|
||||
## Related Topics
|
||||
|
||||
This is related to the **Singleton Table** pattern.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ weight = 1000
|
|||
+++
|
||||
_Authors: Henrik Langbak and Kim Ginnerup, Bording Data_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
This pattern prevent data from being used elsewhere before it is in a system consistent state.
|
||||
NAV inserts a record as soon as the primary key has been set. But the record may not be in a valid state at this point in time. How do you know if a newly inserted record is ready for use?
|
||||
|
||||
## **Description**
|
||||
## Description
|
||||
|
||||
Whenever you need to stall the release of data, you can use this pattern.
|
||||
|
||||
|
|
@ -24,22 +24,11 @@ The solution is an Option Field with two or three values:
|
|||
|
||||
The states should be interpreted as:
|
||||
|
||||
State
|
||||
|
||||
Description
|
||||
|
||||
Open
|
||||
|
||||
Not all data is in place. The record is system inconsistent. The record or record hierarchy is invisible for all other parts of the system.
|
||||
|
||||
Pending
|
||||
|
||||
The record is system consistent. But is awaiting someone to manually raise the state to Released.
|
||||
The record is still invisible.
|
||||
|
||||
Released
|
||||
|
||||
All data is in place and the record is system consistent, and ready for use. It is now visible for the rest of the system. The state can never be reversed.
|
||||
State | Description
|
||||
------|-----
|
||||
Open | Not all data is in place. The record is system inconsistent. The record or record hierarchy is invisible for all other parts of the system.
|
||||
Pending | The record is system consistent. But is awaiting someone to manually raise the state to Released. The record is still invisible.
|
||||
Released | All data is in place and the record is system consistent, and ready for use. It is now visible for the rest of the system. The state can never be reversed.
|
||||
|
||||
The option field name: Release State.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ Group 2: Jens Winberg, Tim Grant, Alen Tot
|
|||
|
||||
Thanks to Tim Grant who had merged the two patterns, corrected, sent for review and finalized them.
|
||||
|
||||
## **Meet the Pattern**
|
||||
## Meet the Pattern
|
||||
|
||||
It should be possible to change which document report object should be used when printing. This configuration can be stored in one place (Report Selection) and available at configuration time. In this situation, the code for printing records can remain the same.
|
||||
|
||||
The purpose of this pattern is to describe the process to implement a reports selection by retrieving the specific **Document Header**, determining which type of document is related to it in the **Report Selection Screen**, and call the report which needs to be printed by passing the specific document header. This specifies the reports that you can print when you work with the various documents for sales and purchases, such as orders, quotes, invoices, and credit memos.
|
||||
|
||||
## **Know the Pattern**
|
||||
## Know the Pattern
|
||||
|
||||
The program can preselect which report will be printed when you print from various types of purchase and sales headers. For example, on an order, the **Order Confirmation** report is automatically printed.
|
||||
|
||||
|
|
@ -26,31 +26,31 @@ The user can, of course, choose to have the program print a different report tha
|
|||
|
||||
This pattern should be used when the user needs to print a new type of document which can have different report layouts, or when the user needs to print several different reports in sequence. This can also be used during a new post and print routine, a test report for journal posting or for re-printing a posted document. Using this pattern will minimize code by allowing a flexible means of specifying different reports to print, instead of hard-coding a report id when printing a document.
|
||||
|
||||
## **Use the Pattern**
|
||||
## Use the Pattern
|
||||
|
||||
The Report Selection Pattern involves the **Report Selection** table as the central configuration element also with the **Report Selection** Page. The table is used in 4 generic functional workflows:
|
||||
|
||||
1) Test Print un-posted
|
||||
1. Test Print un-posted
|
||||
|
||||
If the Report Selection is related to a Test Report relating to a Document:
|
||||
|
||||
* The Document's Page including Printing Actions
|
||||
* Test Report-Print Codeunit 228
|
||||
* The Document's Page including Printing Actions
|
||||
* Test Report-Print Codeunit 228
|
||||
|
||||
2) Print un-posted. If the Report Selection is related to an existing Document (un-posted):
|
||||
2. Print un-posted. If the Report Selection is related to an existing Document (un-posted):
|
||||
|
||||
* The Document's Page (Document / List) including the Printing Actions
|
||||
* The Document-Print Codeunit 229 is used atomically to use the document type in the generic Sales Header table before the report selection is found.
|
||||
* The Document's Page (Document / List) including the Printing Actions
|
||||
* The Document-Print Codeunit 229 is used atomically to use the document type in the generic Sales Header table before the report selection is found.
|
||||
|
||||
3) Print while posting. If the Document is printed at the point of posting then:
|
||||
3. Print while posting. If the Document is printed at the point of posting then:
|
||||
|
||||
* The Document's Post + Print Codeunit (82 or 92), with the related **GetReport** and **PrintReport** functions.
|
||||
* The Document's Post + Print Codeunit (82 or 92), with the related **GetReport** and **PrintReport** functions.
|
||||
|
||||
4) Print after posting. If the Report Selection is related to an existing Document (posted):
|
||||
4. Print after posting. If the Report Selection is related to an existing Document (posted):
|
||||
|
||||
* The specific document related posted header table is used along with a typical function: **PrintRecords** within the respective table.
|
||||
* The specific document related posted header table is used along with a typical function: **PrintRecords** within the respective table.
|
||||
|
||||
## **Example**
|
||||
## Example
|
||||
|
||||
Sales Document (Invoice)
|
||||
|
||||
|
|
@ -65,30 +65,34 @@ Implementation:
|
|||
* Add a new **Option String** to the **Report Selection** table, **Usage** field (including any ML Captions).
|
||||
* Calling of **GetReport** function, passing the document header.
|
||||
|
||||
SalesSetup.GET;
|
||||
IF SalesSetup."Post & Print with ob Queue" THEN
|
||||
```al
|
||||
SalesSetup.GET;
|
||||
IF SalesSetup."Post & Print with ob Queue" THEN
|
||||
SalesPostViaobQueue.EnqueueSalesDoc(SalesHeader)
|
||||
ELSE BEGIN
|
||||
ELSE BEGIN
|
||||
CODEUNIT.RUN(CODEUNIT::"Sales-Post",SalesHeader);
|
||||
GetReport(SalesHeader);
|
||||
END;
|
||||
COMMIT;
|
||||
END;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* Implementing the new document type within the **GetReport** Function, and calling **PrintReport** with the New OptionString
|
||||
|
||||
"Document Type"::Invoice:
|
||||
```al
|
||||
"Document Type"::Invoice:
|
||||
BEGIN
|
||||
IF "Last Posting No." = '' THEN
|
||||
SalesInvHeader."No.":= "No."
|
||||
ELSE
|
||||
SalesInvHeader."No." := "Last Posting No.";
|
||||
SalesInvHeader.SETRECFILTER;
|
||||
PrintReport(ReportSelection.Usage::"S.Invoice");
|
||||
IF "Last Posting No." = '' THEN
|
||||
SalesInvHeader."No.":= "No."
|
||||
ELSE
|
||||
SalesInvHeader."No." := "Last Posting No.";
|
||||
SalesInvHeader.SETRECFILTER;
|
||||
PrintReport(ReportSelection.Usage::"S.Invoice");
|
||||
END;
|
||||
```
|
||||
|
||||
* Implement the new Document Type and calling the associated report ID, passing the document header
|
||||
|
||||
## **NAV Usages**
|
||||
## NAV Usages
|
||||
|
||||
* Codeunits 82, 92, for post and printing purchase and sales documents
|
||||
* Codeunit 229 for Document printing
|
||||
|
|
@ -96,9 +100,7 @@ Implementation:
|
|||
|
||||
This pattern is already used for printing documents like sales invoices, proforma, waybills, Finance Charge Memos, Receipt Documents.
|
||||
|
||||
****
|
||||
|
||||
## **Ideas for improvement**
|
||||
## Ideas for improvement
|
||||
|
||||
The "Usage" Field could be more dynamic through a setup field linked to a document type table mapped to the document header Table ID.
|
||||
|
||||
|
|
@ -110,7 +112,7 @@ Merge the **PrintReport** functions in the purchase and sales post printing func
|
|||
|
||||
Merge the **PrintRecords** functions in the separate posted header tables into a more atomic print selection component.
|
||||
|
||||
## **Related Patterns**
|
||||
## Related Patterns
|
||||
|
||||
* Posting Routine Pattern (Journal/Document)
|
||||
* Post Batch Routine Pattern
|
||||
|
|
|
|||
|
|
@ -4,25 +4,15 @@ weight = 120
|
|||
+++
|
||||
_By Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
_
|
||||
_
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**
|
||||
**
|
||||
**Aliases:** Encapsulation, Separation of Concerns [1]
|
||||
|
||||
**Aliases:** Encapsulation, Separation of Concerns \[1\]
|
||||
|
||||
**
|
||||
**
|
||||
|
||||
**Context**: You want to store and protect sensitive data which already exists in a system, but it is not clear which data needs protection and how to store it.
|
||||
|
||||
**Problem**: Sensitive data is scattered and mixed with other data in various parts of the system (passwords residing in the same table with non-sensitive data, part of the private information might be stored in files, hardcoded text constants, hardcoded info as part of the code etc.).
|
||||
|
||||
****
|
||||
|
||||
**Forces:**
|
||||
|
||||
* **Obscurity:** sensitive data mixed with other data and code makes it hard to have an overview of what needs to be protected.
|
||||
|
|
@ -37,16 +27,13 @@ To apply this pattern in Dynamics NAV, a table structure similar to Table 1261 S
|
|||
|
||||
Figure 1 describes the definition of a table which is already available in Dynamics NAV. This table can be used for storing sensitive data. As a minimum, the table only needs two fields:
|
||||
|
||||
1. The key is of type GUID (Globally Unique Identifier), which is a 128-bit value consisting of multiple groups of hexadecimal digits \[2\]. Each key needs to be unique and will be used for storing and retrieving the protected information.
|
||||
2. The value (the actual data to be encapsulated) is of type BLOB (Binary Large OBject) \[3\], which contains the encrypted or un-encrypted data (for encryption, see the related [**Encryption**][anchor2] pattern).
|
||||
1. The key is of type GUID (Globally Unique Identifier), which is a 128-bit value consisting of multiple groups of hexadecimal digits [2]. Each key needs to be unique and will be used for storing and retrieving the protected information.
|
||||
2. The value (the actual data to be encapsulated) is of type BLOB (Binary Large OBject) [3], which contains the encrypted or un-encrypted data (for encryption, see the related [**Encryption**][anchor2] pattern).
|
||||
|
||||
[![Figure 1- Example definition, table used for Data Encapsulation][image1]][anchor3]
|
||||
|
||||
_Figure 1- Example definition, table used for **Sensitive** **Data** **Encapsulation**_
|
||||
|
||||
**
|
||||
**
|
||||
|
||||
Let's use a software system (which can be a Dynamics NAV extension or customization). Chances are that the system will look similar to the one described in part 1 of Figure 2: there is data handled in various places of the system, on various storage solutions. Intertwined with normal data, there is sensitive data. For example: Dynamics CRM (Customer Relationship Management) connection information could be all stored in one table, and consists of the connection URI (normal data), enabled/disabled status (normal data), and connection password (sensitive data).
|
||||
|
||||
Figure 2 illustrates the system before and after applying the **Sensitive Data Encapsulation** pattern.
|
||||
|
|
@ -64,14 +51,12 @@ Furthermore, actors which have to interact with the data, need to remember where
|
|||
|
||||
_Figure 2 - Data access before and after **Sensitive** **Data** **Encapsulation.**_
|
||||
|
||||
.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
* **Clarity:** when all sensitive data is encapsulated in one place, it is clear which is the information that needs to be protected.
|
||||
* **Simplicity:** easier to protect just a limited number of known resources when they are grouped.
|
||||
* **Homogeneity: **the same protection can be applied to all data, since it is stored in the same place.
|
||||
* **Separation of concerns:** treat each section of the computer program differently, by separating it and clearly addressing its own requirements and limitations. \[1\]
|
||||
* **Homogeneity:** the same protection can be applied to all data, since it is stored in the same place.
|
||||
* **Separation of concerns:** treat each section of the computer program differently, by separating it and clearly addressing its own requirements and limitations. [1]
|
||||
* **Performance**: data protection techniques like **Access Control** and **Encryption** can now be applied only to the sensitive data (not to everything), which improves the performance of the system.
|
||||
|
||||
**Drawbacks:**
|
||||
|
|
@ -82,59 +67,35 @@ _Figure 2 - Data access before and after **Sensitive** **Data** **Encapsulation.
|
|||
|
||||
**References**
|
||||
|
||||
\[1\]
|
||||
[1] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Separation_of_concerns.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Separation\_of\_concerns.
|
||||
[2] "GUID Structure," [Online]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
|
||||
\[2\]
|
||||
[3] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Binary_large_object.
|
||||
|
||||
"GUID Structure," \[Online\]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
[4] waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
|
||||
\[3\]
|
||||
[5] Microsoft, "Multitenant Deployment Architecture," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Binary\_large\_object.
|
||||
[6] B. Botez, "Setup Table design pattern," Microsoft, 2013\. [Online]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. [Accessed 31 07 2016].
|
||||
|
||||
\[4\]
|
||||
[7] "Shotgun Surgery," [Online]. Available: https://en.wikipedia.org/wiki/Shotgun_surgery. [Accessed 31 07 2016].
|
||||
|
||||
waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
[8] M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[5\]
|
||||
[9] "Masking out," [Online]. Available: https://en.wikipedia.org/wiki/Data_masking#Masking_out. [Accessed 29 7 2016].
|
||||
|
||||
Microsoft, "Multitenant Deployment Architecture," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
[10] "Key Vault," Microsoft, [Online]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[6\]
|
||||
[11] "How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. [Accessed 2 8 2016].
|
||||
|
||||
B. Botez, "Setup Table design pattern," Microsoft, 2013\. \[Online\]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. \[Accessed 31 07 2016\].
|
||||
|
||||
\[7\]
|
||||
|
||||
"Shotgun Surgery," \[Online\]. Available: https://en.wikipedia.org/wiki/Shotgun\_surgery. \[Accessed 31 07 2016\].
|
||||
|
||||
\[8\]
|
||||
|
||||
M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[9\]
|
||||
|
||||
"Masking out," \[Online\]. Available: https://en.wikipedia.org/wiki/Data\_masking\#Masking\_out. \[Accessed 29 7 2016\].
|
||||
|
||||
\[10\]
|
||||
|
||||
"Key Vault," Microsoft, \[Online\]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[11\]
|
||||
|
||||
"How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. \[Accessed 2 8 2016\].
|
||||
|
||||
\[12\]
|
||||
|
||||
"sniffer," \[Online\]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef\_sniffer.htm. \[Accessed 02 08 2016\].
|
||||
[12] "sniffer," [Online]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef_sniffer.htm. [Accessed 02 08 2016].
|
||||
|
||||
|
||||
|
||||
[anchor0]: Logo-_2D00_-Protected-Data-Encapsulation.png
|
||||
[anchor1]: /nav/w/designpatterns/277.3-single-point-of-acces
|
||||
[anchor2]: /nav/w/designpatterns/276.2-data-encryption
|
||||
[anchor1]: /navpatterns/1-patterns/security/3-single-point-of-access/
|
||||
[anchor2]: /navpatterns/1-patterns/security/2-data-encryption/
|
||||
[anchor3]: Data-Encapsulation-_2D00_-figure-1.png
|
||||
[anchor4]: Multi-_2D00_-1-2.JPG
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,12 @@ weight = 140
|
|||
+++
|
||||
_By Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
__
|
||||
|
||||
_[![ ][image0]][anchor0]
|
||||
_
|
||||
|
||||
****
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Context**: After applying [**Sensitive Data Encapsulation**][anchor1], all sensitive data is gathered in a known place in the database. This makes it possible to apply further protection best practices.
|
||||
|
||||
**Problem**: If any non-authorized actor manages to get access to a copy of the database, the sensitive data is immediately available in clear-text.
|
||||
|
||||
**
|
||||
**
|
||||
|
||||
**Forces:**
|
||||
|
||||
* **Accessibility:** anyone who managed to steal a copy of the database can at once read the sensitive information.
|
||||
|
|
@ -30,59 +22,23 @@ Figure 1 adds one more step (in continuation of the figure in pattern **[Sensiti
|
|||
|
||||
_Figure 1- **Encryption** becomes easier after first applying [**Sensitive Data Encapsulation**][anchor1]._
|
||||
|
||||
****
|
||||
|
||||
**Usage**: in Dynamics NAV, codeunit 1266 Encryption Management offers an API for encryption of data. The available functionality is described in Table 1\.
|
||||
|
||||
Table 1- Encryption functionality in Dynamics NAV (found in codeunit 1266 Encryption Management)
|
||||
|
||||
**Procedure**
|
||||
|
||||
**Description**
|
||||
|
||||
**EnableEncryption**
|
||||
|
||||
Confirms with the user before enabling encryption in all companies of the current database.
|
||||
|
||||
**EnableEncryptionSilently**
|
||||
|
||||
Enables encryption without UI interaction (to be used for web services).
|
||||
|
||||
**DisableEncryption**
|
||||
|
||||
Disables encryption. Has a boolean "Silent" parameter which enables/disables UI interaction.
|
||||
|
||||
**Encrypt**
|
||||
|
||||
Checks that encryption is possible before encrypting the given text.
|
||||
|
||||
**Decrypt**
|
||||
|
||||
Checks that encryption is possible before decrypting the given text.
|
||||
|
||||
**ExportKey**
|
||||
|
||||
If encryption is possible, it shows a confirmation dialog to confirm the export of the encryption key, and proceeds to saving it to a location chosen by the user.
|
||||
|
||||
**ImportKey**
|
||||
|
||||
Imports the encryption key from a user chosen location.
|
||||
|
||||
**ChangeKey**
|
||||
|
||||
Changes the encryption key.
|
||||
|
||||
**IsEncryptionEnabled**
|
||||
|
||||
Returns TRUE is encryption is enabled.
|
||||
|
||||
**IsEncryptionPossible**
|
||||
|
||||
Check is the correct key is present, which only works if encryption is enabled.
|
||||
|
||||
**DeleteEncryptedDataInAllCompanies**
|
||||
|
||||
Confirms through UI and if the user agrees, deletes all data stored in the Service Password table for all companies in the current database. At the end, it deletes the encryption key.
|
||||
Procedure | Description
|
||||
----------|------------
|
||||
EnableEncryption | Confirms with the user before enabling encryption in all companies of the current database.
|
||||
EnableEncryptionSilently | Enables encryption without UI interaction (to be used for web services).
|
||||
DisableEncryption | Disables encryption. Has a boolean "Silent" parameter which enables/disables UI interaction.
|
||||
Encrypt | Checks that encryption is possible before encrypting the given text.
|
||||
Decrypt | Checks that encryption is possible before decrypting the given text.
|
||||
ExportKey | If encryption is possible, it shows a confirmation dialog to confirm the export of the encryption key, and proceeds to saving it to a location chosen by the user.
|
||||
ImportKey | Imports the encryption key from a user chosen location.
|
||||
ChangeKey | Changes the encryption key.
|
||||
IsEncryptionEnabled | Returns TRUE is encryption is enabled.
|
||||
IsEncryptionPossible | Check is the correct key is present, which only works if encryption is enabled.
|
||||
DeleteEncryptedDataInAllCompanies | Confirms through UI and if the user agrees, deletes all data stored in the Service Password table for all companies in the current database. At the end, it deletes the encryption key.
|
||||
|
||||
**User confirmation of encryption**
|
||||
|
||||
|
|
@ -92,19 +48,16 @@ However, in some versions of Dynamics NAV, there are places where any user who h
|
|||
|
||||
To ask the security administrator if they want to enable encryption, plug the code below into the OnValidate trigger of the data field which is to be encrypted. This trigger will then get executed each time there's a change on the password field. For example, if the user is entering a password, then add a call to this procedure to Password -- OnValidate() trigger:
|
||||
|
||||
```al
|
||||
EncryptionIsNotActivatedQst@1001 : TextConst 'ENU=Data encryption is not activated. It is recommended that you encrypt data. \\Do you want to open the Data Encryption Management window?';
|
||||
|
||||
LOCAL PROCEDURE CheckEncryption@6();
|
||||
|
||||
BEGIN
|
||||
|
||||
IF NOT ENCRYPTIONENABLED THEN
|
||||
|
||||
IF CONFIRM(EncryptionIsNotActivatedQst) THEN
|
||||
|
||||
PAGE.RUN(PAGE::"Data Encryption Management");
|
||||
|
||||
IF NOT ENCRYPTIONENABLED THEN
|
||||
IF CONFIRM(EncryptionIsNotActivatedQst) THEN
|
||||
PAGE.RUN(PAGE::"Data Encryption Management");
|
||||
END;
|
||||
```
|
||||
|
||||
The code above will guide the user on encrypting, storing the encryption key and choosing a safe password, as follows. Consider for example that NAV is used by a small business, where Stan is the business owner. Hence, Stan has access to all NAV setup and security decisions. Stan wants to enable a connection between NAV and CRM. In NAV, he opens page CRM Connection Setup and enters the URL, user name and password for CRM (Figure 10). When Stan leaves the password field, if data encryption is not enabled, then he sees the confirmation message in Figure 2\.
|
||||
|
||||
|
|
@ -157,66 +110,42 @@ In the end, Stan chooses a location for the encryption key on disk, in a standar
|
|||
|
||||
**References**
|
||||
|
||||
\[1\]
|
||||
[1] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Separation_of_concerns.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Separation\_of\_concerns.
|
||||
[2] "GUID Structure," [Online]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
|
||||
\[2\]
|
||||
[3] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Binary_large_object.
|
||||
|
||||
"GUID Structure," \[Online\]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
[4] waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
|
||||
\[3\]
|
||||
[5] Microsoft, "Multitenant Deployment Architecture," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Binary\_large\_object.
|
||||
[6] B. Botez, "Setup Table design pattern," Microsoft, 2013\. [Online]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. [Accessed 31 07 2016].
|
||||
|
||||
\[4\]
|
||||
[7] "Shotgun Surgery," [Online]. Available: https://en.wikipedia.org/wiki/Shotgun_surgery. [Accessed 31 07 2016].
|
||||
|
||||
waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
[8] M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[5\]
|
||||
[9] "Masking out," [Online]. Available: https://en.wikipedia.org/wiki/Data_masking#Masking_out. [Accessed 29 7 2016].
|
||||
|
||||
Microsoft, "Multitenant Deployment Architecture," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
[10] "Key Vault," Microsoft, [Online]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[6\]
|
||||
[11] "How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. [Accessed 2 8 2016].
|
||||
|
||||
B. Botez, "Setup Table design pattern," Microsoft, 2013\. \[Online\]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. \[Accessed 31 07 2016\].
|
||||
|
||||
\[7\]
|
||||
|
||||
"Shotgun Surgery," \[Online\]. Available: https://en.wikipedia.org/wiki/Shotgun\_surgery. \[Accessed 31 07 2016\].
|
||||
|
||||
\[8\]
|
||||
|
||||
M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[9\]
|
||||
|
||||
"Masking out," \[Online\]. Available: https://en.wikipedia.org/wiki/Data\_masking\#Masking\_out. \[Accessed 29 7 2016\].
|
||||
|
||||
\[10\]
|
||||
|
||||
"Key Vault," Microsoft, \[Online\]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[11\]
|
||||
|
||||
"How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. \[Accessed 2 8 2016\].
|
||||
|
||||
\[12\]
|
||||
|
||||
"sniffer," \[Online\]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef\_sniffer.htm. \[Accessed 02 08 2016\].
|
||||
[12] "sniffer," [Online]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef_sniffer.htm. [Accessed 02 08 2016].
|
||||
|
||||
|
||||
|
||||
[anchor0]: Logo-_2D00_-Encryption.png
|
||||
[anchor1]: /nav/w/designpatterns/275.1-sensitive-data-encapsulation
|
||||
[anchor2]: /nav/w/designpatterns/275.sensitive-data-encapsulation
|
||||
[anchor1]: /navpatterns/1-patterns/security/1-sensitive-data-encapsulation/
|
||||
[anchor2]: /navpatterns/1-patterns/security/1-sensitive-data-encapsulation/
|
||||
[anchor3]: Multi-_2D00_-1-2-3.JPG
|
||||
[anchor4]: Encryption-_2D00_-1.JPG
|
||||
[anchor5]: Encryption-_2D00_-2.png
|
||||
[anchor6]: Encryption-_2D00_-3.png
|
||||
[anchor7]: Encryption-_2D00_-4.png
|
||||
[anchor8]: Encryption-_2D00_-5.png
|
||||
[anchor9]: /nav/w/designpatterns/277.3-single-point-of-acces
|
||||
[anchor9]: /navpatterns/1-patterns/security/3-single-point-of-access/
|
||||
|
||||
|
||||
[image0]: Logo-_2D00_-Encryption.png
|
||||
|
|
|
|||
|
|
@ -4,28 +4,23 @@ weight = 160
|
|||
+++
|
||||
_By Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
_[![ ][image0]][anchor0]
|
||||
_
|
||||
|
||||
__
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Context**: Protected data needs to be used. There are many types of entities which might attempt to use the data.
|
||||
|
||||
**Problem**: If no standard way of accessing data exists, then each entity might attempt to build its own system for handing the sensitive data. The data access layer might be implemented over and over again by each entity, without reuse of known best practices and with a lot of code duplication.
|
||||
|
||||
****
|
||||
|
||||
**Forces:**
|
||||
|
||||
* **Code duplication:** if each entity attempts to write its own routines for data access, invariably this will bring duplication.
|
||||
* **No knowledge reuse:** if a bug is found and fixed in one of the implementations, there is no guarantee that all the other implementations will be updated. For example
|
||||
* **Double-Encryption**: lack of care or knowledge could lead a NAV developer to attempt to double-encrypt strings, which would render them unusable.
|
||||
* **Multi-company configuration \[4\]**: in NAV, it is possible to store multiple companies on the same tenant \[5\] database. A developer who has not investigated such a configuration and its consequences on encryption, can attempt to encrypt data in (for example) a Setup Table \[6\] of only one company, which would make this table unusable from any unencrypted company (since the server will observe that encryption is enabled, and try to retrieve it as it were enabled for all companies and fails).
|
||||
* **"Shotgun surgery" \[7\]:** one change in the data access technique (like a new requirement to validate the user's identity before viewing any protected data), calls for updates in every single implementation of data access, if multiple implementations exist. Hence, one change of requirement triggers multiple efforts to update the product.
|
||||
* **Multi-company configuration [4]**: in NAV, it is possible to store multiple companies on the same tenant [5] database. A developer who has not investigated such a configuration and its consequences on encryption, can attempt to encrypt data in (for example) a Setup Table [6] of only one company, which would make this table unusable from any unencrypted company (since the server will observe that encryption is enabled, and try to retrieve it as it were enabled for all companies and fails).
|
||||
* **"Shotgun surgery" [7]:** one change in the data access technique (like a new requirement to validate the user's identity before viewing any protected data), calls for updates in every single implementation of data access, if multiple implementations exist. Hence, one change of requirement triggers multiple efforts to update the product.
|
||||
|
||||
**Solution:** In Dynamics NAV the same library which offers [**Encryption**][anchor1], is also intended to be used as a **Single Point of Access**. Write code to create, read and remove sensitive data only through codeunit 1266 Encryption Management, and never directly.
|
||||
|
||||
In Figure 9 (figure numbering is continued from pattern **[Encryption][anchor2]**), the panels 1, 2 and 3 have applied the [**Sensitive Data Encapsulation**][anchor3] and [**Encryption** ][anchor1]patterns. Once data is encapsulated, no matter if it has been encrypted or not, the next pattern, **Single Point of Access**, becomes available as seen in the panels 2 and 3 of Figure 1\.
|
||||
In Figure 9 (figure numbering is continued from pattern **[Encryption][anchor2]**), the panels 1, 2 and 3 have applied the [**Sensitive Data Encapsulation**][anchor3] and [**Encryption**][anchor1] patterns. Once data is encapsulated, no matter if it has been encrypted or not, the next pattern, **Single Point of Access**, becomes available as seen in the panels 2 and 3 of Figure 1\.
|
||||
|
||||
You can observe that, in panel 3, each usage needs to access separately the encrypted data. This means that each usage needs to implement again the encryption capabilities. This is resolved in panel 4, where all users access one common API which encrypts, hence the workload of encryption is moved from the usages, to the API, and needs to be implemented only once.
|
||||
|
||||
|
|
@ -33,7 +28,7 @@ You can observe that, in panel 3, each usage needs to access separately the encr
|
|||
|
||||
_Figure 1- **Single Point of Access** pattern applied._
|
||||
|
||||
This shows how pattern application is an iterative process, where one step follows another. Refactoring \[8\] the code to apply one pattern, cleans and clarifies the code and in some cases, makes clear the possibility of further refactoring.
|
||||
This shows how pattern application is an iterative process, where one step follows another. Refactoring [8] the code to apply one pattern, cleans and clarifies the code and in some cases, makes clear the possibility of further refactoring.
|
||||
|
||||
**Usage**: call the procedures available in codeunit 1266 Encryption Management to store and access sensitive data.
|
||||
|
||||
|
|
@ -51,60 +46,36 @@ This shows how pattern application is an iterative process, where one step follo
|
|||
|
||||
**References**
|
||||
|
||||
\[1\]
|
||||
[1] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Separation_of_concerns.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Separation\_of\_concerns.
|
||||
[2] "GUID Structure," [Online]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
|
||||
\[2\]
|
||||
[3] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Binary_large_object.
|
||||
|
||||
"GUID Structure," \[Online\]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
[4] waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
|
||||
\[3\]
|
||||
[5] Microsoft, "Multitenant Deployment Architecture," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Binary\_large\_object.
|
||||
[6] B. Botez, "Setup Table design pattern," Microsoft, 2013\. [Online]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. [Accessed 31 07 2016].
|
||||
|
||||
\[4\]
|
||||
[7] "Shotgun Surgery," [Online]. Available: https://en.wikipedia.org/wiki/Shotgun_surgery. [Accessed 31 07 2016].
|
||||
|
||||
waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
[8] M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[5\]
|
||||
[9] "Masking out," [Online]. Available: https://en.wikipedia.org/wiki/Data_masking#Masking_out. [Accessed 29 7 2016].
|
||||
|
||||
Microsoft, "Multitenant Deployment Architecture," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
[10] "Key Vault," Microsoft, [Online]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[6\]
|
||||
[11] "How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. [Accessed 2 8 2016].
|
||||
|
||||
B. Botez, "Setup Table design pattern," Microsoft, 2013\. \[Online\]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. \[Accessed 31 07 2016\].
|
||||
|
||||
\[7\]
|
||||
|
||||
"Shotgun Surgery," \[Online\]. Available: https://en.wikipedia.org/wiki/Shotgun\_surgery. \[Accessed 31 07 2016\].
|
||||
|
||||
\[8\]
|
||||
|
||||
M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[9\]
|
||||
|
||||
"Masking out," \[Online\]. Available: https://en.wikipedia.org/wiki/Data\_masking\#Masking\_out. \[Accessed 29 7 2016\].
|
||||
|
||||
\[10\]
|
||||
|
||||
"Key Vault," Microsoft, \[Online\]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[11\]
|
||||
|
||||
"How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. \[Accessed 2 8 2016\].
|
||||
|
||||
\[12\]
|
||||
|
||||
"sniffer," \[Online\]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef\_sniffer.htm. \[Accessed 02 08 2016\].
|
||||
[12] "sniffer," [Online]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef_sniffer.htm. [Accessed 02 08 2016].
|
||||
|
||||
|
||||
|
||||
[anchor0]: Logo-_2D00_-Single-Point-of-Access.png
|
||||
[anchor1]: /nav/w/designpatterns/276.2-data-encryption
|
||||
[anchor2]: /nav/w/designpatterns/276.encryption
|
||||
[anchor3]: /nav/w/designpatterns/275.1-sensitive-data-encapsulation
|
||||
[anchor1]: /navpatterns/1-patterns/security/2-data-encryption/
|
||||
[anchor2]: /navpatterns/1-patterns/security/2-data-encryption/
|
||||
[anchor3]: /navpatterns/1-patterns/security/1-sensitive-data-encapsulation/
|
||||
[anchor4]: Multi-_2D00_-1-2-3-4.JPG
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ weight = 180
|
|||
+++
|
||||
_By Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
_[![ ][image0]][anchor0]
|
||||
_
|
||||
|
||||
__
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Aliases:** Masking out
|
||||
|
||||
|
|
@ -15,8 +12,6 @@ __
|
|||
|
||||
**Problem**: The entered information is visible during data entry and whenever any user (the one who entered the data, or a foreign user) opens the UI.
|
||||
|
||||
****
|
||||
|
||||
**Forces:**
|
||||
|
||||
* **Information disclosure:** sensitive data is visible in the UI.
|
||||
|
|
@ -31,11 +26,11 @@ _Figure 1 - The field "Password" is masked._
|
|||
|
||||
To apply this pattern in Dynamics NAV, the developer has two choices:
|
||||
|
||||
**1\.** **Mask everywhere**
|
||||
1. **Mask everywhere**
|
||||
|
||||
Mask the field in all pages which expose it or will expose it in the future. In this case, masking needs to be set at the table level, by opening the table in design mode and setting the field's property ExtendedDatatype=Masked.
|
||||
|
||||
**2\.** **Mask only in selected pages**
|
||||
2. **Mask only in selected pages**
|
||||
|
||||
Mask the field in only a subset of pages. Open the pages where the field should be masked in design mode, open the property page for the field in question, and set ExtendedDatatype=Masked. This option can be used for example when a field should be hidden from most users (in most usual pages), but still visible to administrators in specific pages.
|
||||
|
||||
|
|
@ -49,53 +44,29 @@ Mask the field in only a subset of pages. Open the pages where the field should
|
|||
|
||||
**References**
|
||||
|
||||
\[1\]
|
||||
[1] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Separation_of_concerns.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Separation\_of\_concerns.
|
||||
[2] "GUID Structure," [Online]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
|
||||
\[2\]
|
||||
[3] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Binary_large_object.
|
||||
|
||||
"GUID Structure," \[Online\]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
[4] waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
|
||||
\[3\]
|
||||
[5] Microsoft, "Multitenant Deployment Architecture," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Binary\_large\_object.
|
||||
[6] B. Botez, "Setup Table design pattern," Microsoft, 2013\. [Online]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. [Accessed 31 07 2016].
|
||||
|
||||
\[4\]
|
||||
[7] "Shotgun Surgery," [Online]. Available: https://en.wikipedia.org/wiki/Shotgun_surgery. [Accessed 31 07 2016].
|
||||
|
||||
waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
[8] M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[5\]
|
||||
[9] "Masking out," [Online]. Available: https://en.wikipedia.org/wiki/Data_masking\#Masking_out. [Accessed 29 7 2016].
|
||||
|
||||
Microsoft, "Multitenant Deployment Architecture," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
[10] "Key Vault," Microsoft, [Online]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[6\]
|
||||
[11] "How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. [Accessed 2 8 2016].
|
||||
|
||||
B. Botez, "Setup Table design pattern," Microsoft, 2013\. \[Online\]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. \[Accessed 31 07 2016\].
|
||||
|
||||
\[7\]
|
||||
|
||||
"Shotgun Surgery," \[Online\]. Available: https://en.wikipedia.org/wiki/Shotgun\_surgery. \[Accessed 31 07 2016\].
|
||||
|
||||
\[8\]
|
||||
|
||||
M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[9\]
|
||||
|
||||
"Masking out," \[Online\]. Available: https://en.wikipedia.org/wiki/Data\_masking\#Masking\_out. \[Accessed 29 7 2016\].
|
||||
|
||||
\[10\]
|
||||
|
||||
"Key Vault," Microsoft, \[Online\]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[11\]
|
||||
|
||||
"How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. \[Accessed 2 8 2016\].
|
||||
|
||||
\[12\]
|
||||
|
||||
"sniffer," \[Online\]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef\_sniffer.htm. \[Accessed 02 08 2016\].
|
||||
[12] "sniffer," [Online]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef_sniffer.htm. [Accessed 02 08 2016].
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ weight = 190
|
|||
+++
|
||||
_By Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
_[![ ][image0]][anchor0]
|
||||
_
|
||||
|
||||
__
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Context**: The security of data transmission is just as important as the security of data storage. When data is transmitted over the web, Secure Sockets Layer (SSL) is available to be used with the web client in Dynamics NAV. Microsoft's NAV cloud solution has SSL enabled by default. However, if a partner company chooses to deploy their own NAV, then they need to handle SSL explicitly.
|
||||
|
||||
|
|
@ -15,7 +12,7 @@ __
|
|||
|
||||
**Forces:**
|
||||
|
||||
* **Insecure communication:** When the user enters a password, if unprotected, a network sniffer \[12\] could catch and read it. A sniffer is either a software program or hardware device which examine network traffic. Years ago, sniffers were tools used exclusively by professional network engineers, but nowadays, they are also popular with Internet hackers and people just curious about networking. A public Wi-Fi network could easily be eavesdropped by an unwanted actor.
|
||||
* **Insecure communication:** When the user enters a password, if unprotected, a network sniffer [12] could catch and read it. A sniffer is either a software program or hardware device which examine network traffic. Years ago, sniffers were tools used exclusively by professional network engineers, but nowadays, they are also popular with Internet hackers and people just curious about networking. A public Wi-Fi network could easily be eavesdropped by an unwanted actor.
|
||||
|
||||
By using data storage patterns like **Sensitive Data Encapsulation**, **Encryption, Single Point of Access** or **Azure Key Vault**, the sensitive data is preserved securely in the implementation of Dynamics NAV. But before it gets into a secure store, this data needs to be transmitted from the user, through a user interface, on a client-server connection and all the way to the database. Is the data safe while being transmitted?
|
||||
|
||||
|
|
@ -25,7 +22,7 @@ _SSL_ is a web protocol that encrypts data that is transmitted over a network to
|
|||
|
||||
Figure 1 shows data communication between the client (where the user enters data) and the server (which connects further to the database). Without encryption (left side), data is available in clear text over the wire. A person equipped with a network sniffer can easily intercept and read it. On the right side of the picture, SSL is used to encrypt the user's data. Sniffing the traffic would capture the encrypted stream, but access to the real content would be impeded by encryption.
|
||||
|
||||
**[![ ][image1]][anchor1]**
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
_Figure 1 - Data transmission before (http://...) and after SSL encryption (https://...)._
|
||||
|
||||
|
|
@ -42,53 +39,29 @@ _Figure 1 - Data transmission before (http://...) and after SSL encryption (http
|
|||
|
||||
**References**
|
||||
|
||||
\[1\]
|
||||
[1] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Separation_of_concerns.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Separation\_of\_concerns.
|
||||
[2] "GUID Structure," [Online]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
|
||||
\[2\]
|
||||
[3] "Wikipedia," [Online]. Available: https://en.wikipedia.org/wiki/Binary_large_object.
|
||||
|
||||
"GUID Structure," \[Online\]. Available: https://msdn.microsoft.com/en-us/library/aa373931(VS.85).aspx.
|
||||
[4] waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
|
||||
\[3\]
|
||||
[5] Microsoft, "Multitenant Deployment Architecture," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
|
||||
"Wikipedia," \[Online\]. Available: https://en.wikipedia.org/wiki/Binary\_large\_object.
|
||||
[6] B. Botez, "Setup Table design pattern," Microsoft, 2013\. [Online]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. [Accessed 31 07 2016].
|
||||
|
||||
\[4\]
|
||||
[7] "Shotgun Surgery," [Online]. Available: https://en.wikipedia.org/wiki/Shotgun_surgery. [Accessed 31 07 2016].
|
||||
|
||||
waldo, "How Do I: Manage Companies in Microsoft Dynamics NAV 2013 R2".
|
||||
[8] M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[5\]
|
||||
[9] "Masking out," [Online]. Available: https://en.wikipedia.org/wiki/Data_masking#Masking_out. [Accessed 29 7 2016].
|
||||
|
||||
Microsoft, "Multitenant Deployment Architecture," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/dn271675(v=nav.90).aspx.
|
||||
[10] "Key Vault," Microsoft, [Online]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[6\]
|
||||
[11] "How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, [Online]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. [Accessed 2 8 2016].
|
||||
|
||||
B. Botez, "Setup Table design pattern," Microsoft, 2013\. \[Online\]. Available: https://community.dynamics.com/nav/w/designpatterns/76.setup-table. \[Accessed 31 07 2016\].
|
||||
|
||||
\[7\]
|
||||
|
||||
"Shotgun Surgery," \[Online\]. Available: https://en.wikipedia.org/wiki/Shotgun\_surgery. \[Accessed 31 07 2016\].
|
||||
|
||||
\[8\]
|
||||
|
||||
M. Fowler, Refactoring: Improving the design of existing code, Addison Wesley, 1999\.
|
||||
|
||||
\[9\]
|
||||
|
||||
"Masking out," \[Online\]. Available: https://en.wikipedia.org/wiki/Data\_masking\#Masking\_out. \[Accessed 29 7 2016\].
|
||||
|
||||
\[10\]
|
||||
|
||||
"Key Vault," Microsoft, \[Online\]. Available: https://azure.microsoft.com/en-us/services/key-vault/.
|
||||
|
||||
\[11\]
|
||||
|
||||
"How to: Configure SSL to Secure the Connection to Microsoft Dynamics NAV Web Client," Microsoft, \[Online\]. Available: https://msdn.microsoft.com/en-us/library/hh167264(v=nav.90).aspx. \[Accessed 2 8 2016\].
|
||||
|
||||
\[12\]
|
||||
|
||||
"sniffer," \[Online\]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef\_sniffer.htm. \[Accessed 02 08 2016\].
|
||||
[12] "sniffer," [Online]. Available: http://compnetworking.about.com/od/networksecurityprivacy/g/bldef_sniffer.htm. [Accessed 02 08 2016].
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ Use the following patterns to enhance security on your NAV system.
|
|||
|
||||
|
||||
|
||||
[anchor0]: /nav/w/designpatterns/275.sensitive-data-encapsulation
|
||||
[anchor1]: /nav/w/designpatterns/276.encryption
|
||||
[anchor2]: /nav/w/designpatterns/277.single-point-of-access
|
||||
[anchor3]: /nav/w/designpatterns/278.masked-text
|
||||
[anchor4]: /nav/w/designpatterns/279.ssl-in-nav
|
||||
[anchor0]: /navpatterns/1-patterns/security/1-sensitive-data-encapsulation/
|
||||
[anchor1]: /navpatterns/1-patterns/security/2-data-encryption/
|
||||
[anchor2]: /navpatterns/1-patterns/security/3-single-point-of-access/
|
||||
[anchor3]: /navpatterns/1-patterns/security/4-masked-text/
|
||||
[anchor4]: /navpatterns/1-patterns/security/5-ssl-in-nav/
|
||||
|
|
|
|||
|
|
@ -4,49 +4,34 @@ weight = 1060
|
|||
+++
|
||||
_By Jan Hoek at IDYN_
|
||||
|
||||
## **Abstract**
|
||||
## Abstract
|
||||
|
||||
The Setup Specificity Fallback pattern allows users to efficiently define a potentially complex setup in terms of rules and exceptions to these rules, exceptions to the exceptions, etc.
|
||||
|
||||
## **Description**
|
||||
## Description
|
||||
|
||||
The pattern involves a setup table with a compound (i.e. consisting of more than one field) primary key, where each record in the table maps a combination of primary key values to a particular setup value. However, setting up and maintaining each and every combination could prove to be rather labour-intensive.
|
||||
|
||||
With the Setup Specificity Pattern in place, primary key fields in the setup table can have a special value (typically: zero or an empty string) that acts as a wildcard, meaning that the setup record in question applies to a combination of primary key fields with any value in the field containing that wildcard. According to the pattern, when querying the setup table, the application attempts to find the appropriate setup record by searching less and less specific, starting with only specified primary key values, and possibly ending with only wildcard values, replacing specific values with wildcards in a predefined order until a setup record is found, e.g.:
|
||||
|
||||
Step 1\.
|
||||
|
||||
Try to find record with:
|
||||
Field A = value "a"
|
||||
Field B = value "b"
|
||||
If found, return record; if not, go to step 2\.
|
||||
Step 2\.
|
||||
|
||||
Try to find record with:
|
||||
Field A = value "a"
|
||||
Field B = wildcard value
|
||||
If found, return record; if not, go to step 3\.
|
||||
Step 3\.
|
||||
Try to find record with:
|
||||
Field A = wildcard value
|
||||
Field B = value "b"
|
||||
If found, return record; if not, go to step 4\.
|
||||
Step 4\.
|
||||
Try to find record with:
|
||||
Field A = wildcard value
|
||||
Field B = wildcard value
|
||||
If found, return record; if not, optionally return hard-coded value
|
||||
|
|
||||
---|---|---
|
||||
Step 1. | Try to find record with: Field A = value "a" and Field B = value "b" | If found, return record; if not, go to step 2.
|
||||
Step 2. | Try to find record with: Field A = value "a" and Field B = wildcard value | If found, return record; if not, go to step 3\.
|
||||
Step 3. | Try to find record with: Field A = wildcard value and Field B = value "b" | If found, return record; if not, go to step 4.
|
||||
Step 4. | Try to find record with: Field A = wildcard value and Field B = wildcard value | If found, return record; if not, optionally return hard-coded value
|
||||
|
||||
The least specific record can be thought of as the general rule, and each of the subsequent records is an exception to that rule. By defining only exceptions to the rule, instead of each and every combination, the burden of setting up and maintaining the setup records is strongly reduced, and the overall setup is much easier to read and understand.
|
||||
|
||||
## **Usage**
|
||||
## Usage
|
||||
|
||||
1. Create your setup table. Remember that this pattern applies only to setup tables with a compound primary key;
|
||||
2. Create a function that will return the required value from the setup;
|
||||
3. For each level of specificity, the function should test if the corresponding record exists. If it does, the setup value should be returned. If it doesn't, the logic should fall-back to a lower level of specificity.
|
||||
4. (Optional) If even the least specific setup record can not be found, the function may return a blank value or hard-coded default value.
|
||||
|
||||
## **NAV Usages**
|
||||
## NAV Usages
|
||||
|
||||
The Printer Selection table in NAV is an example of the Setup Specificity Fallback pattern. Its primary key consists of two fields. A blank value in either field means that the record applies to any user or report respectively.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ This pattern is about silently handing file transfers between NAV Service Tier a
|
|||
|
||||
## Description
|
||||
|
||||
As a terminology clarification \[1\], note that both "upload" and "download" are named as seen from the client's point of view:
|
||||
As a terminology clarification [1], note that both "upload" and "download" are named as seen from the client's point of view:
|
||||
|
||||
* Download" defines transferring a file from the server to the client.
|
||||
* "Upload" transfers the file from the client to the server.
|
||||
|
|
@ -21,25 +21,29 @@ Sometimes, files must be transferred to or from known locations without triggeri
|
|||
|
||||
In the following, both the historical and the recommended ways of silently transferring files are described. Since we keep both implementations possible for the sake of backward compatibility, we strongly recommend that you use the file-transfer API provided that is provided with the File Management codeunit (419).
|
||||
|
||||
The legacy API for file transfers \[2\]:
|
||||
The legacy API for file transfers [2]:
|
||||
|
||||
\[Ok :=\] UPLOAD(DialogTitle, FromFolder, FromFilter, FromFile, ToFile)
|
||||
```al
|
||||
[Ok :=] UPLOAD(DialogTitle, FromFolder, FromFilter, FromFile, ToFile)
|
||||
|
||||
\[Ok :=\] DOWNLOAD(FromFile, DialogTitle, ToFolder, ToFilter, ToFile)
|
||||
[Ok :=] DOWNLOAD(FromFile, DialogTitle, ToFolder, ToFilter, ToFile)
|
||||
```
|
||||
|
||||
As you can see, this historical API leaves no place for turning off the functionality for showing a dialog. Historically, NAV offered a remedy to this, namely by using the "Magicpath" string, which is the constant '<TEMP\>'. Under this condition, the way to invoke silent file upload or download becomes:
|
||||
|
||||
\[Ok :=\] UPLOAD(DialogTitle, Magicpath, FromFilter, FromFile, ToFile)
|
||||
```al
|
||||
[Ok :=] UPLOAD(DialogTitle, Magicpath, FromFilter, FromFile, ToFile)
|
||||
|
||||
\[Ok :=\] DOWNLOAD(FromFile, DialogTitle, Magicpath, ToFilter, ToFile)
|
||||
[Ok :=] DOWNLOAD(FromFile, DialogTitle, Magicpath, ToFilter, ToFile)
|
||||
```
|
||||
|
||||
This remedy introduced an issue: If we use "Magicpath" instead of **FromFolder** and **ToFolder** specifications, then where do we upload from and where do we download to? The answer is that they are uploaded to and downloaded from the NAV server's temporary folder. The path to the temporary file can be obtained when this file is created, by using the following function in **File Management: <tempFileName\> := ServerTempFileName(<fileExtension\>)**.
|
||||
|
||||
The new API for file transfers in the **File Management **codeunit:
|
||||
The new API for file transfers in the **File Management** codeunit:
|
||||
|
||||
\[Text :=\] UploadFileSilent(ClientFilePath)
|
||||
[Text :=] UploadFileSilent(ClientFilePath)
|
||||
|
||||
\[Text :=\] DownloadTempFile(ServerFileName)
|
||||
[Text :=] DownloadTempFile(ServerFileName)
|
||||
|
||||
Using the API in the **File Management** codeunit instead of the historical API is recommended for all file transferring and file management in NAV implementations.
|
||||
|
||||
|
|
@ -59,21 +63,26 @@ The production manager wants to have the file in a predefined location on his ha
|
|||
|
||||
The NAV developer has written a module to query the CRONUS database and to export the product list in the pre-described XML format required by the web shop. He saves the data in a temporary server file created with this code:
|
||||
|
||||
```al
|
||||
ServerFileName := FileManagement.ServerTempFileName('xml');
|
||||
```
|
||||
|
||||
When the file has been populated with the latest product data, the NAV developer uses the following call to download the file from the temporary location on the server to the predefined location on the client:
|
||||
|
||||
```al
|
||||
FileManagement.DownloadToFile(ServerFileName,ClientFileName);
|
||||
```
|
||||
|
||||
The call to **DownloadToFile** is part of the **File Management **codeunit, and it embeds the silent download offered by **DownloadTempFile**:
|
||||
The call to **DownloadToFile** is part of the **File Management** codeunit, and it embeds the silent download offered by **DownloadTempFile**:
|
||||
|
||||
```al
|
||||
PROCEDURE DownloadToFile@13(ServerFileName@1002 : Text;ClientFileName@1000 : Text);
|
||||
|
||||
VAR TempClientFileName@1001 : Text;
|
||||
|
||||
BEGIN ValidateFileNames(ServerFileName,ClientFileName); TempClientFileName := DownloadTempFile(ServerFileName); MoveFile(TempClientFileName,ClientFileName);
|
||||
|
||||
VAR
|
||||
TempClientFileName@1001 : Text;
|
||||
BEGIN
|
||||
ValidateFileNames(ServerFileName,ClientFileName); TempClientFileName := DownloadTempFile(ServerFileName); MoveFile(TempClientFileName,ClientFileName);
|
||||
END
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ The implementation varies depending on the technology and language used. In obje
|
|||
|
||||
[anchor0]: 0535.Singleton.png
|
||||
[anchor1]: https://en.wikipedia.org/wiki/Singleton_pattern
|
||||
[anchor2]: /nav/w/designpatterns/151.singleton-table
|
||||
[anchor3]: /nav/w/designpatterns/283.singleton-codeunit
|
||||
[anchor2]: /navpatterns/1-patterns/singleton/singleton-table/
|
||||
[anchor3]: /navpatterns/1-patterns/singleton/singleton-codeunit/
|
||||
|
||||
|
||||
[image0]: 0535.Singleton.png
|
||||
|
|
|
|||
|
|
@ -22,23 +22,27 @@ Context: Azure Machine Learning services are paid. However, Dynamics NAV include
|
|||
|
||||
The test below checks that, when the monthly quota is exceeded, then the function IsAzureMLLimitReached returns TRUE.
|
||||
|
||||
\[Test\]
|
||||
PROCEDURE AzureMLProcessingTimeExceedsLimit@11();
|
||||
VAR
|
||||
```al
|
||||
[Test]
|
||||
PROCEDURE AzureMLProcessingTimeExceedsLimit@11();
|
||||
VAR
|
||||
AzureMachineLearningUsage@1004 : Record 2002;
|
||||
ProcessingTime@1000 : Decimal;
|
||||
BEGIN
|
||||
// \[SCENARIO\] Azure Machine Learning Processing time exceeds AzureML limit
|
||||
// \[GIVEN\] AzureMachineLearningUsage \> 0
|
||||
BEGIN
|
||||
// [SCENARIO] Azure Machine Learning Processing time exceeds AzureML limit
|
||||
|
||||
// [GIVEN] AzureMachineLearningUsage > 0
|
||||
Initialize; // calls PermissionManager.SetTestabilitySoftwareAsAService(TRUE);
|
||||
ProcessingTime := LibraryRandom.RandDec(1000,2);
|
||||
AzureMachineLearningUsage.IncrementTotalProcessingTime(ProcessingTime);
|
||||
// \[WHEN\] When IsAzureMLLimitReached is invoked with Limit more than Processing time
|
||||
// \[THEN\] HasAzureLimitReached returns TRUE
|
||||
AzureMachineLearningUsage.IncrementTotalProcessingTime(ProcessingTime);
|
||||
|
||||
// [WHEN] When IsAzureMLLimitReached is invoked with Limit more than Processing time
|
||||
// [THEN] HasAzureLimitReached returns TRUE
|
||||
Assert.IsTrue(AzureMachineLearningUsage.IsAzureMLLimitReached(ProcessingTime - 1),
|
||||
'HasAzureLimitReached returns wrong value when Processing time exceeds Limit.');
|
||||
'HasAzureLimitReached returns wrong value when Processing time exceeds Limit.');
|
||||
PermissionManager.SetTestabilitySoftwareAsAService(FALSE);
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
The figure below explains what happens when the Permission Manager is not a singleton. When it is invoked from different places (first from the test, second from the production code), then different instances of the Permission Manager will fire up and answer. In detail:
|
||||
|
||||
|
|
@ -50,8 +54,6 @@ The figure below explains what happens when the Permission Manager is not a sing
|
|||
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
****
|
||||
|
||||
**Solution:** restrict the number of instantiations of a codeunit to only one, by setting the codeunit property **SingleInstance** to **Yes**.
|
||||
|
||||
Returning to the previous example, let's analyze the case when the codeunit Permission Manager is a singleton codeunit:
|
||||
|
|
@ -60,8 +62,7 @@ Returning to the previous example, let's analyze the case when the codeunit Perm
|
|||
|
||||
When the codeunit Permission Manager is a singleton, then no matter from where it is invoked, the same instance will be reached. Therefore, the status set by the test (SaaS = TRUE) will be reachable from the production code, and the test will pass, as seen in the figure below.
|
||||
|
||||
**[![ ][image3]][anchor3]
|
||||
**
|
||||
[![ ][image3]][anchor3]
|
||||
|
||||
**Consequences**
|
||||
|
||||
|
|
@ -84,9 +85,8 @@ Most of the usages in NAV refer to the so-called "management codeunits". The man
|
|||
* Codeunit 9002 Permission Manager
|
||||
* Etc.
|
||||
|
||||
**Note:** while the object-oriented **Singleton** pattern can restrict the number of instantiations of the singleton to an integer n \> 0, in Dynamics NAV the **Singleton Codeunit** can only have n=1\.
|
||||
**Note:** while the object-oriented **Singleton** pattern can restrict the number of instantiations of the singleton to an integer n > 0, in Dynamics NAV the **Singleton Codeunit** can only have n=1\.
|
||||
|
||||
__
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,10 @@ weight = 1110
|
|||
|
||||
_By Elly Nkya at Microsoft Development Center Copenhagen_
|
||||
|
||||
## [![ ][image0]][anchor0]
|
||||
|
||||
_
|
||||
_
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Problem**: The developer needs to define a single record that can contain a set of rules and behavior (optional, mandatory, or defaulting mechanisms), that apply to a functionality, and can be configured by a user.
|
||||
|
||||
****
|
||||
|
||||
**Forces**
|
||||
|
||||
* You want a central place to define the address and logo of your company (see Company Information table).
|
||||
|
|
@ -36,8 +31,6 @@ In a functionality that is large enough (such as sales, inventory, fixed) you ma
|
|||
|
||||
**4\. Use:** Access the rule in code and use it
|
||||
|
||||
****
|
||||
|
||||
**NAV Usages**
|
||||
|
||||
Rounding rules for Unit-Amounts and Amounts are implemented using the Singleton pattern.
|
||||
|
|
@ -46,35 +39,43 @@ Rounding rules for Unit-Amounts and Amounts are implemented using the Singleton
|
|||
|
||||
**2\. Instantiate:** In codeunit 2, the following code is invoked
|
||||
|
||||
WITH GLSetup DO
|
||||
```al
|
||||
WITH GLSetup DO
|
||||
IF NOT FINDFIRST THEN BEGIN
|
||||
INIT;
|
||||
INSERT;
|
||||
INIT;
|
||||
INSERT;
|
||||
END;
|
||||
```
|
||||
|
||||
**3\. Enforce:** On the General Ledger Setup. The following properties are setup:
|
||||
|
||||
```al
|
||||
DeleteAllowed=false,
|
||||
InsertAllowed=false
|
||||
```
|
||||
|
||||
**4\. Use:** Access the rounding rules are used
|
||||
|
||||
...
|
||||
GLSetup.GET;
|
||||
UnitCostCurrency := ROUND(...,GLSetup."Unit-Amount Rounding Precision");
|
||||
...
|
||||
```al
|
||||
...
|
||||
GLSetup.GET;
|
||||
UnitCostCurrency := ROUND(...,GLSetup."Unit-Amount Rounding Precision");
|
||||
...
|
||||
```
|
||||
|
||||
Or if accessing the rule multiple times and performance is a consideration, use lazy instantiation:
|
||||
|
||||
...
|
||||
GetGLSetup;
|
||||
UnitCostCurrency := ROUND(...,GLSetup."Unit-Amount Rounding Precision");
|
||||
...
|
||||
```al
|
||||
...
|
||||
GetGLSetup;
|
||||
UnitCostCurrency := ROUND(...,GLSetup."Unit-Amount Rounding Precision");
|
||||
...
|
||||
|
||||
LOCAL GetGLSetup()
|
||||
IF NOT GLSetupRead THEN
|
||||
LOCAL GetGLSetup()
|
||||
IF NOT GLSetupRead THEN
|
||||
GLSetup.GET;
|
||||
GLSetupRead := TRUE;
|
||||
GLSetupRead := TRUE;
|
||||
```
|
||||
|
||||
**Related topics**
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ YouTube Video of NAV Singleton:
|
|||
|
||||
[anchor0]: 5554.Singleton-Table.png
|
||||
[anchor1]: https://en.wikipedia.org/wiki/Singleton_pattern
|
||||
[anchor2]: /nav/w/designpatterns/76.setup-table
|
||||
[anchor2]: /navpatterns/1-patterns/singleton/singleton-table/setup-table/
|
||||
[anchor3]: https://www.youtube.com/watch?v=aQPu-s9FkYI&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=13
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,20 +19,15 @@ The overview information consists of summed-up numbers, calculated from business
|
|||
|
||||
**Problem**: NAV stores data in tables. By definition, a table is a repetitive structure containing multiple lines, each line having a different piece of the information. But sometimes this repetitive information needs to be summed-up or otherwise synthetized, and presented as an overview.
|
||||
|
||||
|
||||
**Solution:** Store overview information in a singleton table.
|
||||
|
||||
****
|
||||
|
||||
There are two ways of calculating overview information in NAV.
|
||||
|
||||
1. By using a FlowField. This applies for simpler calculations, like filtered or unfiltered counts, sums etc.
|
||||
2. By writing C/AL code to perform custom calculations. Use this when:
|
||||
|
||||
* * The way to calculate the overview is too complex for flow fields, or
|
||||
* The data needs to be pulled from an external system (like Dynamics CRM, QuickBooks or any external integration).
|
||||
|
||||
****
|
||||
* The way to calculate the overview is too complex for flow fields, or
|
||||
* The data needs to be pulled from an external system (like Dynamics CRM, QuickBooks or any external integration).
|
||||
|
||||
The implementation of Cues is already described in detail on MSDN, in [Creating and Customizing Cues][anchor4] and in [Walkthrough: Creating a Cue Based on a FlowField][anchor5].
|
||||
|
||||
|
|
@ -40,92 +35,34 @@ The implementation of Cues is already described in detail on MSDN, in [Creating
|
|||
|
||||
_Table 1 - Cue tables in Dynamics NAV_ shows some examples of singleton tables used for creating Cues.
|
||||
|
||||
Table ID
|
||||
|
||||
Table Name
|
||||
|
||||
1313
|
||||
|
||||
Activities Cue
|
||||
|
||||
5370
|
||||
|
||||
CRM Synch. Job Status Cue
|
||||
|
||||
9042
|
||||
|
||||
Team Member Cue
|
||||
|
||||
9050
|
||||
|
||||
Warehouse Basic Cue
|
||||
|
||||
9051
|
||||
|
||||
Warehouse WMS Cue
|
||||
|
||||
9052
|
||||
|
||||
Service Cue
|
||||
|
||||
9053
|
||||
|
||||
Sales Cue
|
||||
|
||||
9054
|
||||
|
||||
Finance Cue
|
||||
|
||||
9055
|
||||
|
||||
Purchase Cue
|
||||
|
||||
9056
|
||||
|
||||
Manufacturing Cue
|
||||
|
||||
9057
|
||||
|
||||
Job Cue
|
||||
|
||||
9058
|
||||
|
||||
Warehouse Worker WMS Cue
|
||||
|
||||
9059
|
||||
|
||||
Administration Cue
|
||||
|
||||
9060
|
||||
|
||||
SB Owner Cue
|
||||
|
||||
9061
|
||||
|
||||
RapidStart Services Cue
|
||||
|
||||
9063
|
||||
|
||||
Relationship Mgmt. Cue
|
||||
|
||||
9069
|
||||
|
||||
O365 Sales Cue
|
||||
|
||||
9070
|
||||
|
||||
Accounting Services Cue
|
||||
Table ID | Table Name
|
||||
---------|-----------
|
||||
1313 | Activities Cue
|
||||
5370 | CRM Synch. Job Status Cue
|
||||
9042 | Team Member Cue
|
||||
9050 | Warehouse Basic Cue
|
||||
9051 | Warehouse WMS Cue
|
||||
9052 | Service Cue
|
||||
9053 | Sales Cue
|
||||
9054 | Finance Cue
|
||||
9055 | Purchase Cue
|
||||
9056 | Manufacturing Cue
|
||||
9057 | Job Cue
|
||||
9058 | Warehouse Worker WMS Cue
|
||||
9059 | Administration Cue
|
||||
9060 | SB Owner Cue
|
||||
9061 | RapidStart Services Cue
|
||||
9063 | Relationship Mgmt. Cue
|
||||
9069 | O365 Sales Cue
|
||||
9070 | Accounting Services Cue
|
||||
|
||||
Table 1 - Cue tables in Dynamics NAV
|
||||
|
||||
___
|
||||
_
|
||||
|
||||
|
||||
|
||||
[anchor0]: Cue-Table.png
|
||||
[anchor1]: /nav/w/designpatterns/151.singleton-table
|
||||
[anchor2]: /nav/w/designpatterns/76.setup-table
|
||||
[anchor1]: /navpatterns/1-patterns/singleton/singleton-table/
|
||||
[anchor2]: /navpatterns/1-patterns/singleton/singleton-table/setup-table/
|
||||
[anchor3]: Cue-Table-Figure-1.JPG
|
||||
[anchor4]: https://msdn.microsoft.com/en-us/library/dn789553(v=nav.90).aspx
|
||||
[anchor5]: https://msdn.microsoft.com/en-us/library/ff477101(v=nav.90).aspx
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ The implementation of the pattern involves several considerations:
|
|||
|
||||
Since this kind of tables is a collection of several environment or setup parameters, the primary key does not refer to any business attributes for this kind of tables. However, for maintaining the integrity of the database, it is necessary to define a primary key.
|
||||
|
||||
So, the most common implementation is to have a field "Primary Key" of Code\[10\]. This is populated with a blank value when the record is inserted. This field is not added to the page, so that the user cannot be modify it later.
|
||||
So, the most common implementation is to have a field "Primary Key" of Code[10]. This is populated with a blank value when the record is inserted. This field is not added to the page, so that the user cannot be modify it later.
|
||||
|
||||
**Creating a Page**
|
||||
|
||||
|
|
@ -31,12 +31,14 @@ The **CardPage** type is most suitable for representing this kind of tables. In
|
|||
|
||||
In the **OnOpenPage** trigger, the following code should be added to insert a record when the user opens the page for the first time, if a record does not exist already.
|
||||
|
||||
OnOpenPage()
|
||||
```al
|
||||
OnOpenPage()
|
||||
RESET;
|
||||
IF NOT GET THEN BEGIN
|
||||
INIT;
|
||||
INSERT;
|
||||
INIT;
|
||||
INSERT;
|
||||
END;
|
||||
```
|
||||
|
||||
The following diagram describes the flow of the program, once the user tries to access the setup information. The user opens the page. If the record containing setup information already exists, then the page opens on the existing record. Else, a new empty record is created and the page opens on it.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
|
|
@ -8,7 +8,7 @@ _Originally by Abhishek Ghosh, at Microsoft Development Center Copenhagen_
|
|||
|
||||
This pattern generates the data to be displayed dynamically by combing/processing several data sources. It then displays the resulting dataset without writing to the database.
|
||||
|
||||
[![ ][image0]][anchor0][][anchor1]
|
||||
[![ ][image6]][anchor9]
|
||||
|
||||
## Description
|
||||
|
||||
|
|
@ -24,39 +24,29 @@ This pattern takes a two-step approach to displaying the data:
|
|||
|
||||
* Iterate through a dataitem of the Integer table and display one record from the temporary recordset in each iteration.
|
||||
|
||||
Step 1: Combining data sources to create a dataset
|
||||
#### Step 1: Combining data sources to create a dataset
|
||||
|
||||
In this step, we would process the existing data to create a temporary recordset. The three most common techniques to do this are discussed in the following paragraphs.
|
||||
|
||||
The first technique is mostly used when we want to build the report based on one or more source tables. A lot of processing is required, and we therefore want to store and display information from a temporary recordset. With this technique, we create a dataitem of the source record and then iterate through this dataitem to create the temporary recordset. An advantage of this technique is that it allows the user to perform additional filtering on the data source tables since they are added as additional dataitems and therefore will have their tabs on the request page by default.
|
||||
|
||||
[![ ][image1]][anchor2][][anchor3]
|
||||
[![ ][image7]][anchor10]
|
||||
|
||||
The second technique was made available with NAV 2013 when queries were introduced as a tool to help us combine data from different sources. Instead of writing data into a temporary record variable, we can create a query to combine the data from different sources. This offers better performance than the first technique in almost every situation. However, with a query, we sacrifice the luxury of getting a flexible filtering on the request page for the source dataitem.
|
||||
|
||||
The third technique is to write a function (or a codeunit, if the complexity demands so) that will crunch the data and create the temporary record variable. This function must be invoked from the OnPreReport trigger (or the OnPreDataItem trigger of the Integer dataitem).
|
||||
|
||||
[![ ][image2]][anchor4]
|
||||
[![ ][image8]][anchor11]
|
||||
|
||||
The following table summarizes when to use each of the three techniques:
|
||||
|
||||
Technique
|
||||
Technique | When to Use
|
||||
----------|------------
|
||||
Source Record DataItem | When it is important to offer flexible filtering possibilities on the source data.
|
||||
Query | When performance is critical.
|
||||
Populating temporary table in a function | Only when the source dataset is too complex to use either of the other techniques.
|
||||
|
||||
When to Use
|
||||
|
||||
Source Record DataItem
|
||||
|
||||
When it is important to offer flexible filtering possibilities on the source data.
|
||||
|
||||
Query
|
||||
|
||||
When performance is critical.
|
||||
|
||||
Populating temporary table in a function
|
||||
|
||||
Only when the source dataset is too complex to use either of the other techniques.
|
||||
|
||||
Step 2: Iterating through the Integer dataitem
|
||||
#### Step 2: Iterating through the Integer dataitem
|
||||
|
||||
When you have created the dataset as a temporary record variable or a query, the next step is to iterate through them to display the information. However, the report controller in NAV cannot iterate through temporary records or through the results of a query. This is where the Integer table comes into use.
|
||||
|
||||
|
|
@ -74,11 +64,11 @@ With the second approach, you must limit the number of times the report iterates
|
|||
|
||||
## Example
|
||||
|
||||
Goal: To print a report that lists all contacts that have open documents (both sales and purchase) and displays those documents in chronological order
|
||||
**Goal:** To print a report that lists all contacts that have open documents (both sales and purchase) and displays those documents in chronological order
|
||||
|
||||
A contact can be connected to a vendor or a customer using the Create As Customer/Vendor function on the Contact Card. When a sales/purchase document is created for this customer/vendor, the Contact No. is stored in the Sell-to Contact No. or Buy-from Contact No. fields of the sales and purchase documents. The obvious choice would be to display the data directly from the Sales Header and Purchase Header records. However, the requirement is to display all of them together chronologically, which means they will need to be stored in, and be read from, one common dataset.
|
||||
|
||||
Step 1: Design a new table that will hold the temporary dataset.
|
||||
**Step 1: Design a new table that will hold the temporary dataset.**
|
||||
|
||||
The UI must display the Document Type, Document No., Document Date, Amount Incl. VAT and the name of the salesperson or purchaser. The table is designed as follows.
|
||||
|
||||
|
|
@ -94,11 +84,11 @@ Some additional properties to note:
|
|||
|
||||
* Purchase Header dataitem should be linked to the Contact dataitem through the DataItemLink property -- "Buy-from Contact No.=FIELD(No.)"
|
||||
|
||||
Step 2: In the Sales Header -- OnAfterGetRecord, write the following code to populate the data buffer:
|
||||
**Step 2: In the Sales Header** -- OnAfterGetRecord, write the following code to populate the data buffer:
|
||||
|
||||
[![ ][image5]][anchor7]
|
||||
|
||||
Where:
|
||||
**Where:**
|
||||
|
||||
* ContactDocumentBuffer is a temporary global variable of the buffer table created.
|
||||
|
||||
|
|
@ -107,25 +97,26 @@ Where:
|
|||
|
||||
Similar code must be written in Purchase Header -- OnAfterGetRecord to store data from the purchase documents into the buffer.
|
||||
|
||||
Step 3: Iterate through the temporary records in the Integer dataitem.
|
||||
**Step 3:** Iterate through the temporary records in the Integer dataitem.
|
||||
|
||||
Since the requirement is to sort the data by Document Date, we must sort the ContactDocumentBuffer table by this key. Also, by the time the execution of Integer -- OnPreDataItem begins, the number of records in the temporary buffer should be known already. Therefore, we can limit the number of times to repeat the Integer dataitem to the number of records in the buffer.
|
||||
|
||||
We meet both requirements with the following lines of code in Integer - OnPreDataItem:
|
||||
|
||||
```al
|
||||
ContactDocumentBuffer.SETCURRENTKEY("Document Date");
|
||||
|
||||
SETRANGE(Number,1,ContactDocumentBuffer.COUNT);
|
||||
```
|
||||
|
||||
Lastly, we must move the record pointer by 1 record every time we loop through the Integer dataitem. So, in Integer -- OnAfterGetRecord, we add the following lines of code:
|
||||
|
||||
```al
|
||||
IF Number = 1 THEN
|
||||
|
||||
ContactDocumentBuffer.FINDFIRST
|
||||
|
||||
ContactDocumentBuffer.FINDFIRST
|
||||
ELSE
|
||||
|
||||
ContactDocumentBuffer.NEXT;
|
||||
ContactDocumentBuffer.NEXT;
|
||||
```
|
||||
|
||||
Now, the only task that remains is to design the RDLC layout. (Not part of this application pattern description.)
|
||||
|
||||
|
|
@ -151,7 +142,9 @@ This is a commonly used pattern in several reports, such as:
|
|||
[anchor6]: 4118.Temporary-Dataset-Report-5.jpg
|
||||
[anchor7]: 4010.Temporary-Dataset-Report-6.jpg
|
||||
[anchor8]: https://www.youtube.com/watch?v=QHn5oEOJv0Q&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=10
|
||||
|
||||
[anchor9]: 0250.Temporary-Dataset-Report-1.png
|
||||
[anchor10]: 2376.Temporary-Dataset-Report-2.png
|
||||
[anchor11]: 7607.Temporary-Dataset-Report-3.png
|
||||
|
||||
[image0]: /resized-image.ashx/__size/550x0/__key/communityserver-wikis-components-files/00-00-00-00-42/0250.Temporary-Dataset-Report-1.gif
|
||||
[image1]: /resized-image.ashx/__size/550x0/__key/communityserver-wikis-components-files/00-00-00-00-42/2376.Temporary-Dataset-Report-2.gif
|
||||
|
|
@ -159,3 +152,6 @@ This is a commonly used pattern in several reports, such as:
|
|||
[image3]: 6523.Temporary-Dataset-Report-4.jpg
|
||||
[image4]: 4118.Temporary-Dataset-Report-5.jpg
|
||||
[image5]: 4010.Temporary-Dataset-Report-6.jpg
|
||||
[image6]: 0250.Temporary-Dataset-Report-1.png
|
||||
[image7]: 2376.Temporary-Dataset-Report-2.png
|
||||
[image8]: 7607.Temporary-Dataset-Report-3.png
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -129,7 +129,7 @@ After Platform added support for this specific case, the layout is very simple,
|
|||
|
||||
This will produce the layout we wanted:
|
||||
|
||||
[![ ][image10]][anchor10]
|
||||
[![ ][image12]][anchor13]
|
||||
|
||||
### UI -- Manual Totals Update
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ As mentioned before, there are cases when the user is informed that totals/disco
|
|||
|
||||
The link is part of the sub-part where totals and discounts are added. The visibility is controlled by actually controlling the text (message, or empty), because Platform does not support making only one visible/invisible (it must be a group). When the link is displayed, the styling is also changed for the total/discount controls, and the values are set to 0\.
|
||||
|
||||
[![ ][image11]][anchor11]
|
||||
[![ ][image13]][anchor14]
|
||||
|
||||
## NAV Usages
|
||||
|
||||
|
|
@ -145,8 +145,6 @@ We have implemented these scenarios for sales and purchase documents (order, inv
|
|||
|
||||
The pattern can easily be extended to new sales and purchase documents. If the pattern is followed, it can also be extended to other types of documents, such as in services, jobs, etc.
|
||||
|
||||
****
|
||||
|
||||
**Note:** This new pattern is not implemented in the North America and India versions. This is due to special local features in the two versions regions and the multitude of sales tax/vat options that would make the solution non-user friendly and not optimized for performance.
|
||||
|
||||
## Ideas for Improvement
|
||||
|
|
@ -170,6 +168,8 @@ Platform support to be able to trigger full-page update from the lines. Support
|
|||
[anchor10]: /cfs-file.ashx/__key/communityserver-wikis-components-files/00-00-00-00-42/Fig-11.jpg
|
||||
[anchor11]: /cfs-file.ashx/__key/communityserver-wikis-components-files/00-00-00-00-42/Fig-12.jpg
|
||||
[anchor12]: https://www.youtube.com/watch?v=U3noU-WT8Xk&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=22
|
||||
[anchor13]: Fig-11.jpg
|
||||
[anchor14]: Fig-12.jpg
|
||||
|
||||
|
||||
[image0]: 4300.Fig-1.jpg
|
||||
|
|
@ -184,3 +184,5 @@ Platform support to be able to trigger full-page update from the lines. Support
|
|||
[image9]: Fig-10.jpg
|
||||
[image10]: /resized-image.ashx/__size/550x0/__key/communityserver-wikis-components-files/00-00-00-00-42/Fig-11.jpg
|
||||
[image11]: /resized-image.ashx/__size/550x0/__key/communityserver-wikis-components-files/00-00-00-00-42/Fig-12.jpg
|
||||
[image12]: Fig-11.jpg
|
||||
[image13]: Fig-12.jpg
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@ weight = 1440
|
|||
+++
|
||||
_By Nikola Kukrika, waldo and Gary Winter_
|
||||
|
||||
# Abstract
|
||||
## Abstract
|
||||
|
||||
The Variant façade provides a single interface that can take any Record, RecordRef or RecordID as an argument. With this pattern the code is encapsulated within the single object, with clear separation between common and table specific code.
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
****
|
||||
|
||||
# Problem
|
||||
## Problem
|
||||
|
||||
Since NAV is strongly typed, developers often need to duplicate functionality in order to add support for a new table. The developer would typically start by duplicating the function, changing the record type and implement slight modifications to the code if needed.
|
||||
|
||||
|
|
@ -50,13 +48,13 @@ Problems arising from this way of implementing code are:
|
|||
|
||||
**Conclusion** - **If the functionality needs to be used for many records the approach of duplicating the functions should be avoided.**
|
||||
|
||||
# Solution
|
||||
## Solution
|
||||
|
||||
The Variant façade pattern provides a single interface that would not need to be changed in the future. It will be able to take any record as a parameter. Common code should be kept separately from record specific code and both must be very visible so the developers can easily see what the differences are.
|
||||
|
||||
The Key components of the pattern are:
|
||||
|
||||
## Signature
|
||||
### Signature
|
||||
|
||||
Instead of hardcoding a record type a variant is used as an argument. A Variant Façade function can receive three types of data: Record, RecordID, or RecordRef. This way, it can be reused anywhere in the product and the code will still work.
|
||||
|
||||
|
|
@ -66,7 +64,7 @@ Example - For codeunit 229, Document-Print all of the public methods can be simp
|
|||
|
||||
With this approach the façade function can serve all of the record types and will not need to change in the future.
|
||||
|
||||
## Casting to a record ref
|
||||
### Casting to a record ref
|
||||
|
||||
After the signature it is necessary to decide if the function will support passing of the Record ID and the RecordRef. Code tends to be easier to understand and maintain if only the records are passed as the arguments, however in some cases it is needed to support the other two types.
|
||||
|
||||
|
|
@ -78,71 +76,67 @@ DataTypeManagement.GetRecordRef(RecRelatedVariant,RecordRef)
|
|||
|
||||
* **Support Records only** - If the function supports only passing in the record, it is a good practice to check if the variant is a record:
|
||||
|
||||
```al
|
||||
IF NOT RecordVariant.ISRECORD THEN
|
||||
ERROR(NotARecordErr);
|
||||
```
|
||||
|
||||
ERROR(NotARecordErr);
|
||||
|
||||
## Using the variant
|
||||
### Using the variant
|
||||
|
||||
The variant can be passed instead of record when calling the Page.RUN, Codeunit.RUN or Report.RUN statically:
|
||||
|
||||
For example:
|
||||
|
||||
```al
|
||||
Page.RUN(PageID,Variant);
|
||||
|
||||
Codeunit.RUN(CodeunitID,Variant);
|
||||
|
||||
Report.RUN(ReportID,Variant);
|
||||
```
|
||||
|
||||
These calls are identical to using an actual instance of the record, since the variant will be casted to the record automatically, with all filters, markings and values preserved.
|
||||
|
||||
****
|
||||
|
||||
In case the variant was casted to the RecordRef (by using DataTypeManagement.GetRecordRef(RecRelatedVariant,RecordRef)), it is still possible to invoke the functions statically.
|
||||
|
||||
The RecordRef simply needs to be casted into a variant and passed as a parameter, for example:
|
||||
|
||||
```al
|
||||
VariantArgument := RecordRef;
|
||||
|
||||
Page.RUN(PageID,VariantArgument);
|
||||
```
|
||||
|
||||
Variant can always casted back to the original record in the table specific code, exact process is described below.
|
||||
|
||||
## Table specific code
|
||||
### Table specific code
|
||||
|
||||
To do table-specific processing, it is necessary to get the RecordRef first, since NUMBER parameter will tell us which table it is. In the table-specific code, it is possible to cast the variant back to the original record type, so data is accessible and it is possible to invoke functions.
|
||||
|
||||
To do this, the best practice is to use the COPY function to preserve filters:
|
||||
|
||||
```al
|
||||
SalesHeader.COPY(RecordVariant);
|
||||
```
|
||||
|
||||
Assigning directly such as SalesHeader := RecordVariant, is possible, however all filters will be lost.
|
||||
|
||||
Example of table specific code:
|
||||
|
||||
```al
|
||||
CASE RecordRef.NUMBER OF
|
||||
|
||||
DATABASE::"Sales Header":
|
||||
|
||||
BEGIN
|
||||
|
||||
SalesHeader.COPY(RecordVariant);
|
||||
|
||||
SalesHeader.PrintDocument;
|
||||
|
||||
END;
|
||||
|
||||
DATABASE::"Purchase Header":
|
||||
|
||||
BEGIN
|
||||
|
||||
PurchaseHeader.COPY(RecordVariant)
|
||||
|
||||
...
|
||||
|
||||
END;
|
||||
|
||||
....
|
||||
DATABASE::"Sales Header":
|
||||
BEGIN
|
||||
SalesHeader.COPY(RecordVariant);
|
||||
SalesHeader.PrintDocument;
|
||||
END;
|
||||
DATABASE::"Purchase Header":
|
||||
BEGIN
|
||||
PurchaseHeader.COPY(RecordVariant)
|
||||
...
|
||||
END;
|
||||
....
|
||||
```
|
||||
|
||||
It is a good practice to try to avoid the table specific code if possible.
|
||||
|
||||
|
|
@ -150,88 +144,66 @@ One of the issues with table specific code is that the CASE statement can easily
|
|||
|
||||
There are two possible solutions:
|
||||
|
||||
1. Move the calculations outside of the façade code unit and pass it in as part of the argument table.
|
||||
1. Move the calculations outside of the façade code unit and pass it in as part of the argument table.
|
||||
|
||||
For example, instead of having a case like this within DocumentPrint codeunit:
|
||||
|
||||
```al
|
||||
CASE RecordRef.NUMBER OF
|
||||
|
||||
DATABASE::"Sales Header":
|
||||
|
||||
BEGIN
|
||||
|
||||
SalesHeader.COPY(RecordVariant);
|
||||
|
||||
CASE SalesHeader."Document Type" OF
|
||||
|
||||
SalesHeader."Document Type"::Quote:
|
||||
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Quote");
|
||||
|
||||
SalesHeader."Document Type"::"Blanket Order":
|
||||
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Blanket");
|
||||
|
||||
SalesHeader."Document Type"::Order:
|
||||
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Order");
|
||||
|
||||
SalesHeader."Document Type"::"Return Order":
|
||||
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Return");
|
||||
|
||||
SalesHeader."Document Type"::Invoice:
|
||||
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Invoice");
|
||||
|
||||
SalesHeader."Document Type"::"Credit Memo":
|
||||
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Cr.Memo");
|
||||
|
||||
END;
|
||||
DATABASE::"Sales Header":
|
||||
BEGIN
|
||||
SalesHeader.COPY(RecordVariant);
|
||||
CASE SalesHeader."Document Type" OF
|
||||
SalesHeader."Document Type"::Quote:
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Quote");
|
||||
SalesHeader."Document Type"::"Blanket Order":
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Blanket");
|
||||
SalesHeader."Document Type"::Order:
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Order");
|
||||
SalesHeader."Document Type"::"Return Order":
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Return");
|
||||
SalesHeader."Document Type"::Invoice:
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Invoice");
|
||||
SalesHeader."Document Type"::"Credit Memo":
|
||||
ReportSelections.SETRANGE(Usage,ReportSelections.Usage::"S.Cr.Memo");
|
||||
END;
|
||||
```
|
||||
|
||||
If the ReportSelections.Usage is simply passed into the function from outside, then the case statement is not needed at all. For example:
|
||||
|
||||
```al
|
||||
PrintDocumentArguments."Report Selection Usage" := SalesHeader.GetReportSelectionUsage;
|
||||
|
||||
DocumentPrint.PrintDocument(SalesHeader,PrintDocumentArguments);
|
||||
```
|
||||
|
||||
Where GetReportSelection usage is coded like this:
|
||||
|
||||
```al
|
||||
CASE SalesHeader."Document Type" OF
|
||||
|
||||
SalesHeader."Document Type"::Quote:
|
||||
|
||||
EXIT (ReportSelections.Usage::"S.Quote");
|
||||
|
||||
SalesHeader."Document Type"::"Blanket Order":
|
||||
|
||||
EXIT(ReportSelections.Usage::"S.Blanket");
|
||||
|
||||
SalesHeader."Document Type"::Order:
|
||||
|
||||
EXIT(ReportSelections.Usage::"S.Order");
|
||||
|
||||
SalesHeader."Document Type"::"Return Order":
|
||||
|
||||
EXIT(ReportSelections.Usage::"S.Return");
|
||||
|
||||
SalesHeader."Document Type"::Invoice:
|
||||
|
||||
EXIT(ReportSelections.Usage::"S.Invoice");
|
||||
|
||||
SalesHeader."Document Type"::"Credit Memo":
|
||||
|
||||
EXIT(ReportSelections.Usage::"S.Cr.Memo");
|
||||
SalesHeader."Document Type"::Quote:
|
||||
EXIT (ReportSelections.Usage::"S.Quote");
|
||||
SalesHeader."Document Type"::"Blanket Order":
|
||||
EXIT(ReportSelections.Usage::"S.Blanket");
|
||||
SalesHeader."Document Type"::Order:
|
||||
EXIT(ReportSelections.Usage::"S.Order");
|
||||
SalesHeader."Document Type"::"Return Order":
|
||||
EXIT(ReportSelections.Usage::"S.Return");
|
||||
SalesHeader."Document Type"::Invoice:
|
||||
EXIT(ReportSelections.Usage::"S.Invoice");
|
||||
SalesHeader."Document Type"::"Credit Memo":
|
||||
EXIT(ReportSelections.Usage::"S.Cr.Memo");
|
||||
```
|
||||
|
||||
This way the code is much more reusable and simpler to read.
|
||||
|
||||
1. Use Rules Table to replace the code with data-driven approach.
|
||||
1. Use Rules Table to replace the code with data-driven approach.
|
||||
|
||||
Setup table would contain the list of the reports and their usages. Based on Table ID and usage it is possible to set filters on the setup table and run the object ID from the result.
|
||||
|
||||
For example:
|
||||
|
||||
```al
|
||||
ReportSelectionSetup.SETRANGE("Table ID", RecordRef.NUMBER);
|
||||
|
||||
ReportSelectionSetup.SETRANGE("Usage Type", RecordRef.FieldValue(ArgumentTable."Usage Type");
|
||||
|
|
@ -239,10 +211,9 @@ ReportSelectionSetup.SETRANGE("Usage Type", RecordRef.FieldValue(ArgumentTable."
|
|||
ReportSelectionSetup.FINDFIRST;
|
||||
|
||||
REPORT.RUN(ReportSelectionSetup."Report ID",VariantRecord);
|
||||
```
|
||||
|
||||
****
|
||||
|
||||
# Example
|
||||
## Example
|
||||
|
||||
The following code illustrates how the Variant Façade pattern can be used to implement the Document-Print Codeunit.
|
||||
|
||||
|
|
@ -258,7 +229,7 @@ When adding the support for new records in most cases it is not need to change a
|
|||
|
||||
Note - PrepareRecord function is placed for the illustrational purposes. An improvement would be to move all of the code from the PrepareRecord function before calling the function. So for the SalesHeader and PurchaseHeaders discounts should be calculated before invoking the function. For passing of the argument it should be one of the fields in the Argument Table, thus the entire specific code would be eliminated.
|
||||
|
||||
# Consequences
|
||||
## Consequences
|
||||
|
||||
* Not needed if the functionality needs to support few tables. Don't use it as a hammer
|
||||
* Strongly typing the records has it benefits since it is easier to find usages, errors will be visible at the compilation time.
|
||||
|
|
@ -266,7 +237,7 @@ Note - PrepareRecord function is placed for the illustrational purposes. An impr
|
|||
* Be careful with filters and marks, if the function needs to support multiple records. Test these cases thoughtfully because with bad placement of code the filters can easily be lost.
|
||||
* Case statements can explode if there are too many tables that require specific processing. Then it is a must to find a way to keep the number of options in the CASE statement low. The Rules Table pattern and adding specifics to the Argument Table before invoking the code could help with keep the list shorter.
|
||||
|
||||
# NAV usages
|
||||
## NAV usages
|
||||
|
||||
* Codeunit 452 - Report Distribution Management
|
||||
* Codeunit 700 - Page Management
|
||||
|
|
@ -278,18 +249,14 @@ Note - PrepareRecord function is placed for the illustrational purposes. An impr
|
|||
* Codeunit 1531 - Workflow Change Rec Mgt.
|
||||
* Codeunit 1535 - Approvals Mgmt.
|
||||
|
||||
# Related Topics
|
||||
## Related Topics
|
||||
|
||||
OO Facade [https://en.wikipedia.org/wiki/Facade\_pattern][anchor8]
|
||||
OO Facade [https://en.wikipedia.org/wiki/Facade_pattern][anchor8]
|
||||
|
||||
Argument Table pattern - [https://community.dynamics.com/nav/w/designpatterns/245.argument-table-pattern][anchor9]
|
||||
|
||||
Rules Table pattern
|
||||
|
||||
< --\[if supportAnnotations\]--\>
|
||||
< --\[endif\]--\>
|
||||
|
||||
|
||||
|
||||
[anchor0]: picture1.png
|
||||
[anchor1]: Picture2.png
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ If the table is empty, it means that either the upgrade has run, or there was no
|
|||
|
||||
**Solution:** if using an upgrade table, always validate that the table contains data before doing anything.
|
||||
|
||||
****
|
||||
|
||||
### Upgrade code, can it be rerun safely?
|
||||
|
||||
**Problem**: if the upgrade code is not written in a way that makes it runnable twice ([idempotent][anchor1]), then several failures can happen, including something as critical as data corruption.
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ Unintentional value changes might propagate. Also, it might lead people to belie
|
|||
|
||||
Bad code
|
||||
|
||||
LOCAL PROCEDURE ShowMessage@15(VAR Text@1000 : Text\[250\]);
|
||||
LOCAL PROCEDURE ShowMessage@15(VAR Text@1000 : Text[250]);
|
||||
BEGIN
|
||||
Text := GetMessageText;
|
||||
IF (Text <\> '') AND GenJnlLineInserted THEN
|
||||
MESSAGE(Text);
|
||||
Text := GetMessageText;
|
||||
IF (Text <> '') AND GenJnlLineInserted THEN
|
||||
MESSAGE(Text);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
LOCAL PROCEDURE ShowMessage@15(Text@1000 : Text\[250\]);
|
||||
LOCAL PROCEDURE ShowMessage@15(Text@1000 : Text[250]);
|
||||
BEGIN
|
||||
Text := GetMessageText;
|
||||
IF (Text <\> '') AND GenJnlLineInserted THEN
|
||||
MESSAGE(Text);
|
||||
Text := GetMessageText;
|
||||
IF (Text <> '') AND GenJnlLineInserted THEN
|
||||
MESSAGE(Text);
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Do not write functions that have high class coupling. This makes the code hard t
|
|||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that has class coupling of \> 30
|
||||
Any procedure / trigger that has class coupling of > 30
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Do not write functions that have high cyclomatic complexity. This makes the code
|
|||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that has a cyclomatic complexity \> 25, using the CC3 version mentioned in [this article][anchor0].
|
||||
Any procedure / trigger that has a cyclomatic complexity > 25, using the CC3 version mentioned in [this article][anchor0].
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,23 +7,23 @@ FINDSET, FIND('+') or FIND('-') should only be used when NEXT is used and vice v
|
|||
Bad code
|
||||
|
||||
IF Cust.FIND('-') THEN
|
||||
ERROR(CustIsBlockErr)
|
||||
ERROR(CustIsBlockErr)
|
||||
|
||||
Good code
|
||||
|
||||
IF Cust.FINDFIRST THEN
|
||||
ERROR(CustIsBlockErr)
|
||||
ERROR(CustIsBlockErr)
|
||||
|
||||
Bad code
|
||||
|
||||
IF Cust.FINDFIRST THEN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL Cust.NEXT = 0;
|
||||
REPEAT
|
||||
...
|
||||
UNTIL Cust.NEXT = 0;
|
||||
|
||||
Good code
|
||||
|
||||
IF Cust.FINDSET THEN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL Cust.NEXT = 0;
|
||||
REPEAT
|
||||
...
|
||||
UNTIL Cust.NEXT = 0;
|
||||
|
|
|
|||
|
|
@ -8,25 +8,25 @@ Bad code
|
|||
|
||||
PROCEDURE SetPurchLine@22(VAR CurrentPurchLine@1000 : Record 39);
|
||||
VAR
|
||||
Pegging@1001 : Boolean;
|
||||
Pegging@1001 : Boolean;
|
||||
BEGIN
|
||||
IF Pegging THEN
|
||||
CurrQuantity := CurrentPurchLine."Quantity (Base)"
|
||||
ELSE
|
||||
CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)";
|
||||
IF Pegging THEN
|
||||
CurrQuantity := CurrentPurchLine."Quantity (Base)"
|
||||
ELSE
|
||||
CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)";
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE SetPurchLine@22(VAR CurrentPurchLine@1000 : Record 39);
|
||||
VAR
|
||||
Pegging@1001 : Boolean;
|
||||
Pegging@1001 : Boolean;
|
||||
BEGIN
|
||||
Pegging := IsPegging(CurrentPurchLine);
|
||||
IF Pegging THEN
|
||||
CurrQuantity := CurrentPurchLine."Quantity (Base)"
|
||||
ELSE
|
||||
CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)";
|
||||
Pegging := IsPegging(CurrentPurchLine);
|
||||
IF Pegging THEN
|
||||
CurrQuantity := CurrentPurchLine."Quantity (Base)"
|
||||
ELSE
|
||||
CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)";
|
||||
END;
|
||||
|
||||
Bad code
|
||||
|
|
@ -34,16 +34,16 @@ Bad code
|
|||
// In the example below, the function will always return FALSE.
|
||||
PROCEDURE GetItemsToPlan@22() : Boolean;
|
||||
BEGIN
|
||||
SETRANGE("Document Type","Document Type"::Order);
|
||||
...
|
||||
FINDSET
|
||||
SETRANGE("Document Type","Document Type"::Order);
|
||||
...
|
||||
FINDSET
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE GetItemsToPlan@22() : Boolean;
|
||||
BEGIN
|
||||
SETRANGE("Document Type","Document Type"::Order);
|
||||
...
|
||||
EXIT(FINDSET)
|
||||
SETRANGE("Document Type","Document Type"::Order);
|
||||
...
|
||||
EXIT(FINDSET)
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ Bad code
|
|||
|
||||
Good code
|
||||
|
||||
Any procedure / trigger that has a maintainability index \>= 20\.
|
||||
Any procedure / trigger that has a maintainability index >= 20\.
|
||||
The Maintainability Index is computed as a function:
|
||||
- Lines Of Code (inverse proportional)
|
||||
- the Halstead Volume
|
||||
- Cyclomatic Complexity (inverse proportional).
|
||||
- Lines Of Code (inverse proportional)
|
||||
- the Halstead Volume
|
||||
- Cyclomatic Complexity (inverse proportional).
|
||||
|
||||
More info
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ More info
|
|||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that is \> 100 lines of code
|
||||
Any procedure / trigger that is > 100 lines of code
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ weight = 920
|
|||
+++
|
||||
The number of parameters passed to a string must match the placeholders.
|
||||
|
||||
****
|
||||
|
||||
Bad code
|
||||
|
||||
CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
|
|
@ -18,7 +16,7 @@ Good code
|
|||
...
|
||||
ERROR(CannotDeleteLineErr);
|
||||
|
||||
###
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ Bad code
|
|||
|
||||
LOCAL PROCEDURE Code@1();
|
||||
VAR
|
||||
CAJnlPostBatch@1001 : Codeunit 1103;
|
||||
CAJnlPostBatch@1001 : Codeunit 1103;
|
||||
BEGIN
|
||||
CAJnlPostBatch.Run(CostJnlLine);
|
||||
CAJnlPostBatch.Run(CostJnlLine);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
LOCAL PROCEDURE Code@1();
|
||||
BEGIN
|
||||
CODEUNIT.RUN(CODEUNIT::"CA Jnl.-Post Batch",CostJnlLine);
|
||||
CODEUNIT.RUN(CODEUNIT::"CA Jnl.-Post Batch",CostJnlLine);
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ It affects code readability and can lead to wrong assumptions.
|
|||
|
||||
Bad code
|
||||
|
||||
IF Type <\> Type::FIELD THEN BEGIN
|
||||
...
|
||||
ERROR(...);
|
||||
RecRef.CLOSE;
|
||||
IF Type <> Type::FIELD THEN BEGIN
|
||||
...
|
||||
ERROR(...);
|
||||
RecRef.CLOSE;
|
||||
END;
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
IF Type <\> Type::FIELD THEN BEGIN
|
||||
...
|
||||
RecRef.CLOSE;
|
||||
ERROR(...);
|
||||
IF Type <> Type::FIELD THEN BEGIN
|
||||
...
|
||||
RecRef.CLOSE;
|
||||
ERROR(...);
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -6,33 +6,33 @@ The value assigned to a variable must be used. Else the variable is not necessar
|
|||
|
||||
Bad code
|
||||
|
||||
PROCEDURE AddEntities@1(FilterStr@1000 : Text\[250\]);
|
||||
PROCEDURE AddEntities@1(FilterStr@1000 : Text[250]);
|
||||
VAR
|
||||
Vendor@1001 : Record 23;
|
||||
Count@1002 : Integer;
|
||||
Vendor@1001 : Record 23;
|
||||
Count@1002 : Integer;
|
||||
BEGIN
|
||||
Count := 0;
|
||||
Vendor.SETFILTER("No.",FilterStr);
|
||||
IF Vendor.FINDSET THEN
|
||||
REPEAT
|
||||
"User ID" := USERID;
|
||||
"Vendor No." := Vendor."No.";
|
||||
IF INSERT THEN
|
||||
Count += 1;
|
||||
UNTIL Vendor.NEXT = 0;
|
||||
Count := 0;
|
||||
Vendor.SETFILTER("No.",FilterStr);
|
||||
IF Vendor.FINDSET THEN
|
||||
REPEAT
|
||||
"User ID" := USERID;
|
||||
"Vendor No." := Vendor."No.";
|
||||
IF INSERT THEN
|
||||
Count += 1;
|
||||
UNTIL Vendor.NEXT = 0;
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE AddEntities@1(FilterStr@1000 : Text\[250\]);
|
||||
PROCEDURE AddEntities@1(FilterStr@1000 : Text[250]);
|
||||
VAR
|
||||
Vendor@1001 : Record 23;
|
||||
Vendor@1001 : Record 23;
|
||||
BEGIN
|
||||
Vendor.SETFILTER("No.",FilterStr);
|
||||
IF Vendor.FINDSET THEN
|
||||
REPEAT
|
||||
"User ID" := USERID;
|
||||
"Vendor No." := Vendor."No.";
|
||||
IF INSERT THEN;
|
||||
UNTIL Vendor.NEXT = 0;
|
||||
Vendor.SETFILTER("No.",FilterStr);
|
||||
IF Vendor.FINDSET THEN
|
||||
REPEAT
|
||||
"User ID" := USERID;
|
||||
"Vendor No." := Vendor."No.";
|
||||
IF INSERT THEN;
|
||||
UNTIL Vendor.NEXT = 0;
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -8,37 +8,37 @@ Unused variables affect readability.
|
|||
|
||||
Bad code
|
||||
|
||||
PROCEDURE CheckPostingDate@23(CaptionEntryNo@1005 : Text\[50\]);
|
||||
PROCEDURE CheckPostingDate@23(CaptionEntryNo@1005 : Text[50]);
|
||||
BEGIN
|
||||
IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN
|
||||
ERROR(DateNotAllowedErr,Caption,EntryNo)
|
||||
IF PostingDate \> MaxPostingDate THEN
|
||||
MaxPostingDate := PostingDate;
|
||||
IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN
|
||||
ERROR(DateNotAllowedErr,Caption,EntryNo)
|
||||
IF PostingDate > MaxPostingDate THEN
|
||||
MaxPostingDate := PostingDate;
|
||||
END
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE CheckPostingDate@23();
|
||||
BEGIN
|
||||
IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN
|
||||
ERROR(DateNotAllowedErr,Caption,EntryNo);
|
||||
IF PostingDate \> MaxPostingDate THEN
|
||||
MaxPostingDate := PostingDate;
|
||||
IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN
|
||||
ERROR(DateNotAllowedErr,Caption,EntryNo);
|
||||
IF PostingDate > MaxPostingDate THEN
|
||||
MaxPostingDate := PostingDate;
|
||||
END;
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE IsReturned@14(EntryNo@1002 : Integer) : Decimal;
|
||||
VAR
|
||||
ItemEntry@1000 : Record 32;
|
||||
Quantity@1003 : Integer;
|
||||
ItemEntry@1000 : Record 32;
|
||||
Quantity@1003 : Integer;
|
||||
BEGIN
|
||||
EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE));
|
||||
EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE));
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE IsReturned@14(EntryNo@1002 : Integer) : Decimal;
|
||||
BEGIN
|
||||
EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE));
|
||||
EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE));
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -8,52 +8,52 @@ It will throw an error at runtime.
|
|||
|
||||
Bad code
|
||||
|
||||
FileName@1010 : Text\[250\];
|
||||
FileName@1010 : Text[250];
|
||||
...
|
||||
UploadedFileName@1016 : Text\[1024\];
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := UploadedFileName;
|
||||
|
||||
Good code
|
||||
|
||||
FileName@1010 : Text\[1024\];
|
||||
FileName@1010 : Text[1024];
|
||||
...
|
||||
UploadedFileName@1016 : Text\[1024\];
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := UploadedFileName;
|
||||
|
||||
Bad code
|
||||
|
||||
FileName@1010 : Text\[250\];
|
||||
FileName@1010 : Text[250];
|
||||
...
|
||||
UploadedFileName@1016 : Text\[1024\];
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := UploadedFileName;
|
||||
|
||||
Good code
|
||||
|
||||
FileName@1010 : Text\[250\];
|
||||
FileName@1010 : Text[250];
|
||||
...
|
||||
UploadedFileName@1016 : Text\[1024\];
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := COPYSTR(UploadedFileName,1,250); // In case only the first 250 chars are needed. Similar for fields
|
||||
|
||||
Bad code
|
||||
|
||||
VAR
|
||||
ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests'
|
||||
Subject@1002 : Text\[50\];
|
||||
...
|
||||
ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests'
|
||||
Subject@1002 : Text[50];
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
Subject := ExceededNumberTxt;
|
||||
...
|
||||
Subject := ExceededNumberTxt;
|
||||
|
||||
Good code
|
||||
|
||||
VAR
|
||||
ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests'
|
||||
Subject@1002 : Text\[100\];
|
||||
...
|
||||
ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests'
|
||||
Subject@1002 : Text[100];
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
Subject := ExceededNumberTxt';
|
||||
...
|
||||
Subject := ExceededNumberTxt';
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ weight = 1450
|
|||
+++
|
||||
Do not use the WITH scope when it has a variable whose name is the same as a local variable. This can lead to wrong code assumptions.
|
||||
|
||||
**Given that** "Contract Type" is a field on table ServiceContractHeader, then in the following example there is a parameter name clash with the field name. Which one will be used?
|
||||
**Given that**
|
||||
"Contract Type" is a field on table ServiceContractHeader, then in the following example there is a parameter name clash with the field name. Which one will be used?
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE InsertData@1("Contract Type"@1000 : Option...);
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
WITH ServiceContractHeader DO BEGIN
|
||||
...
|
||||
DimMgt.InsertServContractDim(...,"Contract Type","Contract No.",0,...);
|
||||
...
|
||||
WITH ServiceContractHeader DO BEGIN
|
||||
...
|
||||
DimMgt.InsertServContractDim(...,"Contract Type","Contract No.",0,...);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
|
@ -22,8 +23,8 @@ Good code
|
|||
PROCEDURE InsertData@1(ContractType@1000 : Option...);
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
WITH ServiceContractHeader DO BEGIN
|
||||
...
|
||||
DimMgt.InsertServContractDim(...,ContractType,"Contract No.",0,...);
|
||||
...
|
||||
WITH ServiceContractHeader DO BEGIN
|
||||
...
|
||||
DimMgt.InsertServContractDim(...,ContractType,"Contract No.",0,...);
|
||||
END;
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
title = "Using Calcdate"
|
||||
weight = 1370
|
||||
+++
|
||||
CALCDATE should only be used with DateFormula variables. Alternatively the string should be enclosed using the <\> symbols.
|
||||
CALCDATE should only be used with DateFormula variables. Alternatively the string should be enclosed using the <> symbols.
|
||||
|
||||
Bad code
|
||||
|
||||
IF ReservEntry."Expected Receipt Date" \>
|
||||
CALCDATE('-' + FORMAT("Dampener (Time)") + FirstDate)
|
||||
IF ReservEntry."Expected Receipt Date" >
|
||||
CALCDATE('-' + FORMAT("Dampener (Time)") + FirstDate)
|
||||
THEN
|
||||
|
||||
Good code
|
||||
|
||||
IF ReservEntry."Expected Receipt Date" \>
|
||||
CALCDATE('<-' + FORMAT("Dampener (Time)") + FirstDate + '\>')
|
||||
IF ReservEntry."Expected Receipt Date" >
|
||||
CALCDATE('<-' + FORMAT("Dampener (Time)") + FirstDate + '>')
|
||||
THEN
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ Reason:
|
|||
|
||||
Bad code
|
||||
|
||||
IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDNAME("Location Code"),...)
|
||||
```al
|
||||
IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDNAME("Location Code"),...)
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDCAPTION("Location Code"),...)
|
||||
```al
|
||||
IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDCAPTION("Location Code"),...)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ Declare Text Constant as global variables.
|
|||
|
||||
Bad code
|
||||
|
||||
PROCEDURE GetRequirementText@6(...) : Text\[50\];
|
||||
VAR
|
||||
```al
|
||||
PROCEDURE GetRequirementText@6(...) : Text[50];
|
||||
VAR
|
||||
RequirementOptionsTxt@1002 : TextConst 'ENU=Shipment,Receive,Pick,Put-Away';
|
||||
BEGIN
|
||||
BEGIN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
VAR
|
||||
```al
|
||||
VAR
|
||||
RequirementOptionsTxt@1002 : TextConst 'ENU=Shipment,Receive,Pick,Put-Away';
|
||||
...
|
||||
PROCEDURE GetRequirementText@6(...) : Text\[50\];
|
||||
BEGIN
|
||||
...
|
||||
PROCEDURE GetRequirementText@6(...) : Text[50];
|
||||
BEGIN
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,32 +6,40 @@ Pass user messages using Text Constants. It makes translation easy.
|
|||
|
||||
Bad code
|
||||
|
||||
ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment "';
|
||||
...
|
||||
IF CONFIRM(ImportAttachmentQst + Caption +'?',TRUE) THEN BEGIN
|
||||
```al
|
||||
ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment "';
|
||||
...
|
||||
IF CONFIRM(ImportAttachmentQst + Caption +'?',TRUE) THEN BEGIN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment %1?"';
|
||||
...
|
||||
IF CONFIRM(STRSUBSTNO(ImportAttachmentQst, Caption),TRUE) THEN BEGIN
|
||||
```al
|
||||
ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment %1?"';
|
||||
...
|
||||
IF CONFIRM(STRSUBSTNO(ImportAttachmentQst, Caption),TRUE) THEN BEGIN
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
...
|
||||
```al
|
||||
...
|
||||
IF NOT
|
||||
CONFIRM(
|
||||
STRSUBSTNO(
|
||||
'Difference on Periodic entries: %1 on %2' +
|
||||
'Do you want to continue?',Balance,Date),
|
||||
TRUE)
|
||||
CONFIRM(
|
||||
STRSUBSTNO(
|
||||
'Difference on Periodic entries: %1 on %2' +
|
||||
'Do you want to continue?',Balance,Date),
|
||||
TRUE)
|
||||
THEN
|
||||
ERROR('Program terminated by the user');
|
||||
ERROR('Program terminated by the user');
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
DiffOnPeriodEntiesQst@100 : TextConst 'ENU="Difference on Periodic entries: %1 on %2\\ Do you want to continue?"';
|
||||
ProgramTerminatedErr@200 : TextConst 'ENU="Program terminated by the user"';
|
||||
...
|
||||
```al
|
||||
DiffOnPeriodEntiesQst@100 : TextConst 'ENU="Difference on Periodic entries: %1 on %2\\ Do you want to continue?"';
|
||||
ProgramTerminatedErr@200 : TextConst 'ENU="Program terminated by the user"';
|
||||
...
|
||||
IF NOT CONFIRM(STRSUBSTNO(DiffOnPeriodEntiesQst,Balance,Date),TRUE) THEN
|
||||
ERROR(ProgramTerminatedErr);
|
||||
ERROR(ProgramTerminatedErr);
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@
|
|||
title = "Begin as an 'After Word'"
|
||||
weight = 230
|
||||
+++
|
||||
When BEGIN follows THEN, ELSE, DO, it should be on the same line, preceded by one space character. Bad code
|
||||
When BEGIN follows THEN, ELSE, DO, it should be on the same line, preceded by one space character.
|
||||
|
||||
IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN
|
||||
BEGIN
|
||||
...
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN BEGIN
|
||||
```
|
||||
IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN BEGIN
|
||||
...
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,52 +3,67 @@ title = "Begin-End - Compound Only"
|
|||
weight = 240
|
||||
+++
|
||||
Only use BEGIN..END to enclose compound statements.
|
||||
|
||||
Bad code
|
||||
|
||||
IF FINDSET THEN BEGIN
|
||||
```al
|
||||
IF FINDSET THEN BEGIN
|
||||
REPEAT
|
||||
...
|
||||
...
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF FINDSET THEN
|
||||
```al
|
||||
IF FINDSET THEN
|
||||
REPEAT
|
||||
...
|
||||
...
|
||||
UNTIL NEXT = 0;
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
IF IsAssemblyOutputLine THEN BEGIN
|
||||
```al
|
||||
IF IsAssemblyOutputLine THEN BEGIN
|
||||
TESTFIELD("Order Line No.",0);
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF IsAssemblyOutputLine THEN
|
||||
```al
|
||||
IF IsAssemblyOutputLine THEN
|
||||
TESTFIELD("Order Line No.",0);
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
IF FINDSET THEN
|
||||
```al
|
||||
IF FINDSET THEN
|
||||
REPEAT
|
||||
BEGIN
|
||||
...
|
||||
END;
|
||||
BEGIN
|
||||
...
|
||||
END;
|
||||
UNTIL NEXT = 0;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF FINDSET THEN
|
||||
```al
|
||||
IF FINDSET THEN
|
||||
REPEAT
|
||||
...
|
||||
...
|
||||
UNTIL NEXT = 0;
|
||||
```
|
||||
|
||||
Exception
|
||||
|
||||
// Except for this case
|
||||
IF X THEN BEGIN
|
||||
```al
|
||||
// Except for this case
|
||||
IF X THEN BEGIN
|
||||
IF Y THEN
|
||||
DO SOMETHING;
|
||||
END ELSE (not X)
|
||||
DO SOMETHING;
|
||||
END ELSE (not X)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,14 +2,20 @@
|
|||
title = "Binary Operator to Start Line"
|
||||
weight = 250
|
||||
+++
|
||||
Do not start a line with a binary operator. Bad code
|
||||
Do not start a line with a binary operator.
|
||||
|
||||
"Quantity to Ship" :=
|
||||
Bad code
|
||||
|
||||
```al
|
||||
"Quantity to Ship" :=
|
||||
Quantity
|
||||
- "Quantity Shipped"
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
"Quantity to Ship" :=
|
||||
```al
|
||||
"Quantity to Ship" :=
|
||||
Quantity -
|
||||
"Quantity Shipped"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,30 +2,43 @@
|
|||
title = "Blank Lines"
|
||||
weight = 260
|
||||
+++
|
||||
Do not use blank lines at the beginning or end of any functions, after BEGIN, before END, or inside multiline expressions. Bad code
|
||||
|
||||
PROCEDURE MATRIX\_OnDrillDown@1133(MATRIX\_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
SetupDrillDownCol(MATRIX\_ColumnOrdinal);
|
||||
DrillDown(FALSE,ValueType);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE MATRIX\_OnDrillDown@1133(MATRIX\_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
SetupDrillDownCol(MATRIX\_ColumnOrdinal);
|
||||
DrillDown(FALSE,ValueType);
|
||||
END;
|
||||
Do not use blank lines at the beginning or end of any functions, after BEGIN, before END, or inside multiline expressions.
|
||||
|
||||
Bad code
|
||||
|
||||
IF NameIsValid AND
|
||||
Name2IsValid
|
||||
THEN
|
||||
```al
|
||||
PROCEDURE MATRIX_OnDrillDown@1133(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
|
||||
SetupDrillDownCol(MATRIX_ColumnOrdinal);
|
||||
DrillDown(FALSE,ValueType);
|
||||
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF NameIsValid AND
|
||||
```al
|
||||
PROCEDURE MATRIX_OnDrillDown@1133(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
SetupDrillDownCol(MATRIX_ColumnOrdinal);
|
||||
DrillDown(FALSE,ValueType);
|
||||
END;
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF NameIsValid AND
|
||||
|
||||
Name2IsValid
|
||||
THEN
|
||||
THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF NameIsValid AND
|
||||
Name2IsValid
|
||||
THEN
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@
|
|||
title = "CASE Action"
|
||||
weight = 310
|
||||
+++
|
||||
A CASE action should start on a line after the possibility. Bad code
|
||||
A CASE action should start on a line after the possibility.
|
||||
|
||||
CASE Letter OF
|
||||
Bad code
|
||||
|
||||
```al
|
||||
CASE Letter OF
|
||||
'A': Letter2 := '10';
|
||||
'B': Letter2 := '11';
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
CASE Letter OF
|
||||
```al
|
||||
CASE Letter OF
|
||||
'A':
|
||||
Letter2 := '10';
|
||||
Letter2 := '10';
|
||||
'B':
|
||||
Letter2 := '11';
|
||||
Letter2 := '11';
|
||||
```
|
||||
|
|
@ -2,14 +2,20 @@
|
|||
title = "Colon usage in CASE"
|
||||
weight = 340
|
||||
+++
|
||||
The last possibility on a CASE statement must be immediately followed by a colon. Bad code
|
||||
The last possibility on a CASE statement must be immediately followed by a colon.
|
||||
|
||||
CASE DimOption OF
|
||||
Bad code
|
||||
|
||||
```al
|
||||
CASE DimOption OF
|
||||
DimOption::"Global Dimension 1" :
|
||||
DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code";
|
||||
DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code";
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
CASE DimOption OF
|
||||
```al
|
||||
CASE DimOption OF
|
||||
DimOption::"Global Dimension 1":
|
||||
DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code";
|
||||
DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code";
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,38 +2,45 @@
|
|||
title = "Comments inside Curly Brackets"
|
||||
weight = 350
|
||||
+++
|
||||
Never use curly bracket comments. During development, the "Block comment" functionality can be used instead. However, in production code, block comments are not recommended. Bad code
|
||||
Never use curly bracket comments. During development, the "Block comment" functionality can be used instead. However, in production code, block comments are not recommended.
|
||||
|
||||
PeriodTxt: {Period}
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
PeriodTxt: {Period}
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
PeriodTxt: // Period
|
||||
|
||||
```al
|
||||
PeriodTxt: // Period
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE MATRIX\_OnAfterGetRecord@10(MATRIX\_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
```al
|
||||
PROCEDURE MATRIX_OnAfterGetRecord@10(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
{
|
||||
IF ShowColumnName THEN
|
||||
MatrixHeader := MatrixRecords\[MATRIX\_ColumnOrdinal\].Name
|
||||
MatrixHeader := MatrixRecords[MATRIX_ColumnOrdinal].Name
|
||||
ELSE
|
||||
MatrixHeader := MatrixRecords\[MATRIX\_ColumnOrdinal\].Code;
|
||||
MatrixHeader := MatrixRecords[MATRIX_ColumnOrdinal].Code;
|
||||
}
|
||||
MatrixRecord := MatrixRecords\[MATRIX\_ColumnOrdinal\];
|
||||
MatrixRecord := MatrixRecords[MATRIX_ColumnOrdinal];
|
||||
AnalysisValue := CalcAmt(ValueType,TRUE);
|
||||
MATRIX\_CellData\[MATRIX\_ColumnOrdinal\] := AnalysisValue;
|
||||
END;
|
||||
MATRIX_CellData[MATRIX_ColumnOrdinal] := AnalysisValue;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE MATRIX\_OnAfterGetRecord@10(MATRIX\_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
MatrixRecord := MatrixRecords\[MATRIX\_ColumnOrdinal\];
|
||||
```al
|
||||
PROCEDURE MATRIX_OnAfterGetRecord@10(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
MatrixRecord := MatrixRecords[MATRIX_ColumnOrdinal];
|
||||
AnalysisValue := CalcAmt(ValueType,TRUE);
|
||||
MATRIX\_CellData\[MATRIX\_ColumnOrdinal\] := AnalysisValue;
|
||||
END;
|
||||
MATRIX_CellData[MATRIX_ColumnOrdinal] := AnalysisValue;
|
||||
END;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ title = "Comment Spacing"
|
|||
weight = 360
|
||||
+++
|
||||
Always start comments with // followed by one space character.
|
||||
|
||||
Bad code
|
||||
|
||||
RowNo += 1000; //Move way below the budget
|
||||
|
||||
```al
|
||||
RowNo += 1000; //Move way below the budget
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
RowNo += 1000; // Move way below the budget
|
||||
```al
|
||||
RowNo += 1000; // Move way below the budget
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,19 +3,24 @@ title = "END ELSE Pair"
|
|||
weight = 540
|
||||
+++
|
||||
The END ELSE pair should always appear on the same line.
|
||||
|
||||
Bad code
|
||||
|
||||
IF OppEntry.FIND('-') THEN
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN
|
||||
IF SalesCycleStage.FIND('-') THEN BEGIN
|
||||
...
|
||||
...
|
||||
END
|
||||
ELSE
|
||||
ELSE
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF OppEntry.FIND('-') THEN
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN
|
||||
IF SalesCycleStage.FIND('-') THEN BEGIN
|
||||
...
|
||||
END ELSE
|
||||
...
|
||||
END ELSE
|
||||
...
|
||||
```
|
||||
|
|
@ -3,82 +3,110 @@ title = "Indentation"
|
|||
weight = 650
|
||||
+++
|
||||
In general, use an indentation of two space characters. Logical expressions in the IF, WHILE, and UNTIL parts are indented at least 3, 6, and 6 spaces respectively.
|
||||
|
||||
Bad code
|
||||
|
||||
IF GLSetup."Unrealized VAT" OR
|
||||
(GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment)
|
||||
|
||||
Good code
|
||||
|
||||
IF GLSetup."Unrealized VAT" OR
|
||||
```al
|
||||
IF GLSetup."Unrealized VAT" OR
|
||||
(GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment)
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF GLSetup."Unrealized VAT" OR
|
||||
(GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment)
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
IF GenJnlLine."Account No." <\> ICPartner.Code THEN
|
||||
ICPartner.GET("Account No.");
|
||||
IF GenJnlLine.Amount \> 0 THEN BEGIN
|
||||
```al
|
||||
IF GenJnlLine."Account No." <> ICPartner.Code THEN
|
||||
ICPartner.GET("Account No.");
|
||||
IF GenJnlLine.Amount \> 0 THEN BEGIN
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF GenJnlLine."Account No." <\> ICPartner.Code THEN
|
||||
ICPartner.GET("Account No.");
|
||||
IF GenJnlLine.Amount \> 0 THEN BEGIN
|
||||
...
|
||||
```al
|
||||
IF GenJnlLine."Account No." <> ICPartner.Code THEN
|
||||
ICPartner.GET("Account No.");
|
||||
IF GenJnlLine.Amount > 0 THEN BEGIN
|
||||
...
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
Dialog.OPEN(WindowTxt +
|
||||
'@1@@@@@@@@@@@@@@@@@@@@@@@');
|
||||
```al
|
||||
Dialog.OPEN(WindowTxt +
|
||||
'@1@@@@@@@@@@@@@@@@@@@@@@@');
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
Dialog.OPEN(
|
||||
```al
|
||||
Dialog.OPEN(
|
||||
WindowTxt +
|
||||
'@1@@@@@@@@@@@@@@@@@@@@@@@');
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
TempOldCustLedgEntry.DELETE;
|
||||
// Find the next old entry for application of the new entry
|
||||
```al
|
||||
TempOldCustLedgEntry.DELETE;
|
||||
// Find the next old entry for application of the new entry
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
TempOldCustLedgEntry.DELETE;
|
||||
// Find the next old entry for application of the new entry
|
||||
|
||||
```al
|
||||
TempOldCustLedgEntry.DELETE;
|
||||
// Find the next old entry for application of the new entry
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
IF NOT ("Applies-to Doc. Type" IN
|
||||
\["Applies-to Doc. Type"::Receipt,
|
||||
"Applies-to Doc. Type"::"Return Shipment"\])
|
||||
```al
|
||||
IF NOT ("Applies-to Doc. Type" IN
|
||||
["Applies-to Doc. Type"::Receipt,
|
||||
"Applies-to Doc. Type"::"Return Shipment"])
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF NOT ("Applies-to Doc. Type" IN
|
||||
\["Applies-to Doc. Type"::Receipt,
|
||||
"Applies-to Doc. Type"::"Return Shipment"\])
|
||||
```al
|
||||
IF NOT ("Applies-to Doc. Type" IN
|
||||
["Applies-to Doc. Type"::Receipt,
|
||||
"Applies-to Doc. Type"::"Return Shipment"])
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
WHILE (RemAmt \> 0) OR
|
||||
(RemAmtLCY \> 0)
|
||||
DO
|
||||
```al
|
||||
WHILE (RemAmt > 0) OR
|
||||
(RemAmtLCY > 0)
|
||||
DO
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
WHILE (RemAmt \> 0) OR
|
||||
(RemAmtLCY \> 0)
|
||||
DO
|
||||
```al
|
||||
WHILE (RemAmt > 0) OR
|
||||
(RemAmtLCY > 0)
|
||||
DO
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
UNTIL (RemAmt \> 0) AND
|
||||
(RemAmtLCY \> 0);
|
||||
```al
|
||||
UNTIL (RemAmt > 0) AND
|
||||
(RemAmtLCY > 0);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
UNTIL (RemAmt \> 0) AND
|
||||
(RemAmtLCY \> 0)
|
||||
```al
|
||||
UNTIL (RemAmt > 0) AND
|
||||
(RemAmtLCY > 0)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,13 +2,19 @@
|
|||
title = "Keyword Pairs - Indentation"
|
||||
weight = 730
|
||||
+++
|
||||
The IF..THEN pair, WHILE..DO pair, and FOR..DO pair must appear on the same line or the same level of indentation. Bad code
|
||||
The IF..THEN pair, WHILE..DO pair, and FOR..DO pair must appear on the same line or the same level of indentation.
|
||||
|
||||
IF (x = y) AND
|
||||
(a = b) THEN
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF (x = y) AND
|
||||
(a = b) THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF (x = y) AND
|
||||
(a = b)
|
||||
THEN
|
||||
```al
|
||||
IF (x = y) AND
|
||||
(a = b)
|
||||
THEN
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,19 +2,25 @@
|
|||
title = "Line Start Keywords"
|
||||
weight = 740
|
||||
+++
|
||||
The END, IF, REPEAT, FOR, WHILE, ELSE and CASE statement should always start a line. Bad code
|
||||
The END, IF, REPEAT, FOR, WHILE, ELSE and CASE statement should always start a line.
|
||||
|
||||
IF IsContactName THEN ValidateContactName
|
||||
ELSE IF IsSalespersonCode THEN ValidateSalespersonCode
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF IsContactName THEN ValidateContactName
|
||||
ELSE IF IsSalespersonCode THEN ValidateSalespersonCode
|
||||
ELSE IF IsSalesCycleCode THEN ValidatSalesCycleCode;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF IsContactName THEN
|
||||
ValidateContactName
|
||||
ELSE
|
||||
IF IsSalespersonCode THEN
|
||||
```al
|
||||
IF IsContactName THEN
|
||||
ValidateContactName
|
||||
ELSE
|
||||
IF IsSalespersonCode THEN
|
||||
ValidateSalespersonCode
|
||||
ELSE
|
||||
ELSE
|
||||
IF IsSalesCycleCode THEN
|
||||
ValidatSalesCycleCode;
|
||||
ValidatSalesCycleCode;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@ title = "Lonely Repeat"
|
|||
weight = 760
|
||||
+++
|
||||
The REPEAT statement should always be alone on a line.
|
||||
|
||||
Bad code
|
||||
|
||||
IF ReservEntry.FINDSET THEN REPEAT
|
||||
|
||||
```al
|
||||
IF ReservEntry.FINDSET THEN REPEAT
|
||||
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
IF ReservEntry.FINDSET THEN
|
||||
REPEAT
|
||||
```al
|
||||
IF ReservEntry.FINDSET THEN
|
||||
REPEAT
|
||||
```
|
||||
|
|
@ -3,12 +3,16 @@ title = "Named Invocations"
|
|||
weight = 830
|
||||
+++
|
||||
When calling an object statically use the name, not the number
|
||||
|
||||
Bad code
|
||||
|
||||
PAGE.RUNMODAL(525,SalesShptLine)
|
||||
|
||||
```al
|
||||
PAGE.RUNMODAL(525,SalesShptLine)
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
PAGE.RUNMODAL(PAGE::"Posted Sales Shipment Lines",SalesShptLine)
|
||||
```al
|
||||
PAGE.RUNMODAL(PAGE::"Posted Sales Shipment Lines",SalesShptLine)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,19 +3,24 @@ title = "Nested WITHs"
|
|||
weight = 850
|
||||
+++
|
||||
Do not nest WITHs that reference different types of objects.
|
||||
|
||||
Bad code
|
||||
|
||||
WITH PostedWhseShptLine DO BEGIN
|
||||
...
|
||||
WITH ItemLedgEntry DO
|
||||
```al
|
||||
WITH PostedWhseShptLine DO BEGIN
|
||||
...
|
||||
WITH ItemLedgEntry DO
|
||||
InsertBufferRec(...,"Serial No.","Lot No.",...);
|
||||
...
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
WITH PostedWhseShptLine DO BEGIN
|
||||
...
|
||||
InsertBufferRec(...,ItemLedgEntry."Serial No.",ItemLedgEntry."Lot No.",...);
|
||||
...
|
||||
END;
|
||||
```al
|
||||
WITH PostedWhseShptLine DO BEGIN
|
||||
...
|
||||
InsertBufferRec(...,ItemLedgEntry."Serial No.",ItemLedgEntry."Lot No.",...);
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,26 +3,34 @@ title = "One Statement Per Line"
|
|||
weight = 910
|
||||
+++
|
||||
A line of code should not have more than one statement.
|
||||
|
||||
Bad code
|
||||
|
||||
IF OppEntry.FIND('-') THEN EXIT
|
||||
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN EXIT
|
||||
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
IF OppEntry.FIND('-') THEN
|
||||
EXIT
|
||||
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN
|
||||
EXIT
|
||||
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
TotalCost += Cost; TotalAmt += Amt;
|
||||
|
||||
|
||||
```al
|
||||
TotalCost += Cost; TotalAmt += Amt;
|
||||
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
TotalCost += Cost;
|
||||
TotalAmt += Amt;
|
||||
```al
|
||||
TotalCost += Cost;
|
||||
TotalAmt += Amt;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@
|
|||
title = "Separate IF and ELSE"
|
||||
weight = 1050
|
||||
+++
|
||||
IF and ELSE statements should be on separate lines. Bad code
|
||||
IF and ELSE statements should be on separate lines.
|
||||
|
||||
IF Atom\[i+1\] = '\>' THEN HasLogicalOperator := TRUE ELSE BEGIN
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF Atom[i+1] = '>' THEN HasLogicalOperator := TRUE ELSE BEGIN
|
||||
...
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IF Atom\[i+1\] = '\>' THEN
|
||||
```al
|
||||
IF Atom[i+1] = '>' THEN
|
||||
HasLogicalOperator := TRUE
|
||||
ELSE BEGIN
|
||||
ELSE BEGIN
|
||||
...
|
||||
END;
|
||||
END;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,36 +3,44 @@ title = "Spacing Binary Operators"
|
|||
weight = 1120
|
||||
+++
|
||||
There must be exactly one space character on each side of a binary operator such as = + - AND OR =. The parameter comma operator however, should have no spaces.
|
||||
|
||||
Bad code
|
||||
|
||||
"Line Discount %" := "Line Discount Amount"/"Line Value"\*100
|
||||
|
||||
```al
|
||||
"Line Discount %" := "Line Discount Amount"/"Line Value"*100
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
"Line Discount %" := "Line Discount Amount" / "Line Value" \* 100;
|
||||
|
||||
```al
|
||||
"Line Discount %" := "Line Discount Amount" / "Line Value" * 100;
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
StartDate := CALCDATE('<+'+FORMAT(Days + i)+'D\>', StartDate);
|
||||
|
||||
```al
|
||||
StartDate := CALCDATE('<+'+FORMAT(Days + i)+'D>', StartDate);
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
StartDate := CALCDATE('<+' + FORMAT(Days + i) + 'D\>',StartDate);
|
||||
|
||||
```al
|
||||
StartDate := CALCDATE('<+' + FORMAT(Days + i) + 'D>',StartDate);
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
StartDate := 0D; // Initialize
|
||||
|
||||
```al
|
||||
StartDate := 0D; // Initialize
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
StartDate := 0D; // Initialize
|
||||
```al
|
||||
StartDate := 0D; // Initialize
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,37 +2,45 @@
|
|||
title = "Spacing Brackets and ::"
|
||||
weight = 1130
|
||||
+++
|
||||
There must be no spaces characters before and after \[\] dimension brackets symbols or :: option symbols.
|
||||
There must be no spaces characters before and after [] dimension brackets symbols or :: option symbols.
|
||||
|
||||
Bad code
|
||||
|
||||
A\[i\] \[j\] := Amt;
|
||||
|
||||
```al
|
||||
A[i] [j] := Amt;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
A\[i\]\[j\] := Amt;
|
||||
|
||||
```al
|
||||
A[i][j] := Amt;
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
"Currency Exchange Rate"."Fix Exchange Rate Amount" :: Currency:
|
||||
|
||||
```al
|
||||
"Currency Exchange Rate"."Fix Exchange Rate Amount" :: Currency:
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
"Currency Exchange Rate"."Fix Exchange Rate Amount"::Currency:
|
||||
|
||||
```al
|
||||
"Currency Exchange Rate"."Fix Exchange Rate Amount"::Currency:
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
IF FIND (Which) THEN
|
||||
|
||||
```al
|
||||
IF FIND (Which) THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
IF FIND(Which) THEN
|
||||
```al
|
||||
IF FIND(Which) THEN
|
||||
```
|
||||
|
|
@ -3,24 +3,30 @@ title = "Spacing Unary Operators"
|
|||
weight = 1140
|
||||
+++
|
||||
There must be no space between a unary operator and its argument (except for the NOT keyword).
|
||||
|
||||
Bad code
|
||||
|
||||
IF NOT(Type = Type::Item) THEN
|
||||
|
||||
```al
|
||||
IF NOT(Type = Type::Item) THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
IF NOT (Type = Type::Item) THEN
|
||||
|
||||
```al
|
||||
IF NOT (Type = Type::Item) THEN
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
DiscAmt := - "Discount Amount";
|
||||
|
||||
```al
|
||||
DiscAmt := - "Discount Amount";
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
DiscAmt := -"Discount Amount";
|
||||
```al
|
||||
DiscAmt := -"Discount Amount";
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,23 +2,30 @@
|
|||
title = "Temporary Variable Naming"
|
||||
weight = 1200
|
||||
+++
|
||||
The name of a temporary variable must be prefixed with the word Temp and not otherwise. Bad code
|
||||
|
||||
JobWIPBuffer@1002 : TEMPORARY Record 1018;
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
TempJobWIPBuffer@1002 : TEMPORARY Record 1018;
|
||||
|
||||
The name of a temporary variable must be prefixed with the word Temp and not otherwise.
|
||||
|
||||
Bad code
|
||||
|
||||
TempJobWIPBuffer@1002 : Record 1018;
|
||||
|
||||
```al
|
||||
JobWIPBuffer@1002 : TEMPORARY Record 1018;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
CopyOfJobWIPBuffer@1002 : Record 1018;
|
||||
```al
|
||||
TempJobWIPBuffer@1002 : TEMPORARY Record 1018;
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
TempJobWIPBuffer@1002 : Record 1018;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
CopyOfJobWIPBuffer@1002 : Record 1018;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,88 +3,113 @@ title = "TextConst Suffixes"
|
|||
weight = 1210
|
||||
+++
|
||||
TextConst variable names should have a suffix (an approved three-letter suffix: Msg, Tok, Err, Qst, Lbl, Txt) describing usage.
|
||||
|
||||
Bad code
|
||||
|
||||
CannotDeleteLine@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLine,TABLECAPTION);
|
||||
```al
|
||||
CannotDeleteLine@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLine,TABLECAPTION);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLineErr,TABLECAPTION);
|
||||
```al
|
||||
CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLineErr,TABLECAPTION);
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
Text000@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"';
|
||||
...
|
||||
SalesLine.FIELDERROR(Type,STRSUBSTNO(Text000,...);
|
||||
...
|
||||
```al
|
||||
Text000@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"';
|
||||
...
|
||||
SalesLine.FIELDERROR(Type,STRSUBSTNO(Text000,...);
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
TypeHasBeenChangedErr@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"';
|
||||
...
|
||||
SalesLine.FIELDERROR(Type,STRSUBSTNO(TypeHasBeenChangedErr,...);
|
||||
...
|
||||
```al
|
||||
TypeHasBeenChangedErr@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"';
|
||||
...
|
||||
SalesLine.FIELDERROR(Type,STRSUBSTNO(TypeHasBeenChangedErr,...);
|
||||
...
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
Text004@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.';
|
||||
...
|
||||
Window@1007 : Dialog;
|
||||
...
|
||||
```al
|
||||
Text004@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.';
|
||||
...
|
||||
Window@1007 : Dialog;
|
||||
...
|
||||
Window.OPEN(Text004);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
IndentingMsg@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.';
|
||||
...
|
||||
Window@1007 : Dialog;
|
||||
...
|
||||
```al
|
||||
IndentingMsg@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.';
|
||||
...
|
||||
Window@1007 : Dialog;
|
||||
...
|
||||
Window.OPEN(IndentingMsg);
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
Text002@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?';
|
||||
...
|
||||
IF CONFIRM(Text002,TRUE,GLAcc.TABLECAPTION) THEN
|
||||
```al
|
||||
Text002@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?';
|
||||
...
|
||||
IF CONFIRM(Text002,TRUE,GLAcc.TABLECAPTION) THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
OpenWhereUsedWindowQst@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?';
|
||||
...
|
||||
IF CONFIRM(OpenWhereUsedWindowQst,TRUE,GLAcc.TABLECAPTION) THEN
|
||||
```al
|
||||
OpenWhereUsedWindowQst@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?';
|
||||
...
|
||||
IF CONFIRM(OpenWhereUsedWindowQst,TRUE,GLAcc.TABLECAPTION) THEN
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
Selection := STRMENU(Text003,2);
|
||||
...
|
||||
Text003@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components';
|
||||
```al
|
||||
Selection := STRMENU(Text003,2);
|
||||
...
|
||||
Text003@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components';
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
Selection := STRMENU(CopyFromQst,2);
|
||||
...
|
||||
CopyFromQst@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components';
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
DATASET
|
||||
{
|
||||
...
|
||||
{ 1 ;1 ;Column ;Chart\_of\_AccountsCaption;
|
||||
SourceExpr=Chart\_of\_AccountsCaption }
|
||||
...
|
||||
Chart\_of\_AccountsCaption@9647 : TextConst 'ENU=Chart of Accounts';
|
||||
```al
|
||||
DATASET
|
||||
{
|
||||
...
|
||||
{ 1 ;1 ;Column ;Chart_of_AccountsCaption;
|
||||
SourceExpr=Chart_of_AccountsCaption }
|
||||
...
|
||||
Chart_of_AccountsCaption@9647 : TextConst 'ENU=Chart of Accounts';
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
DATASET
|
||||
{
|
||||
...
|
||||
{ 1 ;1 ;Column ;Chart\_of\_AccountsCaption;
|
||||
SourceExpr=ChartOfAccountsLbl }
|
||||
...
|
||||
ChartOfAccountsLbl@9647 : TextConst 'ENU=Chart of Accounts';
|
||||
```al
|
||||
DATASET
|
||||
{
|
||||
...
|
||||
{ 1 ;1 ;Column ;Chart_of_AccountsCaption;
|
||||
SourceExpr=ChartOfAccountsLbl }
|
||||
...
|
||||
ChartOfAccountsLbl@9647 : TextConst 'ENU=Chart of Accounts';
|
||||
```
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue