<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[msnJournals]]></title><description><![CDATA[msnJournals is about sharing the learnings of Dynamics 365, Dynamics NAV, Business Central, SharePoint and other Microsoft products. ]]></description><link>https://www.msnjournals.com/home</link><generator>RSS for Node</generator><lastBuildDate>Thu, 08 Dec 2022 08:34:45 GMT</lastBuildDate><atom:link href="https://www.msnjournals.com/blog-feed.xml" rel="self" type="application/rss+xml"/><item><title><![CDATA[Consume Business Central APIs in SharePoint web part ]]></title><description><![CDATA[This post can help the developers in a scenario where consuming Business Central APIs secured with Azure AD and OAuth 2.0 from within a...]]></description><link>https://www.msnjournals.com/post/consume-business-central-apis-in-sharepoint-web-part</link><guid isPermaLink="false">622236072c4efa643a929598</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[OAuth]]></category><category><![CDATA[SharePoint]]></category><pubDate>Sat, 05 Mar 2022 09:44:36 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_3c352381f7af41d18d10e236b09d62cd~mv2.jpg/v1/fit/w_1000,h_628,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>This post can help the developers in a scenario where consuming Business Central APIs secured with Azure AD and OAuth 2.0 from within a SharePoint client-side web part. This post explains how to create a SharePoint web part that uses Business Central data.</p>
<h2><strong>Scenario</strong></h2><p>Creating a web part that fetch and display customers data from Business Central on selecting a Company in the dropdown list. In this scenario, the web part needs data from Customers and Companies entities from Business Central using APIs.</p>
<h2><strong>Create a web part project</strong></h2><p>Follow the steps in <a href="https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part" target="_blank" ><u>Build your first SharePoint client-side web part</u></a> page to create a new web part project. Name the web part as <strong>Customers</strong> instead of <strong>HelloWorld</strong>. Otherwise, the complete Customers web part project's source code can be downloaded from <a href="https://github.com/msnraju/sharepoint-customers-webpart" target="_blank" ><u>GitHub</u></a>. </p>
<p> </p><h2><strong>API permissions</strong></h2><p>To consume Business Central APIs, it should be authenticated with Azure AD OAuth 2.0 authentication. SharePoint can acquire permissions for the web part from Azure AD, OAuth 2.0 for the resources configured in <strong>package-solution.json</strong> / <strong>solution </strong>/ <strong>webApiPermissionRequests</strong>. </p>
<h3><strong>package-solution.json</strong></h3><p>The Following <strong>package-solution.json</strong> file contains the required permissions to access Business Central APIs. </p><pre><code>{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
  "solution": {
    "name": "customers-webpart",
    "id": "ff91b46e-3292-4100-9b77-c38d91fc6ed2",
    "version": "1.0.0.14",
    "includeClientSideAssets": true,
    "skipFeatureDeployment": false,
    "isDomainIsolated": false,
    "developer": {
      "name": "",
      "websiteUrl": "",
      "privacyUrl": "",
      "termsOfUseUrl": "",
      "mpnId": "Undefined-1.13.1"
    },
    "webApiPermissionRequests": [
      {
        "resource": "Dynamics 365 Business Central",
        "scope": "Financials.ReadWrite.All"
      }
    ]
  },
  "paths": {
    "zippedPackage": "solution/customers-webpart.sppkg"
  }
}
</code></pre><h2><strong>Services to consume Business Central APIs</strong></h2><p>Web part context has factory methods (<strong>aadHttpClientFactory)</strong> that can create HTTP client (<strong>AadHttpClient)</strong> for an endpoint. The same HTTP client can be used to consume secured APIs. SharePoint framework will take care of OAuth 2.0, sending access token in HTTP headers etc.</p>

<p>The following two services fetch data from Companies and Customers using <strong>AadHttpClient</strong> from Business Central.</p>
<h3><strong>Companies.service.ts</strong></h3><p>The following <strong>CompaniesService</strong> class gets companies data from Business Central API.</p><pre><code>import { WebPartContext } from "@microsoft/sp-webpart-base";
import { AadHttpClient, AadHttpClientResponse } from '@microsoft/sp-http';
import { ClientUrl } from "../config/WebAPIs.constants";
import { APIResponse } from "../models/APIResponse.model";
import { Company } from "../models/Company.model";

export default class CompaniesService {
    constructor(private context: WebPartContext, private environment: string) {
        this.environment = environment || "Production";
    }

    public getCompanies(): Promise<Company[]> {
        return new Promise((resolve, reject) => {
            const apiUrl = `${ClientUrl}/v2.0/${this.environment}/api/v2.0/companies`;

            this.context.aadHttpClientFactory
                .getClient(ClientUrl)
                .then((client: AadHttpClient): void => {
                    client
                        .get(apiUrl, AadHttpClient.configurations.v1)
                        .then((response: AadHttpClientResponse) => {
                            if (response.ok) {
                                return response.json()
                                    .then((response: APIResponse<Company>): void => {
                                        resolve(response.value);
                                    });
                            } else {
                                response.text().then(text => {
                                    reject(JSON.parse(text).error);
                                });
                            }
                        })
                        .catch(error => {
                            reject(error);
                        });
                }).catch(error => {
                    reject(error);
                });
        });
    }
}</code></pre><h3><strong>Customers.service.ts</strong></h3><p>The following <strong>CustomerService</strong> class gets customers data from Business Central API.</p><pre><code>import { WebPartContext } from "@microsoft/sp-webpart-base";
import { AadHttpClient, AadHttpClientResponse } from '@microsoft/sp-http';
import { ClientUrl } from "../config/WebAPIs.constants";
import { Customer } from "../models/Customer.model";
import { APIResponse } from "../models/APIResponse.model";

export default class CustomerService {
  constructor(private context: WebPartContext, private environment: string, private companyId: string) {
    this.environment = environment || "Production";
  }

  public getCustomers(): Promise<Customer[]> {
    return new Promise((resolve, reject) => {
      if (!this.companyId) {
        reject(new Error('Company ID should not be blank.'));
        return;
      }

      const apiUrl = `${ClientUrl}/v2.0/${this.environment}/api/v2.0/companies(${this.companyId})/customers`;

      this.context.aadHttpClientFactory
        .getClient(ClientUrl)
        .then((client: AadHttpClient): void => {
          client
            .get(apiUrl, AadHttpClient.configurations.v1)
            .then((response: AadHttpClientResponse) => {
              if (response.ok) {
                response.json()
                  .then((apiResponse: APIResponse<Customer>) => {
                    resolve(apiResponse.value);
                  });
              } else {
                response.text().then(text => {
                  reject(JSON.parse(text).error);
                });
              }
            })
            .catch(error => {
              reject(error);
            });
        })
        .catch(error => {
          reject(error);
        });
    });
  }
}</code></pre><h2><strong>React components</strong></h2><p>The following <strong>CompaniesDropdown</strong> and <strong>CompaniesList</strong> React components renders data using the above services. These code samples are using Fluent UI react components.</p>
<h3><strong>CompaniesDropdown.tsx</strong></h3><p>The following <strong>CompaniesDropdown</strong> class is a React component to render a dropdown using Companies service.</p><pre><code>import * as React from 'react';
import { ICompaniesDropdownProps } from './ICompaniesDropdownProps';
import CompaniesService from '../../../../services/Companies.service';
import { Company } from '../../../../models/Company.model';
import { Dropdown } from '@fluentui/react-northstar';

interface ICompaniesDropdownState {
  companies: Company[];
  loaded: boolean;
  hasError: boolean;
  error?: Error;
}

export default class CompaniesDropdown extends React.Component<ICompaniesDropdownProps, ICompaniesDropdownState, {}> {
  constructor(props: ICompaniesDropdownProps) {
    super(props);
    this.state = { companies: [], loaded: false, hasError: false };
  }

  public componentDidMount() {
    const service = new CompaniesService(this.props.context, this.props.environment);

    service.getCompanies().then(companies => {
      this.setState({ companies: companies, loaded: true });
    }).catch(error => {
      this.setState({ companies: [], loaded: true, hasError: true, error: error });
    });
  }

  public render(): React.ReactElement<ICompaniesDropdownProps> {
    const items = !this.state.loaded ? [] : this.state.companies.map((item) => {
      return { key: item.id, header: item.name };
    });

    const onChange = (_: any, event: any) => {
      this.props.onChange(event.value.key);
    };

    return (
      <Dropdown items={items} placeholder="Select company" onChange={onChange.bind(this)} />
    );
  }
}</code></pre><h3><strong>CustomerList.tsx</strong></h3><p>The following <strong>CustomerList</strong> class is a React component to render a list using Customers service.</p><pre><code>import * as React from 'react';
import { Loader, Segment, Text, List, Card, CardHeader, CardBody } from '@fluentui/react-northstar';
import { ICustomerListProps } from './ICustomerListProps';
import { Customer } from '../../../../models/Customer.model';
import CustomerService from '../../../../services/Customers.service';

interface ICustomerListState {
  customers: Customer[];
  loaded: boolean;
  hasError: boolean;
  error?: Error;
}

export default class CustomerList extends React.Component<ICustomerListProps, ICustomerListState, {}> {
  constructor(props: ICustomerListProps) {
    super(props);
    this.state = { customers: [], loaded: false, hasError: false };
  }

  public componentDidMount() {
    this.getCustomers();
  }

  public componentDidUpdate(prevProps: ICustomerListProps) {
    if (prevProps.companyId != this.props.companyId) {
      this.getCustomers();
    }
  }

  public render(): React.ReactElement<ICustomerListProps> {
    return (<Card fluid ghost>
      <CardHeader><Text weight="bold" content="Customers" /></CardHeader>
      <CardBody>{this.renderBody()}</CardBody>
    </Card>);
  }

  public renderBody() {
    if (!this.props.companyId) {
      return (<Text content="You must select a Company." />);
    }

    if (!this.state.loaded) {
      return (<Loader label="Loading..." />);
    }

    if (this.state.hasError)
      return (
        <Segment inverted color="red">
          <Text style={{ whiteSpace: "pre-wrap" }} as="pre" content={`${this.state.error.message}`} />
        </Segment>);

    const listItems = this.state.customers.map(customer => {
      return {
        key: customer.id,
        header: customer.displayName,
        content: `${customer.addressLine1} ${customer.addressLine2} ${customer.city} ${customer.postalCode}`
      };
    });

    return (<List navigable items={listItems} />);
  }

  private getCustomers() {
    const { context, environment, companyId } = this.props;

    if (!companyId) {
      this.setState({ customers: [], loaded: false, hasError: false, error: null });
    }

    const service = new CustomerService(context, environment, companyId);
    service.getCustomers()
      .then(customers => {
        this.setState({ customers: customers, loaded: true, hasError: false, error: null });
      })
      .catch(error => {
        this.setState({ customers: [], loaded: true, hasError: true, error: error });
      });
  }
}
</code></pre><h3><strong>Customers.tsx</strong></h3><p>The following <strong>Customers</strong> class is a React component to render <strong>CompaniesDropdown</strong> and <strong>CustomerList</strong> components.</p><pre><code>import * as React from 'react';
import { Provider, teamsTheme } from '@fluentui/react-northstar';
import CompaniesDropdown from './companies-dropdown/CompaniesDropdown';
import CustomerList from './customer-list/CustomerList';
import { ICustomersProps } from './ICustomersProps';

interface ICustomersState {
  companyId: string;
}

export default class Customers extends React.Component<ICustomersProps, ICustomersState, {}> {
  constructor(props: ICustomersProps) {
    super(props);
    this.state = { companyId: null };
  }

  private onChange(value: string) {
    this.setState({ companyId: value });
  }

  public render(): React.ReactElement<ICustomersProps> {
    return (
      <Provider theme={teamsTheme}>
        <CompaniesDropdown {... this.props} onChange={this.onChange.bind(this)} />
        <br />
        <CustomerList {... this.props} companyId={this.state.companyId} ></CustomerList>
      </Provider>
    );
  }
}
</code></pre><h3><strong>CustomersWebPart.ts</strong></h3><p>The following <strong>CustomersWebPart</strong> class is a SharePoint web part that renders <strong>Customers</strong> React component.</p><pre><code>import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'CustomersWebPartStrings';
import Customers from './components/Customers';
import { ICustomersProps } from './components/ICustomersProps';

export interface ICustomersWebPartProps {
  environment: string;
}

export default class CustomersWebPart extends BaseClientSideWebPart<ICustomersWebPartProps> {

  public render(): void {
    const element: React.ReactElement<ICustomersProps> = React.createElement(
      Customers,
      { context: this.context, environment: this.properties.environment }
    );

    ReactDom.render(element, this.domElement);
  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('environment', {
                  label: strings.EnvironmentFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}</code></pre><h2><strong>Build & Deploy the Customers web part</strong></h2><p>Run the following command at the project root folder to build and package the solution:</p><pre><code>gulp build && gulp bundle --ship && gulp package-solution --ship</code></pre><p>This will generate a solution file in \sharepoint\solution folder with .sppkg extension that can be deployed in SharePoint's App Catalog site. </p>
<figure><img src="https://static.wixstatic.com/media/394025_f541dea83c1542d3818d14df288352d8~mv2.jpg/v1/fit/w_1000,h_688,al_c,q_80/file.png"  ></figure><p>After deploying the web part solution in App Catalog site, the SharePoint administrator has to <strong>Accept</strong> API access in the SharePoint admin center.</p>
<figure><img src="https://static.wixstatic.com/media/394025_e73e931d52ca4330b772d11af5517e88~mv2.jpg/v1/fit/w_1000,h_612,al_c,q_80/file.png"  ></figure><h2><strong>Web part output</strong></h2><p>The following is the output of the Customers web part: </p>
<figure><img src="https://static.wixstatic.com/media/394025_e903a1961e93458abd1106afa880079c~mv2.jpg/v1/fit/w_1000,h_852,al_c,q_80/file.png"  ></figure><h2><strong>Conclusion</strong></h2><p>The above explanation is good enough to understand the concept of using Business Central APIs in SharePoint. The same approach can be used to consume Dataverse, Dynamics CRM, Graph APIs etc. To access multiple resources, an array of resource and scope should be updated in <strong>webApiPermissionRequests</strong> in <strong>package-soulution.json</strong> file. </p>

<p>Happy Coding!!!</p>

<p>Complete source code is available at <a href="https://github.com/msnraju/sharepoint-customers-webpart" target="_blank" ><u>GitHub</u></a>.</p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #SharePoint #NodeJS #WebAPIs </p>]]></content:encoded></item><item><title><![CDATA[Testing Business Central Online APIs using Postman]]></title><description><![CDATA[This post explains how to test Microsoft Dynamics Business Central Online APIs using Postman with complete details. Postman is a very...]]></description><link>https://www.msnjournals.com/post/testing-business-central-online-apis-using-postman</link><guid isPermaLink="false">62188ec36049b24451d0096d</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[OAuth]]></category><pubDate>Fri, 25 Feb 2022 18:36:53 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_e4c161f42d2340f2b7f670d884b698a1~mv2.png/v1/fit/w_1000,h_623,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>This post explains how to test Microsoft Dynamics Business Central Online APIs using Postman with complete details.</p>
<p>Postman is a very useful tool for developers to test various types of HTTP requests, including REST APIs. Business Central supports REST APIs in both On-Premises and Online environments. To test Business Central Online APIs, client application must pass through OAuth2 authentication. An advantage with Postman is that it supports OAuth2 authentication, therefore testing Business Central APIs becomes easy.</p>
<h2><strong>App registration</strong></h2><p>Business Central uses Azure Active Directory for authentication. To call Business Central APIs in Postman, an access token should be passed in the HTTP request headers. To get access token, firstly, it is required to register an App in Azure Portal.</p>

<p>The following sections explains how to register an <strong>Application</strong> in <a href="https://portal.azure.com/" target="_blank" ><u>Azure Portal</u></a>.</p>
<h3><strong>Register a new Application</strong></h3><p>1.  Login to <a href="https://portal.azure.com/" target="_blank" ><u>Azure Portal</u></a> with an Azure AD account.</p>
<p>2.  Search for App registrations in the search box and select <strong>App registrations</strong> > <strong>New registration</strong>.</p>
<p>3.  Enter the application name, select the selected supported account type, and click the <strong>Register</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_a3d51c710bf44978b66097918d9fad2c~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png"  ></figure><h3><strong>Authentication</strong></h3>
<p>1. Select <strong>Authentication</strong> and click the <strong>Add a platform</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_0215502bc38b4386b94e3516dd0a8835~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png"  ></figure><p>2. Select <strong>Web</strong> in the <strong>Configure platforms</strong> panel.</p><figure><img src="https://static.wixstatic.com/media/394025_9dc87a195ffc4b39bd3a5e2bbe3058b5~mv2.png/v1/fit/w_764,h_720,al_c,q_80/file.png"  ></figure><p>3. Enter <a href=""https://api.businesscentral.dynamics.com"" target="_blank" ><u>https://api.businesscentral.dynamics.com</u></a> in <strong>Redirect URLs</strong> and click the <strong>Configure</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_c6733f2ae49b4cf69c75b77a17f68fc3~mv2.png/v1/fit/w_738,h_720,al_c,q_80/file.png"  ></figure><h3><strong>API permissions</strong></h3>
<p>1. Select <strong>API permissions</strong> and click the <strong>Add a permission</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_f970200329c8450fbca75908bbf83808~mv2.png/v1/fit/w_938,h_720,al_c,q_80/file.png"  ></figure><p>2. Select <strong>Dynamics 365 Business Central</strong> in <strong>Request API permissions</strong> panel.</p><figure><img src="https://static.wixstatic.com/media/394025_8d8d0b35f7c94ce4be47457817e910cb~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png"  ></figure><p>3. Select <strong>Delegated permissions</strong>, select permissions and click the <strong>Add permissions</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_50c4898a63b948e885c4b4291d34efe7~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png"  ></figure>
<h3><strong>Certificates & secrets</strong></h3>
<p>1. Select <strong>Certificates & secrets</strong> and click the <strong>New client secret</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_df98ecac3885472bad05c2407d895ee7~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png"  ></figure><p>2. Enter <strong>Description</strong> and click the <strong>Add</strong> button.</p>
<figure><img src="https://static.wixstatic.com/media/394025_512d78417bf0489f8d19cd13edb6f500~mv2.png/v1/fit/w_746,h_720,al_c,q_80/file.png"  ></figure><p>3. Note the client secret <strong>Value</strong> and secure it. Once the application page is closed, client secret cannot be retrieved.</p><figure><img src="https://static.wixstatic.com/media/394025_110c7f2ed4104b30b20742930ecca3ab~mv2.png/v1/fit/w_1000,h_260,al_c,q_80/file.png"  ></figure>
<h3><strong>Inputs required for Postman</strong></h3><p>The following inputs are required in Postman to configure OAuth Authentication details: </p>

<p>1. Select <strong>Overview</strong> and note the <strong>Application (client) ID </strong>and the <strong>Directory (tenant) ID.</strong></p><figure><img src="https://static.wixstatic.com/media/394025_14f4f23047e84a39aacb85ba6adf3a5b~mv2.png/v1/fit/w_1000,h_682,al_c,q_80/file.png"  ></figure><p>2) Click the <strong>Endpoints</strong> button in the <strong>Overview</strong> page and note the <strong>OAuth 2.0 authorization endpoint (v1)</strong> and the <strong>OAuth 2.0 token endpoint (v1)</strong> URLs.</p><figure><img src="https://static.wixstatic.com/media/394025_c8aaf0dd4bae4b3195702c04b9760b55~mv2.png/v1/fit/w_1000,h_613,al_c,q_80/file.png"  ></figure><h2><strong>Postman</strong></h2><p>The following sections explains how to update authentication details in Postman.</p>
<p>1. Open Postman, click the <strong>Authorization</strong> tab, and select OAuth 2.0 in the Type dropdown. </p><figure><img src="https://static.wixstatic.com/media/394025_f40998d522d546e9b326e48ba3a68b22~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png"  ></figure><p>2. Enter a meaningful name in the <strong>Token Name</strong> field.</p>
<p>3. Enter <a href="https://api.businesscentral.dynamics.com" target="_blank" ><u>https://api.businesscentral.dynamics.com</u></a> in <strong>Callback URL</strong> field.</p>
<p>4. Enter the <strong>OAuth 2.0 authorization endpoint (v1) </strong>and the <strong>OAuth 2.0 token endpoint (v1)</strong> URLs noted in <strong>Auth URL </strong>and<strong> Access Token URL</strong> fields.</p>
<p>5. Enter the noted <strong>Application (client) ID</strong> in <strong>Client ID</strong> field.</p>
<p>6. Paste the <strong>Value</strong> copied in <strong>Certificates & secrets</strong> in the <strong>Client Secret</strong> field.</p>
<p>7.  Select <strong>Advanced Options</strong> tab and enter <a href="https://api.businesscentral.dynamics.com" target="_blank" ><u>https://api.businesscentral.dynamics.com</u></a> in the <strong>Resource</strong> field, and click the <strong>Get New Access Token</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_59bec760ccef46bfa0385eda8cfb0237~mv2.png/v1/fit/w_1000,h_712,al_c,q_80/file.png"  ></figure><p>8. Sign in with the AD account in the <strong>Sign in to your account</strong> window and <strong>Consent on behalf of your organization</strong> and click the <strong>Accept</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_8c93a1d72ea54cd4bd501f4069c6d645~mv2.png/v1/fit/w_608,h_720,al_c,q_80/file.png"  ></figure><p>9. Click the <strong>Proceed</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_beb151f83d8b415db83fda648474b139~mv2.png/v1/fit/w_920,h_571,al_c,q_80/file.png"  ></figure><p>10. Click the <strong>Use Token</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_593a70d9d7e84d3eb14216ace91489fb~mv2.png/v1/fit/w_1000,h_326,al_c,q_80/file.png"  ></figure><p>11. Enter <a href="https://api.businesscentral.dynamics.com/v2.0/production/api/v2.0/companies" target="_blank" ><u>https://api.businesscentral.dynamics.com/v2.0/production/api/v2.0/companies</u></a> in the URL and click the <strong>Send</strong> button.</p>
<p>The following is the response from the <strong>companies</strong> API.</p><figure><img src="https://static.wixstatic.com/media/394025_c96899d688db4b72a29b440bd95dc6f0~mv2.png/v1/fit/w_1000,h_534,al_c,q_80/file.png"  ></figure><p>The following is the response from the <strong>customers</strong> API.</p><figure><img src="https://static.wixstatic.com/media/394025_10910658b3e5491eb932bbdacf6d8ca2~mv2.png/v1/fit/w_1000,h_574,al_c,q_80/file.png"  ></figure><h2><strong>Conclusion</strong></h2><p>Postman supports POST, GET, PATCH, DELETE and many more HTTP methods, so it is possible to test all CRUD (CREATE, READ, UPDATE, and DELETE) operations with Postman. Please refer to <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/" target="_blank" ><u>https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/</u></a> for standard APIs available in Business Central .</p>

<p>Postman collection for the above examples is available at <a href="https://github.com/msnraju/postman-collection-01" target="_blank" ><u>GitHub</u></a><u>.</u></p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #Postman #WebAPIs</p>]]></content:encoded></item><item><title><![CDATA[How to call Business Central Online APIs from Node.js Application]]></title><description><![CDATA[This post explains how to read data from Microsoft Dynamics 365 Business Central (Online) APIs in a Node.js application,  and also how to...]]></description><link>https://www.msnjournals.com/post/how-to-call-business-central-online-apis-from-node-js-application</link><guid isPermaLink="false">621217e1f8d0b22855c26c3d</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[OAuth]]></category><pubDate>Sun, 20 Feb 2022 13:53:39 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_4d4cda1913514cfa91ae73570d000280~mv2.png/v1/fit/w_1000,h_643,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>This post explains how to read data from Microsoft Dynamics 365 Business Central (Online) APIs in a <a href="https://nodejs.org/" target="_blank" ><u>Node.js</u></a> application,  and also how to configure <strong>App registrations</strong> in <a href="https://portal.azure.com" target="_blank" ><u>Azure Portal</u></a> that can be used in Node.js console application to get Access Token and a sample code that acquires access token using <a href="https://docs.microsoft.com/en-us/javascript/api/overview/azure/activedirectory" target="_blank" ><u>Windows Azure Active Directory Authentication Library (ADAL) for Node.js</u></a><u>,</u>  and retrieve companies data from <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_company_get" target="_blank" ><u>Companies Web API</u></a>.</p>
<h2><strong>App registration</strong></h2><p>The following sections explains how to register <strong>Application</strong> in <a href="https://portal.azure.com" target="_blank" ><u>Azure Portal</u></a> for Node.js console application with API access to D365 Business Central (Online).</p>
<h3><strong>Register a new Application</strong></h3><p>1.  Login to <a href="https://portal.azure.com" target="_blank" ><u>Azure Portal</u></a> with your Azure AD account.</p>
<p>2.  Search for App registrations in the search box and select <strong>App registrations</strong> > <strong>New registration</strong>.</p>
<p>3.  Enter the application name, select the selected supported account type, and click the <strong>Register</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_c629fb66f1ea4246a4b6b7b31bd134c0~mv2.png/v1/fit/w_1000,h_870,al_c,q_80/file.png"  ></figure><p>4.  Azure AD will assign a unique <em>Client ID</em> and an <em>Object ID</em>. These details can be seen in the <strong>Overview</strong> page.</p><figure><img src="https://static.wixstatic.com/media/394025_a1f455f3ee6146a897aa36d8013cfc72~mv2.png/v1/fit/w_1000,h_1000,al_c,q_80/file.png"  ></figure><h3><strong>Authentication</strong></h3><p>1.  Select <strong>Authentication</strong>, scroll down to <strong>Advanced Settings</strong> > <strong>Allow public client flows</strong> and select <strong>Enable the following mobile and desktop flows</strong>.</p><figure><img src="https://static.wixstatic.com/media/394025_37acb1d1e7b848c7b77c6475580cfdeb~mv2.png/v1/fit/w_1000,h_292,al_c,q_80/file.png"  ></figure><h3><strong>Permissions and Consent</strong></h3><p>1.  Select <strong>API Permissions</strong> and go to <strong>Add a permission</strong></p><figure><img src="https://static.wixstatic.com/media/394025_6f2a00b3e98f4e78ac3309b53506d311~mv2.png/v1/fit/w_1000,h_1000,al_c,q_80/file.png"  ></figure><p>2.  Select <strong>Dynamics 365 Business Central</strong> in the <strong>Request API permissions</strong> page.</p><figure><img src="https://static.wixstatic.com/media/394025_ae07fced2b9948cabbd5ea5859b95344~mv2.png/v1/fit/w_1000,h_751,al_c,q_80/file.png"  ></figure><p>3.  Select <strong>Delegated permissions</strong>, check <strong>user_impersonation</strong> and <strong>Financials.ReadWrite.All</strong> permissions, and click the <strong>Add permissions</strong> button.</p><figure><img src="https://static.wixstatic.com/media/394025_3bb62909562e4a2aa10e5f932aef98b7~mv2.png/v1/fit/w_1000,h_841,al_c,q_80/file.png"  ></figure><p>4.  Click the <strong>Grant admin consent for Contoso</strong> button to grant admin consent to this application.</p><figure><img src="https://static.wixstatic.com/media/394025_b7e1e8cf56b546f4908eddc7f4b7e60f~mv2.png/v1/fit/w_1000,h_513,al_c,q_80/file.png"  ></figure><p>5.  After providing admin consent in <strong>API permissions</strong> page, status should be “<em>Granted for Contoso</em>”.</p><figure><img src="https://static.wixstatic.com/media/394025_33d513736af34c749c5dca443fb98884~mv2.png/v1/fit/w_1000,h_513,al_c,q_80/file.png"  ></figure><h2><strong>Node.js Application</strong></h2><p>Following is the typescript code to get access token from Azure Active Directory using OAuth2 authentication and to get the companies data from D365 Business Central (Online) APIs.</p>
<h3><strong>bc-connector.ts</strong></h3><p>BCConnector class is used to get access token using ADAL library. The <strong>connect</strong> is a function which calls ADAL's acquireToken functions and return a promise of access token.</p><pre><code>import * as adal from 'adal-node';

export class BCConnector {
    authorityUrl: string;
    resource: string;
    adalContext: adal.AuthenticationContext;
    tokenResponse?: adal.TokenResponse;

    constructor(tenantId: string, resource: string, private clientId: string, private username: string, private password: string) {
        this.authorityUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/token`;
        this.resource = `https://${resource}/`;

        this.adalContext = new adal.AuthenticationContext(this.authorityUrl);
    }

    connect(): Promise<adal.TokenResponse> {
        return new Promise((resolve, reject) => {
            const adalCallback: adal.AcquireTokenCallback = (error: Error, response: adal.TokenResponse | adal.ErrorResponse) => {
                if (!error) {
                    console.log('Authentication successful.');
                    this.tokenResponse = response as adal.TokenResponse;
                    resolve(this.tokenResponse);
                }
                else {
                    console.log('Authentication failed.');
                    reject(error);
                }
            }

            console.log('Authenticating ....');
            if (this.tokenResponse && this.tokenResponse.refreshToken) {
                this.adalContext.acquireTokenWithRefreshToken(this.tokenResponse.refreshToken, this.clientId, this.resource, adalCallback);
            } else {
                this.adalContext.acquireTokenWithUsernamePassword(this.resource, this.username, this.password, this.clientId, adalCallback);
            }
        });
    }
}</code></pre><h3><strong>index.ts</strong></h3><p>The following code gets the access token using BCConnector class and calls the Business Central <em>companies</em> API using <em>http request</em> to get the companies data.</p>
<pre><code>import { BCConnector } from './bc-connector';
import * as https from 'https';

const tenantId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx';
const clientId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx';
const hostName = 'api.businesscentral.dynamics.com';
const userName = 'admin@xxxx.onmicrosoft.com';
const password = 'xxxxxxxx';

const dynamics = new BCConnector(tenantId,
    hostName,
    clientId,
    userName,
    password);

dynamics.connect().then(response => {
    console.log(`retrieving data from bc...`);
    var req = https.request({
        hostname: 'api.businesscentral.dynamics.com',
        path: '/v2.0/production/api/v2.0/companies',
        headers: {
            "accept": "application/json",
            "Authorization": `Bearer ${response.accessToken}`
        }
    }, res => {
        res.on('data', d => {
            const responseJson = JSON.parse(d.toString());
            console.log(JSON.stringify(responseJson, null, 2));
        })
    });

    req.on('error', error => {
        console.error(error)
    })

    req.end();
}).catch(error => {
    console.error(error);
});</code></pre><h3><strong>Output</strong></h3><p>The following is the output from the Node.js console application.</p><pre><code>Authenticating ....                                                                                             
Authentication successful.                                                                                      
retrieving data from bc...                                                                                      
{                                                                                                               
  "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/Production/api/v2.0/$metadata#companies",    
  "value": [                                                                                                    
    {                                                                                                           
      "id": "f53e0828-3d8a-eb11-bb5f-000d3a398a56",                                                             
      "systemVersion": "19.3.34541.34662",                                                                      
      "name": "CRONUS USA, Inc.",                                                                               
      "displayName": "",                                                                                        
      "businessProfileId": "",                                                                                  
      "systemCreatedAt": "2021-03-21T12:01:27.08Z",                                                             
      "systemCreatedBy": "00000000-0000-0000-0000-000000000001",                                                
      "systemModifiedAt": "2021-03-21T12:01:27.08Z",                                                            
      "systemModifiedBy": "00000000-0000-0000-0000-000000000001"                                                
    },                                                                                                          
    {                                                                                                           
      "id": "c2c5eb36-3d8a-eb11-bb5f-000d3a398a56",                                                             
      "systemVersion": "19.3.34541.34662",                                                                      
      "name": "My Company",                                                                                     
      "displayName": "",                                                                                        
      "businessProfileId": "",                                                                                  
      "systemCreatedAt": "2021-03-21T12:01:50.367Z",                                                            
      "systemCreatedBy": "00000000-0000-0000-0000-000000000001",                                                
      "systemModifiedAt": "2021-03-21T12:01:50.367Z",                                                           
      "systemModifiedBy": "00000000-0000-0000-0000-000000000001"                                                
    }                                                                                                           
  ]                                                                                                             
}                                                                                                               </code></pre><h2><strong>Conclusion</strong></h2><p>The above code is just an example to read data from Business Central (Online). In this application, Business Central APIs can be used to perform all types of operations such as create, read, update, etc.</p>

<p>Node.js is a very popular platform to build robust applications, and very easy to develop applications using Node.js. Node.js application can be used as a middleware to integrate Business Central with other applications. Power Automate is a good option for integrations, but if there is a complex logic this would be a better approach.</p>

<p>Happy Coding!!!</p>

<p>Complete source code is available at <a href="https://github.com/msnraju/nodejs-bc-connector" target="_blank" ><u>GitHub</u></a><u>.</u></p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #NodeJS #WebAPIs</p>
]]></content:encoded></item><item><title><![CDATA[All you need to know about Business Central Webhooks]]></title><description><![CDATA[Webhooks is a great feature introduced in Business Central to notify other applications about data changes occurring in entities....]]></description><link>https://www.msnjournals.com/post/all-you-need-to-know-about-business-central-webhooks</link><guid isPermaLink="false">61236379a5914600150ac18a</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><pubDate>Wed, 25 Aug 2021 10:24:22 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_1f76ce083fe64e49adeed7a8367959f9~mv2.png/v1/fit/w_937,h_347,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Webhooks is a great feature introduced in Business Central to notify other applications about data changes occurring in entities. Webhooks are a way for applications to communicate between them automatically. When a record is created / updated / deleted in Business Central, a notification is sent to the subscriber automatically.  </p>
<figure><img src="https://static.wixstatic.com/media/394025_1f76ce083fe64e49adeed7a8367959f9~mv2.png/v1/fit/w_937,h_347,al_c,q_80/file.png"  ></figure>
<h2>Comparison between Publisher-Subscriber Events and Webhooks</h2><p>Webhooks' design pattern is very similar to Publisher-Subscriber events in AL, but it is over HTTP. The design pattern (PubSub) for Publisher-Subscriber and Webhooks is same. The below comparison may help you to understand the differences better.</p>
<h3>Publisher-Subscriber events</h3><ul>
  <li>Publisher-Subscriber events work within Business Central Extensions. 													 </li>
  <li>Subscriber functions are executed synchronously.</li>
  <li>Subscriber function can cause performance issues because it is executed within the transaction.                                 </li>
</ul><h3>Webhooks</h3><ul>
  <li>Webhooks work across applications. Publisher is Business Central and the subscriber can be any other application.</li>
  <li>Webhooks work asynchronously. </li>
  <li>There will not be any performance impact to Business Central.</li>
</ul><h2>Use cases</h2><p>Extend the Business Central functionality </p><ul>
  <li>Generating Airway bill on Sales Shipment in Warehouse application.</li>
  <li>Sending E-Mail, SMS, WhatsApp notifications to customer on Sales Order release by an external application.</li>
</ul><p>Synchronize the Business Central data</p><ul>
  <li>Synchronize Customer, Vendor masters with CRM application</li>
  <li>Synchronize Inventory masters with Warehouse application. </li>
</ul><h2>How to create Webhook</h2><p>As of now, all APIs in Business Central support Webhooks, with the following exceptions:</p><ol>
  <li>API page with temporary / system table as source</li>
  <li>API page with a composite key</li>
  <li>API type query</li>
</ol><h2>Working with Webhooks</h2><p>Subscriber for Business Central Webhook is always an external application. Typically, its a web application developed in .Net / Node.js, or it can be in any other framework / language. </p>

<p>The following series of code are written in Node.JS, and ExpressJS to illustrate Webhook operations. Business Central on premise is used just to avoid the complexity of registering Client Application in Azure portal, generating OAuth2 Authorization Token, etc.</p>
<h3>Setup</h3><p>The following JavaScript code listens to https port 3000 and assigns some constants which will be used in other functions.</p><pre><code>const fs = require("fs");
const httpntlm = require("httpntlm");
const express = require("express");
const https = require("https");

const companyId = "74e35bd0-2590-eb11-bb66-000d3abcddd1";
const baseUrl = <a href=""http://localhost:7048/BC180";
//" target="_blank" ><u>"http://localhost:7048/BC180";
</u></a>
// replace with actual values
const defaults = {
  username: "USER-NAME",
  password: "PASSWORD",
  workstation: "",
  domain: "DOMAIN-NAME",
};

const app = express();
const port = 3000;

const key = fs.readFileSync("./ssl/key.pem");
const cert = fs.readFileSync("./ssl/cert.pem");
const server = https.createServer({ key: key, cert: cert }, app);

app.use(express.json());
app.use(
  express.urlencoded({
    extended: true,
  })
);

server.listen(port, () => {
  console.log(`Example app listening at https://localhost:${port}`);
}); </code></pre><h3>Register a Webhook Subscription</h3><p>The following code registers webhook subscription for the Customer entity. If a customer is created, updated or deleted a notification is send to the subscriber. </p><pre><code>app.get("/register-webhook", function (req, res) {
  const body = {
    notificationUrl: `https://localhost:${port}/customer-notification`,
    resource: `/api/v2.0/companies(${companyId})/customers`,
    clientState: "state123",
  };

  const options = {
    ...defaults,
    headers: {
      "Content-Type": "application/json",
    },
    url: `${baseUrl}/api/v2.0/subscriptions`,
    body: JSON.stringify(body),
  };

  httpntlm.post(options, function (err, response) {
    if (err) {
      return err;
    }

    res.setHeader("content-type", "application/json");
    res.send(response.body);
  });
});</code></pre><h3>Receive Notifications</h3><p>The following code receives notifications from Business Central when a Customer entity is updated. Also, it executes at the time of registering / renewing the subscription to validate "notificationUrl".</p><pre><code>app.post("/customer-notification", function (req, res) {
  // response to validation requests
  if (req.query.validationToken) {
    res.send(req.query.validationToken);
    return;
  }

  console.log("Customer entity updates:");
  console.log(req.body);
  res.send("");
});</code></pre>
<p><em>Note: When creating a subscription and renewing a subscription, the client has to return the "validationToken" in the body with response code 200.</em></p>
<h3>Renew the Webhook Subscription</h3><p>By default, the Webhook Subscription will be expired after 3 days if it is not renewed. This setting can be changed in "CustomSettings.config" file. The following code renews the Webhook Subscription for the Customer entity. </p>
<pre><code> 
app.post("/renew-subscription", function (req, res) {
  const subscriptionId = req.body.subscriptionId;
  const eTag = req.body.eTag;

  const body = {
    notificationUrl: `https://localhost:${port}/customer-notification`,
    resource: `/api/v2.0/companies(${companyId})/customers`,
    clientState: "state123",
  };

  const options = {
    ...defaults,
    url: `${baseUrl}/api/v2.0/subscriptions('${subscriptionId}')`,
    headers: {
      "Content-Type": "application/json",
      "If-Match": eTag,
    },
    body: JSON.stringify(body),
  };

  httpntlm.patch(options, function (err, response) {
    if (err) {
      return err;
    }

    res.setHeader("content-type", "application/json");
    res.send(response.body);
  });
});</code></pre><h3>Unsubscribe the Webhook Subscription</h3><p>It is always better to unsubscribe when the notifications are no longer needed, otherwise the system has to try again and again until it expires. The following code deletes the Webhook Subscription. </p><pre><code>app.post("/delete-subscription", function (req, res) {
  const subscriptionId = req.body.subscriptionId;
  const eTag = req.body.eTag;

  const options = {
    ...defaults,
    url: `${baseUrl}/api/v2.0/subscriptions('${subscriptionId}')`,
    headers: {
      "If-Match": eTag,
    },
  };

  httpntlm.delete(options, function (err, response) {
    if (err) {
      return err;
    }

    res.setHeader("content-type", "application/json");
    if (response.statusCode == 204) {
      res.send("subscription deleted");
    } else {
      res.send(response.body);
    }
  });
});</code></pre><h3>Get Subscriptions</h3><p>The following code gets the active Webhook Subscriptions in Business Central.</p><pre><code>app.get("/get-subscriptions", function (req, res) {
  const options = {
    ...defaults,
    url: `${baseUrl}/api/v2.0/subscriptions`,
  };

  httpntlm.get(options, function (err, response) {
    if (err) {
      return err;
    }

    res.setHeader("content-type", "application/json");
    res.send(response.body);
  });
});</code></pre><h2>Webhook Limitations</h2><p>Only create, update and delete events are supported to send notifications to the subscribers. At the present, It is not possible to create a custom notification for an event like sales order approved / rejected, order shipped, delivered etc. </p>
<p>Instead, Firebase can be used to publish custom notifications, but that's for another post.</p>

<p>An anonymous URL is required to subscribe Webhook therefore, Business Central cannot be a subscriber.  We always need an external application to get notifications from other applications into Business Central. </p>
<p>Power-Automate can be one of the options to overcome this limitation. </p>
<h2>Conclusion</h2><p>Webhooks are very helpful to send notifications to external applications asynchronously without compromising performance.  </p>

<p>This is the area where compromised designs are seen in most of the implementations.  Often, Push & Pull / Polling techniques are used to synchronize data with external applications, but both techniques can cause performance issues. Pull / Polling technique brings unnecessary load on the system by requesting data repeatedly at set intervals. Push technique can cause delay in completing the transaction (which causes locking errors). </p>

<p>Push is always better than Pull but in an asynchronous way (using job queues or background sessions). Webhooks does the same thing without any need of writing a single line of code.</p>

<p>Happy Coding!!!</p>

<p>You can find complete source code at <a href="https://github.com/msnraju/business-central-webhooks" target="_blank" ><u>GitHub</u></a><u>.</u></p>

<p><u>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #NodeJS #Webhooks</u></p>]]></content:encoded></item><item><title><![CDATA[How to detect anomalies in Business Central data using AL Http datatypes]]></title><description><![CDATA[Anomaly Detector is an Azure Cognitive Service that detects anomalies in time-series data. It consists of simple REST APIs that can be...]]></description><link>https://www.msnjournals.com/post/how-to-detect-anomalies-in-business-central-data-using-al-http-datatypes</link><guid isPermaLink="false">6045ebf765e2ab002b7d8189</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><pubDate>Tue, 09 Mar 2021 07:39:47 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_4b02223186324f7aa67ace4e20cdf282~mv2.png/v1/fit/w_772,h_297,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Anomaly Detector is an Azure Cognitive Service that detects anomalies in time-series data. It consists of simple REST APIs that can be consumed by any application. This post explains how to setup Azure Anomaly Detector Service, and how to consume Anomaly Detector REST APIs in Business Central using AL Http datatypes.</p>
<h2>Why and Where to find Anomalies</h2><p>Purpose of Anomaly Detector is to identify unknown threats in time-series data. Anomalies can be detected in any time-series data. </p>

<p>The following are the potential data sources in Business Central for anomaly detection:</p><ul>
  <li>Bank Ledger (Payments / Receipts)</li>
  <li>Cash / GL  (Payments / Receipts)</li>
  <li>Sales (Invoices / Cr. Memos)</li>
  <li>Purchase (Invoices / Cr. Memos)</li>
  <li>Row Material Consumption (Item Ledger)</li>
  <li>Production Output (Item Ledger)</li>
</ul><p>Any data that has Time (Date / Time / DateTime) and Value (Integer / Decimal) columns is a valid data source for anomaly detection.</p>
<h2>How to Setup Cognitive Service</h2><ul>
  <li>Login to <a href="https://portal.azure.com/" target="_blank" rel="noopener">https://portal.azure.com</a></li>
</ul><figure><img src="https://static.wixstatic.com/media/394025_4b02223186324f7aa67ace4e20cdf282~mv2.png/v1/fit/w_772,h_297,al_c,q_80/file.png"  ></figure><ul>
  <li>Select Cognitive Services and click the Create button in Cognitive Services page.</li>
</ul><figure><img src="https://static.wixstatic.com/media/394025_452c00f5f223450d83e79f09310d7c49~mv2.png/v1/fit/w_814,h_223,al_c,q_80/file.png"  ></figure><ul>
  <li>Search for "Anomaly Detector" and click the Create button in "Anomaly Detector" card.</li>
</ul><figure><img src="https://static.wixstatic.com/media/394025_05af23ec1dcd47858a29b0d03a23ddc0~mv2.png/v1/fit/w_674,h_636,al_c,q_80/file.png"  ></figure><ul>
  <li>Update the project details and click the "Review + Create" button.</li>
</ul><figure><img src="https://static.wixstatic.com/media/394025_5ef23ad605e84c178940ddfe0f25e4c6~mv2.png/v1/fit/w_907,h_633,al_c,q_80/file.png"  ></figure><ul>
  <li>After creating your "Anomaly Detector" service, open the service and click the Overview link in the left panel.</li>
</ul><figure><img src="https://static.wixstatic.com/media/394025_027c54fb96f443ccb7fdb877d0e8344a~mv2.png/v1/fit/w_1000,h_443,al_c,q_80/file.png"  ></figure><p>You can check Endpoint URL (masked) in the above screen.</p><ul>
  <li>Click the "Keys and Endpoint" link in the left panel or the "Click here to manage keys" link in the previous screen to get the API key.</li>
</ul><figure><img src="https://static.wixstatic.com/media/394025_4ac561e30fb647289dc2e38ce0d69aa3~mv2.png/v1/fit/w_1000,h_543,al_c,q_80/file.png"  ></figure><p>You can take the API key (KEY 1) and the Endpoint URL from the above screen.</p>
<h2>AL Code to detect anomalies using REST APIs</h2><p>The following code sends date wise bank transactions to anomaly detector service and retrieves anomalies in those transactions. </p><p><strong>Constants / Labels : </strong>SubscriptionKeyLbl is masked for security reasons. EndpointLbl is the value of "KEY 1" from "Keys and Endpoint" screen. Ideally these values should be kept in a Setup table.</p><pre><code>var
        RequestUrlLbl: Label '%1/anomalydetector/v1.0/timeseries/entire/detect', Locked = true, Comment = '%1 = Endpoint';
        EndpointLbl: Label 'https://anomalydetector.cognitiveservices.azure.com', Locked = true;
        SubscriptionKeyLbl: Label 'XXXX95d2XXXX1ebaXXXX4fdbXXXX898', Locked = true;</code></pre>
<p><strong>FindAnomalies </strong>method takes Bank Account No., From Date and To Date parameters and returns Anomalies in the data.</p><pre><code>procedure FindAnomalies(BankAccountNo: Code[20]; FromDate: Date; ToDate: Date; AnomalyData: Dictionary of [Date, Decimal]): Boolean
var
	TimeSeriesData: Dictionary of [Date, Decimal];
begin
	ToTimeSeriesData(BankAccountNo, FromDate, ToDate, TimeSeriesData);
	exit(CheckAnomaly(TimeSeriesData, AnomalyData));
end;</code></pre>
<p><strong>ToTimeSeriesData</strong> method takes data from "Bank Account Ledger Entry" and stores it in the TimeSeriesData variable.</p>
<p><em>Note: Query object can be used to achieve the same for better performance. </em></p><pre><code>local procedure ToTimeSeriesData(BankAccountNo: Code[20]; FromDate: Date; ToDate: Date; TimeSeriesData: Dictionary of [Date, Decimal])
var
	BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
	PrevValue: Decimal;
begin
	BankAccountLedgerEntry.Reset();
	BankAccountLedgerEntry.SetRange("Bank Account No.", BankAccountNo);
	BankAccountLedgerEntry.SetRange("Posting Date", FromDate, ToDate);
	if BankAccountLedgerEntry.FindSet() then
		repeat
			if TimeSeriesData.Get(BankAccountLedgerEntry."Posting Date", PrevValue) then
				TimeSeriesData.Set(BankAccountLedgerEntry."Posting Date", PrevValue + BankAccountLedgerEntry."Amount (LCY)")
			else
				TimeSeriesData.Add(BankAccountLedgerEntry."Posting Date", BankAccountLedgerEntry."Amount (LCY)");
		until BankAccountLedgerEntry.Next() = 0;
end;</code></pre><p><strong>TimeSeriesDataToJson</strong> method convert TimeSeriesData from Dictionary to JSON object.</p><pre><code>local procedure TimeSeriesDataToJson(TimeSeriesData: Dictionary of [Date, Decimal]): JsonObject
var
	JTimeSeriesData: JsonObject;
	JSeriesItem: JsonObject;
	JSeriesItems: JsonArray;
	PostingDate: Date;
	Value: Decimal;
begin
	foreach PostingDate in TimeSeriesData.Keys do begin
		TimeSeriesData.Get(PostingDate, Value);
		JSeriesItem.Add('timestamp', Format(PostingDate, 0, 9));
		JSeriesItem.Add('value', Format(Value, 0, 9));

		JSeriesItems.Add(JSeriesItem);
	end;

	JTimeSeriesData.Add('series', JSeriesItems);
	JTimeSeriesData.Add('maxAnomalyRatio', 0.25);
	JTimeSeriesData.Add('sensitivity', 95);
	JTimeSeriesData.Add('granularity', 'daily');
	exit(JTimeSeriesData);
end;</code></pre>
<p><strong>CheckAnomaly</strong> method takes TimeSeries data and returns anomalies in that data.</p><pre><code>local procedure CheckAnomaly(TimeSeriesData: Dictionary of [Date, Decimal]; AnomalyData: Dictionary of [Date, Decimal]): Boolean
var
	JTimeSeriesData: JsonObject;
	JResponse: JsonObject;
begin
	JTimeSeriesData := TimeSeriesDataToJson(TimeSeriesData);
	if not GetAPIResponse(JTimeSeriesData, JResponse) then
		exit;

	exit(AnomalyValuesToDataSet(JResponse, TimeSeriesData, AnomalyData));
end;</code></pre>
<p><strong>GetAPIResponse </strong>method sends TimeSeriesData to REST API and returns the JSON response from the API.</p><pre><code>local procedure GetAPIResponse(JTimeSeriesData: JsonObject; JResponse: JsonObject): Boolean
var
	HttpClient: HttpClient;
	RequestHeaders: HttpHeaders;
	ContentHeaders: HttpHeaders;
	ReqHttpContent: HttpContent;
	ResHttpResponseMessage: HttpResponseMessage;
	ContentTypeValues: array[1024] of Text;
	JsonText: Text;
	Url: Text;
begin
	RequestHeaders := HttpClient.DefaultRequestHeaders();
	RequestHeaders.Add('Ocp-Apim-Subscription-Key', SubscriptionKeyLbl);

	JTimeSeriesData.WriteTo(JsonText);
	ReqHttpContent.WriteFrom(JsonText);
	ReqHttpContent.GetHeaders(ContentHeaders);
	if ContentHeaders.GetValues('Content-Type', ContentTypeValues) then
		ContentHeaders.Remove('Content-Type');
	ContentHeaders.Add('Content-Type', 'application/json');

	Url := StrSubstNo(RequestUrlLbl, EndpointLbl);
	if not HttpClient.Post(Url, ReqHttpContent, ResHttpResponseMessage) then
		exit;

	if not ResHttpResponseMessage.IsSuccessStatusCode() then
		exit;

	ResHttpResponseMessage.Content.ReadAs(JsonText);
	JResponse.ReadFrom(JsonText);
	exit(true);
end;</code></pre>
<p><strong>AnomalyValuesToDataSet</strong> method reads the API response, populates the anomaly data in a Dictionary variable and returns it.</p><pre><code>local procedure AnomalyValuesToDataSet(JResponse: JsonObject; TimeSeriesData: Dictionary of [Date, Decimal]; AnomalyData: Dictionary of [Date, Decimal]): Boolean
var
	JAnomalyValues: JsonArray;
	JToken: JsonToken;
	Index: Integer;
	PostingDate: Date;
	Amount: Decimal;
begin
	if not JResponse.Get('isAnomaly', JToken) then
		exit(false);

	JAnomalyValues := JToken.AsArray();
	foreach JToken in JAnomalyValues do begin
		Index += 1;

		if JToken.AsValue().AsBoolean() then begin
			TimeSeriesData.Keys.Get(Index, PostingDate);
			TimeSeriesData.Get(PostingDate, Amount);
			AnomalyData.Add(PostingDate, Amount);
		end;
	end;

	exit(true);
end;</code></pre><h2>Conclusion</h2><p>Azure Cognitive Service is the one which provides artificial intelligence to business applications like Business Central. Obviously, it is more convenient to have a system that can detect potential threats automatically, instead of analyzing data manually. Azure Cognitive Service fulfills that need.</p>

<p>Source code can be downloaded from <a href="https://github.com/msnraju/al-anomaly-detector" target="_blank" rel="noopener"><u>GitHub</u></a>.</p>

<p>Happy Coding!!!</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral #DynamicsNAV #AzureCognitiveService #AzureAnomalyDetector</p>]]></content:encoded></item><item><title><![CDATA[Analyze data using Query Analyzer in Business Central]]></title><description><![CDATA[Inline Query Analyzer is a tool to analyze data in Business Central. This is an open source project intended to help developers for quick...]]></description><link>https://www.msnjournals.com/post/analyze-data-using-query-analyzer-in-business-central</link><guid isPermaLink="false">5fb77d8b518fc40017289c6b</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[Performace]]></category><pubDate>Fri, 20 Nov 2020 11:01:20 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_ed51f9270d5b4797aded5669b82fec4d~mv2.gif/v1/fit/w_1000,h_652,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Inline Query Analyzer is a tool to analyze data in Business Central. This is an open source project intended to help developers for quick retrieval of data for analysis purposes. You can write SQL like queries to retrieve data. Users can view the query results in Grid and JSON views.</p>
<h2>How to install?</h2><p>This is an open source AL Project, source code can be downloaded from <a href="https://github.com/msnraju/inline-query" target="_blank" rel="noopener"><u>https://github.com/msnraju/inline-query</u></a>. This tool is using Object IDs from 50100 to 50149, you may need to renumber the objects, if that Object ID range is already used. </p>
<h2>Want to contribute?</h2><p>Anyone can contribute to this project by submitting Pull Request at <a href="https://github.com/msnraju/inline-query" target="_blank" rel="noopener"><u>https://github.com/msnraju/inline-query</u></a>. Submitting Ideas and reporting bugs is also a good way of contributing. </p>
<h2>Demo</h2><p>SELECT c1, c2 FROM t</p>
<p>Query aggregated data in columns c1, c2 from a table </p><figure><img src="https://static.wixstatic.com/media/394025_1066821acd4d47a787eaca88690d3825~mv2.png/v1/fit/w_980,h_547,al_c,q_80/file.png"  ></figure><h3>SELECT c1, c2 FROM t</h3><p>Query data in columns c1, c2 from a table </p><figure><img src="https://static.wixstatic.com/media/394025_b3201c179b21493bb4e11d364d6f1229~mv2.png/v1/fit/w_971,h_527,al_c,q_80/file.png"  ></figure><h3>SELECT * FROM t</h3><p>Query all rows and columns from a table </p><figure><img src="https://static.wixstatic.com/media/394025_ed3449c837fb47939e4be3bd237b2c93~mv2.png/v1/fit/w_977,h_522,al_c,q_80/file.png"  ></figure><h3>SELECT c1, c2 FROM t WHERE condition</h3><p>Query data and filter rows with a condition from a table</p><figure><img src="https://static.wixstatic.com/media/394025_845d27f06afa4590bb8987814e6099b3~mv2.png/v1/fit/w_979,h_527,al_c,q_80/file.png"  ></figure><h3>SELECT TOP n c1, c2 FROM t WHERE condition</h3><p>Query top n filter rows, columns c1, c2 with a condition from a table </p><figure><img src="https://static.wixstatic.com/media/394025_2321626ec91345e5817b4b9fd4acf600~mv2.png/v1/fit/w_979,h_523,al_c,q_80/file.png"  ></figure><h3>SELECT TOP n c1, c2 FROM t ORDER BY c3</h3><p>Query top n rows sorted by column 3, in columns c1, c2 from a table </p><figure><img src="https://static.wixstatic.com/media/394025_b9e5cf92a398405eac63f561956c8dce~mv2.png/v1/fit/w_982,h_531,al_c,q_80/file.png"  ></figure><p><strong><em>Not supported in the current version:</em></strong></p>
<p><em>GROUP BY, HAVING, DISTINCT, JOIN, UNION</em></p>
<h2>Conclusion</h2><p>Inline Query is a library with a small compiler written in AL to support SQL like queries in Business Central. Inline Query Analyzer is a page that uses Inline Query library to execute SQL like queries in Business Central. In the Online / SAAS environment we do not have access to the SQL database. Using this tool, SQL like queries can be executed in SAAS environment without any access to the SQL database.</p>

<p>See my <a href="https://www.msnjournals.com/post/inline-query-sql-like-queries-in-business-central" target="_blank" rel="noopener"><u>previous post</u> </a>for more details on how to use Inline Query in AL Language.</p>

<p>Source code can be downloaded from <a href="https://github.com/msnraju/inline-query" target="_blank" rel="noopener"><u>GitHub</u></a>. Issues can be reported at <a href="https://github.com/msnraju/inline-query/issues" target="_blank" rel="noopener"><u>https://github.com/msnraju/inline-query/issues</u></a>.</p>
<p>#MsDyn365 #MsDyn365BC #BusinessCentral #DynamicsNAV #InlineQuery</p>]]></content:encoded></item><item><title><![CDATA[Inline Query | SQL like Queries in Business Central]]></title><description><![CDATA[Inline Query is a library that can execute SQL like Queries in Business Central AL Language. This is a small compiler in AL that compiles...]]></description><link>https://www.msnjournals.com/post/inline-query-sql-like-queries-in-business-central</link><guid isPermaLink="false">5fac828e8b322b001781ec3b</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[Performace]]></category><pubDate>Thu, 12 Nov 2020 08:38:07 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_aff5c90fb69e44bca25b764cee407994~mv2.png/v1/fit/w_1000,h_720,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Inline Query is a library that can execute SQL like Queries in Business Central AL Language. This is a small compiler in AL that compiles and executes SQL like queries in text constants or text variables. </p>
<h2>Query Object vs Inline Query</h2><p>Inline Query is not a replacement for Query Objects. At present, Inline Query can only support retrieval of a single column from a single table (joins are not supported). Whereas Query Object supports multiple tables and multiple columns.</p>
<h2>Why Inline Query?</h2><p>I (MSN Raju) have created this library just to try something new. Actually, there is no purpose when I started writing this, but now I can give some use cases. </p>

<p>The following are the benefits:</p><ul>
  <li>Dynamic - Inline Queries can compose and run at the runtime. </li>
  <li>No Record Variables - No need to declare record variables, it is just a simple Text Query.</li>
  <li>Readability - SQL is like simple English and it is easy to read. Example: select sum(Amount) from [G/L Entry] where [No.] = 'CASH' and [Document Type] = 'Payment'. Anybody can understand what this line is about.</li>
  <li>No of Lines - To write the same above query in AL, at least 3 lines of code should be written. Whereas, for Inline Query, it is a single line statement.</li>
</ul><p>Limitations / Disadvantages:</p><ul>
  <li>It compiles the Query Text every time before executing. This is not good for performance. </li>
  <li>FlowFields, multiple columns, and ORDER BY are not supported.</li>
  <li>Only SELECT queries are supported.</li>
</ul><h2>Query Syntax</h2>
<p>SELECT [TOP n] FUNCTION(<Field Name>) | <Field Name> [AS <Column Name>]  FROM [<Company Name>.]<Table Name> [ WHERE <Field Name> OPERATOR <Filter Value> [ AND <Field Name> OPERATOR <Filter Value>] ]</p>
<h3>Company Name</h3><p>Name of the company from which data is to be retrieved, and this is optional. If there are any spaces or special characters in the company name, then it should be enclosed with brackets.</p>
<h3>Table Name</h3><p>Name of the table from which data is to be retrieved. If there are any spaces or special characters in the table name, then it should be enclosed with brackets.</p>
<h3>Field Name</h3><p>Name of the field in the source table. If there are any spaces or special characters in the field name, then it should be enclosed with brackets.</p>
<h3>FUNCTION (Aggregate Function)</h3><p>The following aggregate functions are supported in Inline Quaries:</p><ul>
  <li>Count - To get a no. of records in the table after applying filters</li>
  <li>Min - To get minimum value of the field in the table after applying filters</li>
  <li>Max - To get maximum value of the field in the table after applying filters</li>
  <li>Avg - To get average value of the field in the table after applying filters</li>
  <li>Sum - To get sum of the field values in the table after applying filters</li>
  <li>First - To get the first value of the field in the table after applying filters</li>
  <li>Last - To get the last value of the field in the table after applying filters</li>
</ul><h3>OPERATOR</h3><p>The following operators are supported in Inline Queries:</p><ul>
  <li>Equal to: <strong>= </strong></li>
  <li>Less than: <strong><</strong></li>
  <li>Less than or equal to: <strong><=</strong></li>
  <li>Greater than:<strong> ></strong></li>
  <li>Greater than or equal to: <strong>>=</strong></li>
  <li>Not equal to:  <strong><></strong></li>
  <li>Like: <strong>LIKE</strong> </li>
</ul><p>Note: <em>For LIKE - Field Filter expressions are accepted as Filter Value. (Internally uses SetFilter statement)</em></p>
<h3>Filter Value</h3><p>The following are the possible Filter Values:</p><ul>
  <li>BOOLEAN: true or false can be use as filter value to apply filter on Boolean field.</li>
  <li>NUMBER: Integer and Decimal values are accepted as filter values to apply filter on Number type fields.</li>
  <li>TEXT CONST: Text constant values should always be wrapped with single quotes. Text Constant should be used to apply filter on Text, Code, Date, Time, DateTime, Option, Enum type fields. </li>
</ul><p>Filter Value for LIKE operator should be a Text Constant and Filter Value should be in regional format for Date, Time and DateTime fields. </p>
<h2>Inline Query Codeunit</h2><p>This codeunit contains methods that can execute Inline Queries. It has the following methods:</p><ul>
  <li>AsInteger</li>
</ul><p>Input: Query Text </p>
<p>Returns: Integer vaue</p><ul>
  <li>AsDecimal</li>
</ul><p>Input: Query Text </p>
<p>Returns: Decimal value.</p><ul>
  <li>AsBigInteger </li>
</ul><p>Input: Query Text </p>
<p>Returns: BigInteger value.</p><ul>
  <li>AsDate </li>
</ul><p>Input: Query Text</p>
<p>Returns: Date value</p><ul>
  <li>AsTime </li>
</ul><p>Input: QueryText </p>
<p>Returns: Time value. </p><ul>
  <li>AsDateTime - </li>
</ul><p>Input: Query Text </p>
<p>Returns: DateTime value. </p><ul>
  <li>AsBoolean - </li>
</ul><p>Input: Query Text </p>
<p>Returns: Boolean value. </p><ul>
  <li>AsText </li>
</ul><p>Input: Query Text </p>
<p>Returns: Text value. </p><ul>
  <li>AsCode </li>
</ul><p>Input: Query Text</p>
<p>Returns: Boolean value. </p><ul>
  <li>AsJsonArray</li>
</ul><p>Input: Query Text</p>
<p>Returns: JsonArray</p>

<p>Note: <em>SUM and AVG aggregate functions are not applicable to AsDate, AsTime and AsDateTime methods. Also, FIRST and LAST aggregate functions are the only functions that can be used in AsBoolean, AsText and AsCode methods.</em></p>
<h2>Examples</h2>
<p>1) Count of released Sales Orders</p>

<p>Query</p><pre><code>SELECT COUNT(1) FROM [Sales Header] WHERE Status = 'Released'</code></pre><p>AL Code</p><pre><code>procedure GetOrderCount(): Integer
var
	InlineQuery: Codeunit "Inline Query";
	OrderCount: Integer;
	QueryTxt: Label 'SELECT COUNT(1) FROM [Sales Header] WHERE Status = ''Released''', Locked = true;
begin
	OrderCount := InlineQuery.AsInteger(QueryTxt);
	exit(OrderCount);
end;</code></pre>
<p>2) Sales Order's total "Amount including VAT" for a particular order.</p>

<p>Query</p><pre><code>SELECT SUM([Amount Including VAT]) FROM [Sales Line] WHERE [Document Type] = 'Order' AND [Document No.]='ORD001'</code></pre><p>AL Code</p><pre><code>local procedure GetOrderAmount(): Decimal
var
	InlineQuery: Codeunit "Inline Query";
	OrderAmount: Integer;
	QueryTxt: Label 'SELECT SUM([Amount Including VAT]) FROM [Sales Line] WHERE [Document Type] = ''Order'' AND [Document No.] = ''ORD001''', Locked = true;
begin
	OrderAmount := InlineQuery.AsDecimal(QueryTxt);
	exit(OrderAmount);
end;</code></pre><p>3) Average Order value per Sales Line:</p>

<p>Query</p><pre><code>SELECT AVG([Amount Including VAT]) FROM [Sales Line] WHERE [Document Type] LIKE 'Order|Invoice'</code></pre><p>AL Code</p><pre><code>local procedure GetAverageValue(): Decimal
var
	InlineQuery: Codeunit "Inline Query";
	OrderAmount: Integer;
	QueryTxt: Label 'SELECT AVG([Amount Including VAT]) FROM [Sales Line] WHERE [Document Type] LIKE ''Order|Invoice''', Locked = true;
begin
	OrderAmount := InlineQuery.AsDecimal(QueryTxt);
	exit(OrderAmount);
end;</code></pre><h2>Conclusion</h2><p>This is an example on how AL Language can be extended. Though I have written tests for this library with 90%+ code coverage, but still It has to go through tough testing for production use. And most importantly compiled queries should be cached for better performance. I will implement the same in the next version.</p>

<p>You can find source code at <a href="https://github.com/msnraju/inline-query" target="_blank" rel="noopener"><u>GitHub</u></a>.</p>

<p>Happy Coding!!!</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral #DynamicsNAV #InlineQuery</p>]]></content:encoded></item><item><title><![CDATA[Facade Pattern in Business Central - AL Language]]></title><description><![CDATA[Facade Pattern is a Design Pattern to hide complexity of the multiple sub systems by providing a simple interface to the client. This...]]></description><link>https://www.msnjournals.com/post/facade-pattern-in-business-central-al-language</link><guid isPermaLink="false">5f9e3ddd28ca5c001779f48f</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[Design Patterns]]></category><pubDate>Sun, 01 Nov 2020 15:18:24 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_204c67eb568c4700b45839fbbadd5fee~mv2.png/v1/fit/w_1000,h_653,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Facade Pattern is a Design Pattern to hide complexity of the multiple sub systems by providing a simple interface to the client. This pattern involves a single codeunit which provides simple methods required by the client and delegates call to methods of existing internal codeunits.</p>
<h2>Class Diagram</h2><p>Facade pattern class diagram:</p><figure><img src="https://static.wixstatic.com/media/394025_204c67eb568c4700b45839fbbadd5fee~mv2.png/v1/fit/w_1000,h_653,al_c,q_80/file.png"  ></figure><ul>
  <li>Subsystem: Each class / codeunit in Subsystems has some functionality that will be accessed by client using Facade.</li>
  <li>Facade: This will be the interface for client to access complex subsystem.</li>
  <li>Client: Client will access the subsystems using Facade</li>
</ul>
<h2>Facade: Sample Scenario</h2><p>Let's take a simple scenario. Create a facade codeunit which will wrap the functionality to search an item by name, and to search a resource by name in two different codeunits (internal / subsystem). </p>
<h3>Subsystem (ItemSearchImpl.Codeunit.al)</h3><p>This is an internal codeunit. Methods in this codeunit can be accessed only within the current project / module. Internal access will ensure that it is not exposed to the Client.</p>
<pre><code>codeunit 50130 "Item Search Impl"
{
    Access = Internal;

    procedure SearchItems(SearchText: Text[100]; Items: List of [Text[100]])
    var
        Item: Record Item;
    begin
        Item.SetFilter(Description, '@*' + SearchText + '*');
        if Item.FindSet() then
            repeat
                Items.Add(Item.Description);
            until Item.Next() = 0;
    end;
}</code></pre><h3>Subsystem (ResourceSearchImpl.Codeunit.al)</h3><pre><code>codeunit 50131 "Resource Search Impl"
{
    Access = Internal;

    procedure SearchResource(SearchText: Text[100]; Resources: List of [Text[100]])
    var
        Resource: Record Resource;
    begin
        Resource.SetFilter(Name, '@*' + SearchText + '*');
        if Resource.FindSet() then
            repeat
                Resources.Add(Resource.Name);
            until Resource.Next() = 0;
    end;
}
</code></pre><h3>Facade codeunit (ItemResourceSearch.Codeunit.al)</h3><p>This is a Facade codeunit which provides access to the functionalities in "Item Search Impl" and "Resource Search Impl" codeunits to the Client.</p><pre><code>codeunit 50132 "Item / Resource Search"
{
    Access = Public;
    
    procedure SearchItems(SearchText: Text[100]; Items: List of [Text[100]])
    var
        ItemSearchImpl: Codeunit "Item Search Impl";
    begin
        ItemSearchImpl.SearchItems(SearchText, Items);
    end;

    procedure SearchResource(SearchText: Text[100]; Resources: List of [Text[100]])
    var
        ResourceSearchImpl: Codeunit "Resource Search Impl";
    begin
        ResourceSearchImpl.SearchResource(SearchText, Resources);
    end;
}</code></pre><h2>Module Architecture</h2><p>Business Central Module Architecture has some special requirements. See <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-blueprint#facade-codeunits" target="_blank" rel="noopener"><u>Facade codeunit rules</u></a>.</p>
<h2>Conclusion</h2><p>Using this Design Pattern, we can simplify the APIs of Business Central extension / module / project. The Facade codeunit should not be modified and should only be extended.</p>

<p>Happy Coding!!!</p>

<p>Source Code can be downloaded from <a href="https://github.com/msnraju/design-patterns" target="_blank" rel="noopener"><u>GitHub</u></a></p>

<p><a href="https://www.msnjournals.com/home/search/.hash.msdyn365" target="_blank" rel="noopener">#MSDyn365</a> <a href="https://www.msnjournals.com/home/search/.hash.msdyn365bc" target="_blank" rel="noopener">#MSDyn365BC</a> <a href="https://www.msnjournals.com/home/search/.hash.businesscentral" target="_blank" rel="noopener">#BusinessCentral</a> <a href="https://www.msnjournals.com/home/search/.hash.dynamicsnav" target="_blank" rel="noopener">#DynamicsNAV</a> <a href="https://www.msnjournals.com/home/search/.hash.designpatterns" target="_blank" rel="noopener">#DesignPatterns</a></p>]]></content:encoded></item><item><title><![CDATA[Basics of JSON data types in Business Central]]></title><description><![CDATA[Understand the basics of JSON data types and how to handle JSON data in Business Central AL Language using JSON data types.]]></description><link>https://www.msnjournals.com/post/basics-of-json-data-types-in-business-central</link><guid isPermaLink="false">5f9a7eeaf796df0017d3caff</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><pubDate>Fri, 30 Oct 2020 08:12:26 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_fa0633a47df445a59616eda2ce3a4375~mv2.png/v1/fit/w_690,h_330,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>This post is about understanding the basics of JSON and how to handle JSON data in Business Central AL Language using JSON data types. JSON data types are introduced in AL Language with Business Central / Microsoft Dynamics NAV 2018.  JSON stands for <strong>J</strong>ava<strong>S</strong>cript <strong>O</strong>bject <strong>N</strong>otation. The JSON format is a lightweight data interchange format originally specified by <a href="http://www.crockford.com/" target="_blank" rel="noopener">Douglas Crockford</a>. </p>
<h2>Why JSON?</h2><p>JSON is text only, easy to read, easy to understand and lightweight format to transfer the data between applications or from the client to the server (Web APIs / Web Services). JSON files are also used to maintain configuration data for example AL project's app.json, vscode's launch.json, settings.json are the JSON configuration files.</p>
<h2>JSON Data types</h2><p>Following are the data types supported in JSON Objects. An Object or an Array can be root of the JSON file / document.</p><ul>
  <li>JsonObject</li>
  <li>JsonArray</li>
  <li>String</li>
  <li>Number</li>
  <li>Boolean</li>
  <li>Null</li>
</ul><p>Primitive data types supported in JSON format are String (Text, Code), Number (Integer, Decimal), and Boolean data types only. Because JavaScript supports String, Number and Boolean data types only, all other types are Object types in JavaScript. To support other complex types, they are serialized into text.</p>

<p>Example: the following is a Date type property, but it is formatted as String. But the value can be retrieved as Date data type by using JsonValue.AsDate() method.</p><pre><code>  "orderDate": "2022-01-21"</code></pre><h2>JsonObject</h2><p>JSON objects are always enclosed with curly braces, and it can contain one or more properties separated by comma.</p>

<p><strong>Property / Attribute / Member</strong></p>
<p>JsonObject can only contain properties, JSONProperty has key value pair syntax. Key values are separated by colon symbol (:). The property key should be a string enclosed with double quotes ("), and the property value can be of any data type supported by JSON.</p>

<p>In the below example "name" is the key, and "Business Central" is the value. </p><pre><code>"name": "Business Central"</code></pre><p>JsonObject with single property</p><pre><code>{
	"name":  "Business Central"
}</code></pre><p>JsonObject with multiple properties</p><pre><code>{
	"name":  "Business Central",
	"version": 16.1,
	"company": "Microsoft",
	"isOnPrem": false
}</code></pre><h2>JsonArray</h2><p>Array is a set of values where the value can be of any data type. </p>

<p>In the below example, "numbers" property value is a string array, "odds" property value is number array, and "customers" property value is a array of objects.</p><pre><code>{
	"numbers":  ["One", "Two", "Three"],
	"odds": [1, 3, 5, 7],
	"customers": [
		{ "name": "Nike" },
		{ "name": "Adidas" },
		{ "name": "Puma" },
	]
}</code></pre><h2>String, Number, Boolean</h2><p>String values should be enclosed with double quotes. In the below example "Business Central" is a string type value.</p><pre><code>"name":  "Business Central"</code></pre><p>Number can be an integer value or a decimal value. In the below example 43 and 5.8 are the number type values. </p><pre><code>"age":  43,
"height": 5.8</code></pre><p>A Boolean value should be either true or false. In the below example false is the Boolean type value. </p><pre><code>"isOnPrem": false</code></pre><h2>JsonValue</h2><p>JsonValue variable is a joker type variable, it can contain value of any primitive data type like Text, Code, Integer, Decimal, Date, Date Time Date Formula etc. </p>
<h2>JsonToken</h2><p>JsonToken variable is a joker type variable, it can contain JsonObject, JsonArray, or JsonValue variable. It is like Variant data type for JSON variables. </p>
<h2>Null</h2><p>Null values are used when there is no value. Null means nothing or unknown, null doesn’t mean false or zero or empty. In the below the example "modifiedOn" is null. </p><pre><code>"modifiedOn": null</code></pre><h2>Useful Facts</h2><ul>
  <li>In AL by default JsonObject, JsonArray variables are auto initialized. To assign a new instance to the same variable, "Clear" method should be used.</li>
  <li>In AL by default JsonValue, JsonToken variables are initialized to null.</li>
  <li>All JSON variables are reference types (pointer). This means even method parameter is not defined as a reference type (var), but still it is treated as reference type. This also means, if a JSON variable is modified anywhere in the code flow (anywhere in the call stack) the original variable is modified. </li>
</ul><h2>Construction of JsonObject </h2><p>The following code is an example of how to create JSON Object. The JsonObject (JSalesOrder) of a Sales Order is returned after adding multiple properties to it.</p><pre><code>local procedure SalesOrderToJson(SalesHeader: Record "Sales Header"): JsonObject
var
	JSalesOrder: JsonObject;
begin
	// Sales Order Properties
	JSalesOrder.Add('orderNo', SalesHeader."No.");
	JSalesOrder.Add('orderDate', SalesHeader."Order Date");
	JSalesOrder.Add('sellToCustomerNo', SalesHeader."Sell-to Customer No.");
	JSalesOrder.Add('amountIncludingVAT', SalesHeader."Amount Including VAT");
	JSalesOrder.Add('isApprovedForPosting', SalesHeader.IsApprovedForPosting());
	JSalesOrder.Add('lines', SalesLinesToJson(SalesHeader));
	exit(JSalesOrder);
end;</code></pre><h2>Construction of JsonArray </h2><p>The following code is an example of how to create or add multiple items to a JsonArray. The JsonArray (JSalesLines) of multiple Sales Lines JsonObjects are returned. </p><pre><code>local procedure SalesLinesToJson(SalesHeader: Record "Sales Header"): JsonArray
var
	SalesLine: Record "Sales Line";
	JSalesLines: JsonArray;
begin
	SalesLine.SetRange("Document Type", SalesHeader."Document Type");
	SalesLine.SetRange("Document No.", SalesHeader."No.");
	if SalesLine.FindSet then
		repeat
			AddSalesLineToJson(SalesLine, JSalesLines);
		until SalesLine.Next() = 0;

	exit(JSalesLines);
end;

local procedure AddSalesLineToJson(SalesLine: Record "Sales Line"; JSalesLines: JsonArray)
var
	JSalesLine: JsonObject;
begin
	// Sales Line Attributes
	JSalesLine.Add('type', SalesLine.Type.AsInteger());
	JSalesLine.Add('no', SalesLine."No.");
	JSalesLine.Add('quantity', SalesLine.Quantity);
	JSalesLine.Add('unitPrice', SalesLine."Unit Price");
	JSalesLine.Add('amount', SalesLine."Line Amount");

	JSalesLines.Add(JSalesLine);
end;</code></pre>
<p>Sample Output of a Sales Order JSON Object in text format of the above code:</p><pre><code>{
  "orderNo": "101009",
  "orderDate": "2022-01-21",
  "sellToCustomerNo": "38128456",
  "amountIncludingVAT": 2887.11,
  "isApprovedForPosting": true,
  "lines": [
    {
      "type": 2,
      "no": "1976-W",
      "quantity": 5.0,
      "unitPrice": 396.562,
      "amount": 1982.81
    },
    {
      "type": 2,
      "no": "1964-W",
      "quantity": 2.0,
      "unitPrice": 452.152,
      "amount": 904.3
    }
  ]
}</code></pre><h2>Read JsonObject</h2><p>The following code is an example of how to read properties from a JsonObject. It is reading properties of the Sales Order JsonObject (JSalesOrder).</p><pre><code>local procedure ReadSalesOrderJson(JSalesOrder: JsonObject; SalesHeader: Record "Sales Header")
var
	JOrderNoToken: JsonToken;
	JOrderDateToken: JsonToken;
	JSellToCustomerNoToken: JsonToken;
	JLinesToken: JsonToken;
	JLinesArray: JsonArray;
begin
	if JSalesOrder.Get('orderNo', JOrderNoToken) then
		SalesHeader."No." := JOrderNoToken.AsValue().AsCode();

	if JSalesOrder.Get('orderDate', JOrderDateToken) then
		SalesHeader."Order Date" := JOrderDateToken.AsValue().AsDate();

	if JSalesOrder.Get('sellToCustomerNo', JSellToCustomerNoToken) then
		SalesHeader."Sell-to Customer No." := JSellToCustomerNoToken.AsValue().AsCode();

	if JSalesOrder.Get('lines', JLinesToken) then begin
		JLinesArray := JLinesToken.AsArray(); // Array of Objects
		ReadSalesLinesJson(JLinesArray, SalesHeader);
	end;
end;</code></pre><h2>Read JsonArray</h2><p>The following code is an example on how to read Items from JsonArray. It reads Sales Lines from JsonArray (JSalesLines).</p><pre><code>local procedure ReadSalesLinesJson(JSalesLines: JsonArray; SalesHeader: Record "Sales Header")
var
	SalesLine: Record "Sales Line";
	JSalesLineToken: JsonToken;
	JSalesLine: JsonObject;

	JTypeToken: JsonToken;
	JNoToken: JsonToken;
	JQuantityToken: JsonToken;
begin
	foreach JSalesLineToken in JSalesLines do begin
		JSalesLine := JSalesLineToken.AsObject();
		SalesLine."Document Type" := SalesHeader."Document Type";
		SalesLine."Document No." := SalesHeader."No.";

		if JSalesLine.Get('type', JTypeToken) then
			SalesLine.Type := "Sales Line Type".FromInteger(JTypeToken.AsValue().AsInteger());

		if JSalesLine.Get('no', JNoToken) then
			SalesLine."No." := JNoToken.AsValue().AsCode();

		if JSalesLine.Get('quantity', JQuantityToken) then
			SalesLine.Quantity := JQuantityToken.AsValue().AsDecimal();
	end;
end;</code></pre><h2>Conclusion</h2><p>Though it is not a great post, you can find several posts explaining everything about JSON, but I still feel there is a need of this post so that Business Central developers can understand the basics of JSON in their language. I saw many developers using JSON data types without understanding the basics (mostly copy, and paste). In my opinion, even if one has successfully completed an integration project using JSON documents etc. without knowing the fundamentals, it has no worth.</p>

<p>Never late to begin, learn the basics to be a better coder.</p>

<p>Happy Coding!!!</p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV</p>]]></content:encoded></item><item><title><![CDATA[The Rules Pattern in Business Central - AL Language]]></title><description><![CDATA[The Rules Pattern is a Design Pattern to simplify the complex business logic which is based on multiple if-else statements.]]></description><link>https://www.msnjournals.com/post/the-rules-pattern-in-business-central-al-language</link><guid isPermaLink="false">5f86c49e88c88c00188af8e6</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[Design Patterns]]></category><pubDate>Wed, 14 Oct 2020 15:48:11 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_7c83e8db28284253bbbedae43070a3a1~mv2.png/v1/fit/w_966,h_360,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>The Rules Pattern is a Design Pattern to simplify the complex business logic which is based on multiple if-else statements. Using this Design Pattern each if-else branch in the business logic is segregated into a separate rules and also the processing logic is separated from the rules.</p>
<h2>Class Diagram</h2><figure><img src="https://static.wixstatic.com/media/394025_7c83e8db28284253bbbedae43070a3a1~mv2.png/v1/fit/w_966,h_360,al_c,q_80/file.png"  ></figure><h2>Demo Scenario                                                                                                               </h2><p>To understand the Rules Design Pattern better, the following scenarios are being used:</p><ul>
  <li>If the Customer's "Customer Price Group" is 'GOLD' and the "Business Posting Group" is 'DOM' then 15% discount will be given, else if the "Business Posting Group" is other than 'DOM' 25% discount will be given.</li>
  <li>If the Customer's "Customer Price Group" is 'SILVER' and the "Business Posting Group" is 'DOM' then 10% discount will be given, else if the "Business Posting Group" is other than 'DOM' 15% discount will be given.</li>
</ul><h2>Rule Interface (DiscountRule.Interface.al)</h2><p>This is the implementation of Rule Interface for Discount Calculation. The method "Process" is going to return the Discount Percent based on conditions defined in the "CanProcess" method. If the "CanProcess" method returns false, it means there is no discount applicable for the given customer.</p><pre><code>interface DiscountRule
{
    procedure CanProcess(CustomerNo: Code[20]): Boolean;
    procedure Process(CustomerNo: Code[20]): Decimal;
}</code></pre><h2>Evaluator (DiscountEvaluator.Codeunit.al)</h2><p>The following logic calculates maximum possible discount. This evaluates the incoming Rule and updates the discount, if the previous discount is less than the current discount.</p><pre><code>codeunit 50106 DiscountEvaluator
{
    procedure Evaluate(DiscountRule: Interface DiscountRule; CustomerNo: Code[20]; var Discount: Decimal)
    var
        NewDiscount: Decimal;
    begin
        if not DiscountRule.CanProcess(CustomerNo) then
            exit;

        NewDiscount := DiscountRule.Process(CustomerNo);
        if NewDiscount > Discount then
            Discount := NewDiscount;
    end;
}</code></pre><h2>Rules</h2><p>End number of Discount Rules can be implemented, in this example two discount rules are being used. First Rule is for Gold Customers, and the second one for the Silver Customers.</p>
<h3>Rule One (GoldCustomerDiscountRule.Codeunit.al)</h3><pre><code>codeunit 50110 GoldCustomerDiscountRule implements DiscountRule
{
    procedure CanProcess(CustomerNo: Code[20]): Boolean;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        if Customer."Customer Price Group" = 'GOLD' then
            exit(true);
    end;

    procedure Process(CustomerNo: Code[20]): Decimal;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        case Customer."Gen. Bus. Posting Group" of
            'DOM':
                exit(0.15);
            else
                exit(0.25);
        end;
    end;
}</code></pre><h3>Rule Two (SilverCustomerDiscountRule.Codeunit.al)</h3><pre><code>codeunit 50111 SilverCustomerDiscountRule implements DiscountRule
{
    procedure CanProcess(CustomerNo: Code[20]): Boolean;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        if Customer."Customer Price Group" = 'SILVER' then
            exit(true);
    end;

    procedure Process(CustomerNo: Code[20]): Decimal;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        case Customer."Gen. Bus. Posting Group" of
            'DOM':
                exit(0.1);
            else
                exit(0.15);
        end;
    end;
}</code></pre>
<h2>Rule Engine / Processor  (DiscountCalculator.Codeunit.al)</h2><p>This is the main codeunit to calculate the Discount. This follows Open Closed Principle which means it is closed for modification and open for extension. </p>

<p>To modify the discount logic, there is no need to change anything in this codeunit. To modify any condition or the logic in the Discount Rule, the appropriate Discount Rule codeunit has to be modified. To add a new Discount Rule, a new codeunit can be created that implements the Rule Interface and it has to be attached to the Processor (in OnExecute subscriber). </p><pre><code>codeunit 50105 DiscountCalculator
{
    procedure Execute(CustomerNo: Code[20]): Decimal
    var
        DiscountEvaluator: Codeunit DiscountEvaluator;
        Discount: Decimal;
    begin
        OnExecute(DiscountEvaluator, CustomerNo, Discount);
        exit(Discount);
    end;

    [BusinessEvent(false)]
    local procedure OnExecute(DiscountEvaluator: Codeunit DiscountEvaluator; CustomerNo: Code[20]; var Discount: Decimal)
    begin
    end;
}</code></pre><h2>Rules Collection (DiscountRuleSubscriber.Codeunit.al)</h2><p>This codeunit will attach the available Discount Rules to the Processor (DiscountCalculator).</p><pre><code>codeunit 50112 DiscountRuleSubscriber
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::DiscountCalculator, 'OnExecute', '', false, false)]
    local procedure OnExecute(DiscountEvaluator: Codeunit DiscountEvaluator; CustomerNo: Code[20]; var Discount: Decimal)
    var
        GoldCustomerDiscountRule: Codeunit GoldCustomerDiscountRule;
        SilverCustomerDiscountRule: Codeunit SilverCustomerDiscountRule;
    begin
        DiscountEvaluator.Evaluate(GoldCustomerDiscountRule, CustomerNo, Discount);
        DiscountEvaluator.Evaluate(SilverCustomerDiscountRule, CustomerNo, Discount);
    end;
}</code></pre><h2>Conclusion</h2><p>Design Patterns represents the best practices used by the experienced developers to solve general problems. In my opinion, the objective and the solution are more important than the implementation itself. Design patterns can be implemented in different ways depending on the Language features and the capability of the developer. And the most important thing is design patterns should not be implemented forcefully when it is not necessary.</p>

<p>The Rules Pattern solves complexity of the business problem by dividing it into multiple classes / codeunits so that the code becomes readable and extendable for future enhancements.</p>
<p> </p>
<p>Happy Coding!!!</p>

<p>Source Code can be downloaded from <a href="https://github.com/msnraju/design-patterns" target="_blank" rel="noopener"><u>GitHub</u></a></p>

<p><em>Credits to </em><a href="http://ardalis.com/blog" target="_blank" rel="noopener"><em>Steve Smith</em></a><em> who is the originator of this design pattern. </em></p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #DesignPatterns</p>]]></content:encoded></item><item><title><![CDATA[Generic Validator Method Pattern ]]></title><description><![CDATA[Generic Validator Method Pattern is a design pattern which is an extension of Generic Method Pattern. Purpose of this design pattern is...]]></description><link>https://www.msnjournals.com/post/generic-validator-method-pattern</link><guid isPermaLink="false">5f8185f8f5ae940017c6fa35</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[Design Patterns]]></category><pubDate>Sun, 11 Oct 2020 07:49:03 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_eb9f67aa747c46de9a4d5ca744c5e544~mv2.png/v1/fit/w_921,h_514,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Generic Validator Method Pattern is a design pattern which is an extension of Generic Method Pattern. Purpose of this design pattern is to validate one or more entities in a specific context. This is a very commonly used pattern in other languages, just that it is adjusted to Generic Method Pattern.</p>
<h2>Generic Method Pattern</h2><p>It has a public method and two business events which can be subscribed to change or extend the behaviour of the method.</p>

<p>Public Method may contain the following sequence of calls:</p><ol>
  <li>UI Confirmation (Optional)</li>
  <li>OnBefore Business Event </li>
  <li>Execute Logic</li>
  <li>OnAfter Business Event</li>
  <li>UI Acknowledgement (Optional)</li>
</ol><p>Generic Method Pattern better explained in "<a href="https://www.youtube.com/watch?v=hauZJEz-GN0&#38;t=2544s" target="_blank" rel="noopener"><u>Design patterns for developing repeatable IP for Dynamics 365 Business Central</u></a><u>"</u></p>
<h2>Generic Validator Method Pattern</h2><p>It has two public methods and two business events which can be subscribed to change or extend the behaviour of the method.</p>

<p>This pattern contains HasErrors and Validate public functions.</p>
<h3>HasErrors Method</h3><p>This method will check validation errors in the entity and stores errors in Errors parameter and returns true if there are any validation errors. </p>

<p>This method should contain the following sequence of calls:</p><ol>
  <li>OnBefore Business Event </li>
  <li>Execute Logic</li>
  <li>OnAfter Business Event</li>
</ol><h3>Validate Method</h3><p>This method will throw a multi line error message with all validation errors found in HasErrors method.  </p>
<h2>Sample Code</h2><pre><code>codeunit 50101 CreateSalesOrderValidator
{
    procedure Validate(Customer: Record Customer)
    var
        Errors: List of [Text];
    begin
        if HasErrors(Customer, Errors) then
            Error(ErrorsToText(Errors));
    end;

    procedure HasErrors(Customer: Record Customer; Errors: List of [Text]): Boolean
    var
        Handled: Boolean;
    begin
        OnBeforeCreateSalesOrderValidator(Customer, Handled);
        DoCreateSalesOrder(Customer, Errors, Handled);
        OnAfterCreateSalesOrderValidator(Customer);

        if Errors.Count() > 0 then
            exit(true);
    end;

    local procedure ErrorsToText(Errors: List of [Text]): Text
    var
        ErrText: Text;
        TxtBuffer: TextBuilder;
    begin
        foreach ErrText in Errors do
            TxtBuffer.AppendLine(ErrText);

        exit(TxtBuffer.ToText());
    end;

    local procedure DoCreateSalesOrder(Customer: Record Customer; Errors: List of [Text]; Handled: Boolean)
    begin
        if Handled then
            exit;

        ValidateGenBusinessPostingGroup(Customer, Errors);
        ValidateVATBusPostingGroup(Customer, Errors);
        ValidateVATRegistrationNo(Customer, Errors);
    end;

    local procedure ValidateGenBusinessPostingGroup(Customer: Record Customer; Errors: List of [Text])
    var
        GenBusPostingGroupErr: Label 'Gen. Bus. Posting Group should not be empty.';
    begin
        if Customer."Gen. Bus. Posting Group" = '' then
            Errors.Add(GenBusPostingGroupErr);
    end;

    local procedure ValidateVATBusPostingGroup(Customer: Record Customer; Errors: List of [Text])
    var
        GenBusPostingGroupErr: Label 'VAT Bus. Posting Group should not be empty.';
    begin
        if Customer."VAT Bus. Posting Group" = '' then
            Errors.Add(GenBusPostingGroupErr);
    end;

    local procedure ValidateVATRegistrationNo(Customer: Record Customer; Errors: List of [Text])
    var
        GenBusPostingGroupErr: Label 'Customer should have a valid VAT Registration No.';
    begin
        if Customer."VAT Registration No." = '' then
            Errors.Add(GenBusPostingGroupErr);
    end;

    [BusinessEvent(false)]
    local procedure OnBeforeCreateSalesOrderValidator(Customer: Record Customer; var Handled: Boolean)
    begin
    end;

    [BusinessEvent(false)]
    local procedure OnAfterCreateSalesOrderValidator(Customer: Record Customer)
    begin
    end;
}</code></pre><h2>Use Cases</h2><p>This pattern can be used in the following senarios:</p><ul>
  <li>Postings, Actions</li>
</ul><p>		Validate Transactional Entities before posting or before performaning an action.</p><ul>
  <li>APIs, Web Services</li>
</ul><p>		Validating Entities before importing or accepting data via APIs or Web Services.</p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #DesignPatterns </p>]]></content:encoded></item><item><title><![CDATA[How to use ReactJS Components in Control Add-ins]]></title><description><![CDATA[Control Add-in is an Object type in Business Central to develop User Controls using JavaScript, and CSS. Though it is a very good option...]]></description><link>https://www.msnjournals.com/post/how-to-use-reactjs-components-in-control-add-ins</link><guid isPermaLink="false">5f51bd808ec7190017e6ebc0</guid><category><![CDATA[AL Language]]></category><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><pubDate>Fri, 04 Sep 2020 10:28:32 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_84c7aa3b9d51481eb3d041e3f25ad140~mv2.gif/v1/fit/w_1000,h_577,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Control Add-in is an Object type in Business Central to develop User Controls using JavaScript, and CSS. Though it is a very good option to enhance UI / UX in Business Central, we often have to struggle to build HTML markup in JavaScript, because we have to use string concatenate functions, or complex DOM methods build HTML elements. ReactJS library can give us the flexibility to write HTML markup inside JavaScript.</p>
<h2>About ReactJS</h2><p>ReactJS is a JavaScript library developed by the Facebook team to develop single page applications. ReactJS uses a new file type called JSX (JavaScript and Xml) which allows to write HTML markup in JavaScript (this is the exact requirement). Using npm scripts ReactJS application can be transpiled into JavaScript and CSS files.</p>

<p>A Simple React Component:</p><pre><code>class HelloMessage extends React.Component {
  render() {
    return (
      <div>
        Hello {this.props.name}
      </div>
    );
  }
}

ReactDOM.render(
  <HelloMessage 
name="Taylor" />,
  document.getElementById('hello-example')
);
</code></pre><h2>ReactJS Application</h2><p>A new ReactJS application can be created using the following npx command. npx is a node package runner.  You must install <a href="https://nodejs.org/en/download/" target="_blank" rel="noopener"><u>node.js</u></a> before executing this command.</p><pre><code>npx create-react-app my-app</code></pre><p>The following command build the ReactJS application and creates a build folder with transpiled JavaScript, and CSS files.</p><pre><code>npm run build</code></pre><p>build folder:</p><pre><code>  330.3 KB  build\static\js\3.2860d5eb.chunk.js
  19.96 KB  build\static\js\0.3e8005e8.chunk.js
  13.43 KB  build\static\js\4.c188f31c.chunk.js
  11.62 KB  build\static\css\3.fb09108b.chunk.css
  6.76 KB   build\static\js\5.eb238462.chunk.js
  5.16 KB   build\static\js\6.1a36259c.chunk.js
  1.21 KB   build\static\js\runtime-main.386c5c00.js
  856 B     build\static\js\main.da43e367.chunk.js
  584 B     build\static\js\7.3093821e.chunk.js
  214 B     build\static\css\main.b2c4a7d2.chunk.css</code></pre><h2>Bundle JS, CSS files using Gulp</h2><p>Gulp is a JavaScript task runner. Using Gulp, multiple JavaScript and CSS files can be bundled into a single JavaScript, and into a single CSS file.</p>

<p>install the following packages:</p><pre><code>npm install --save-dev gulp
npm install --save-dev gulp-clean-css
npm install --save-dev gulp-concat</code></pre><p>"gulp-clean-css",  "gulp-concat" are the gulp plugins to clean CSS files and concatenate files respectively.</p>
<h3>gulpfile.js</h3><p>Create "gulpfile.js" file in React Application's root folder. This takes JavaScript and CSS files from the build folder and bundles them into a single JavaScript and CSS files and saves in Control Add-in folder.</p>

<p><strong>ControlAddInFolder</strong>: is the relative path to Control Add-in AL project folder.</p><pre><code>var gulp = require('gulp');
var concat = require('gulp-concat');
var cleanCss = require('gulp-clean-css');

const ControlAddInFolder = '../app/src/ContentEditor';
const ControlAddInName = 'content-editor';

gulp.task('pack-js', function () {
    return gulp.src(['build/static/js/*.js'])
        .pipe(concat(`${ControlAddInName}.js`))
        .pipe(gulp.dest(`${ControlAddInFolder}/js`));
});

gulp.task('pack-css', function () {
    return gulp.src(['build/static/css/*.css'])
        .pipe(concat(`${ControlAddInName}.css`))
        .pipe(cleanCss())
        .pipe(gulp.dest(`${ControlAddInFolder}/css`));
});

gulp.task('default', gulp.series('pack-js', 'pack-css'));</code></pre><h2>Update build script in package.json</h2><p>Update "package.json" to execute gulp after completing build automatically.</p>

<p>"react-scripts build && gulp" :- will execute gulp (gulpfile.js) after being build.</p><pre><code> "scripts": {
 "start": "react-scripts start",
 "build": "react-scripts build && gulp"
  },</code></pre><h2>ReactJS Component (ContentEditor.js)</h2><p>The following React component renders "wix-rich-content-editor" in "controlAddIn" element on receiving "onLoadContent" custom event. And it will dispatch "onContentChange" custom event on content change.</p><pre><code>import React from 'react';
import ReactDOM from 'react-dom';
import { EditorState, RichContentEditor } from 'wix-rich-content-editor';
import { createLinkPlugin } from 'wix-rich-content-plugin-link';
import { createCodeBlockPlugin } from 'wix-rich-content-plugin-code-block';
import { createHashtagPlugin } from 'wix-rich-content-plugin-hashtag';
import { createHtmlPlugin } from 'wix-rich-content-plugin-html';
import 'wix-rich-content-editor-common/dist/styles.min.css';
import 'wix-rich-content-editor/dist/styles.min.css';
import 'wix-rich-content-plugin-link/dist/styles.min.css';
import 'wix-rich-content-plugin-code-block/dist/styles.min.css';
import 'wix-rich-content-plugin-hashtag/dist/styles.min.css';
import 'wix-rich-content-plugin-html/dist/styles.min.css';
import './App.css';

import {
  convertFromRaw,
  convertToRaw,
} from 'wix-rich-content-editor-common';

const PLUGINS = [createLinkPlugin, createCodeBlockPlugin, createHashtagPlugin, createHtmlPlugin];
const ON_LOAD_CONTENT_EVENT = 'onLoadContent';
const ON_CONTENT_CHANGE_EVENT = 'onContentChange';

window.addEventListener(ON_LOAD_CONTENT_EVENT, (e) => {
  ReactDOM.render(
    <React.StrictMode>
      <ContentEditor content={e.detail} />
    </React.StrictMode>,
    document.getElementById('controlAddIn')
  );
})

class ContentEditor extends React.Component {
  constructor(props) {
    super(props);

    this.refsEditor = React.createRef();
    if (this.props.content) {
      const contentState = convertFromRaw(this.props.content);
      this.state = {
        editorState: EditorState.createWithContent(contentState),
      }
    } else {
      this.state = { editorState: EditorState.createEmpty(), };
    }

    this.onLoadContent = this.onLoadContent.bind(this);
    window.addEventListener(ON_LOAD_CONTENT_EVENT, this.onLoadContent)
  }

  onLoadContent(e) {
    if (!e.detail)
      return;

    const contentState = convertFromRaw(e.detail);
    this.setState({
      editorState: EditorState.createWithContent(contentState),
    });
  }

  onChange = editorState => {
    this.setState({
      editorState,
    });

    const rawContent = convertToRaw(editorState.getCurrentContent());
    let event = new CustomEvent(ON_CONTENT_CHANGE_EVENT, { detail: rawContent });
    window.dispatchEvent(event);
  };

  componentDidMount() {
    this.refsEditor.current.focus();
  }

  componentWillUnmount() {
    window.removeEventListener(ON_LOAD_CONTENT_EVENT, this.onLoadContent);
  }

  render() {
    return (
      <div className='content-editor-container'>
        <RichContentEditor
          ref={this.refsEditor}
          plugins={PLUGINS}
          onChange={this.onChange} editorState={this.state.editorState} />
      </div>
    );
  }
}

export default ContentEditor;</code></pre><p>Install the following packages to use "wix-rich-content-editor":</p><pre><code>npm install classnames
npm install wix-rich-content-editor
npm install wix-rich-content-plugin-code-block
npm install wix-rich-content-plugin-hashtag
npm install wix-rich-content-plugin-html
npm install wix-rich-content-plugin-link</code></pre><h2>Custom Events (content-editor-events.js)</h2><p>Custom events are used to communicate ReactJS component with Control Add-in.</p>

<p>onLoadContent: this event tells React Component that it has to render new content.</p>
<p>onContentChange: this event tells Control Add-in that the content is modified in the React Component.</p><pre><code>function onLoadContent(content) {
    const event = new CustomEvent('onLoadContent', { detail: content });
    window.dispatchEvent(event);
}

window.addEventListener('onContentChange', function (e) {
    const content = e.detail;
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('OnContentChange', [content])
});

window.LoadContent = function (content) {
    if (JSON.stringify(content) == '{}')
        onLoadContent(null);
    else
        onLoadContent(content);
}
</code></pre><h2>Control Add-in (ContentEditor.ControlAddin.al)</h2><p>The above "gulpfile.js" creates "content-editor.js", "content-editor.css" files in "app/src/ContentEditor" folder when ReactJS application's build command is executed. These files should be used in Scripts and StyleSheets in Control Add-ins.</p><pre><code>controladdin ContentEditor
{
    RequestedHeight = 300;
    VerticalStretch = true;
    VerticalShrink = true;
    HorizontalStretch = true;
    HorizontalShrink = true;
    Scripts =
        './src/ContentEditor/js/content-editor-events.js',
        './src/ContentEditor/js/content-editor.js';
    StyleSheets = './src/ContentEditor/css/content-editor.css';

    event OnContentChange(content: JsonObject)
    procedure LoadContent(content: JsonObject)
}</code></pre>
<h2>Customer Card (CustomerCardExt.PageExt.al)</h2><p>Using "ContentEditor" Control Add-in, the "About" and the "Twitter" group controls are added after the "General" group.</p>
<p>This will allow to write rich text content in the "About" and the "Twitter" groups.</p>
<pre><code>pageextension 50120 CustomerCardExt extends "Customer Card"
{
    layout
    {
        addafter(General)
        {
            group(About)
            {
                Caption = 'About';

                usercontrol("ContentEditor"; ContentEditor)
                {
                    trigger OnContentChange(Content: JsonObject)
                    var
                        ContentText: Text;
                        OStream: OutStream;
                    begin
                        Detail.CreateOutStream(OStream, TextEncoding::UTF8);
                        Content.WriteTo(OStream);
                        Modify();
                    end;
                }
            }
            group(Twitter)
            {
                Caption = 'Twitter';

                usercontrol("Twitter ContentEditor"; ContentEditor)
                {
                    trigger OnContentChange(Content: JsonObject)
                    var
                        ContentText: Text;
                        OStream: OutStream;
                    begin
                        Twitter.CreateOutStream(OStream, TextEncoding::UTF8);
                        Content.WriteTo(OStream);
                        Modify();
                    end;
                }
            }
        }
    }

    trigger OnAfterGetCurrRecord()
    begin
        LoadAbout();
        LoadTwitter();
    end;

    local procedure LoadAbout()
    var
        JObject: JsonObject;
        IStream: InStream;
    begin
        CalcFields(Detail);
        if Detail.HasValue() then begin
            Detail.CreateInStream(IStream, TextEncoding::UTF8);
            JObject.ReadFrom(IStream);
        end;

        CurrPage.ContentEditor.LoadContent(JObject);
    end;

    local procedure LoadTwitter()
    var
        JObject: JsonObject;
        IStream: InStream;
    begin
        CalcFields(Twitter);
        if Twitter.HasValue() then begin
            Twitter.CreateInStream(IStream, TextEncoding::UTF8);
            JObject.ReadFrom(IStream);
        end;

        CurrPage."Twitter ContentEditor".LoadContent(JObject);
    end;
}</code></pre><h2>Code in Action</h2><p>The following screenshot shows "About" and "Twitter" tabs with rich text content embeded.</p><figure><img src="https://static.wixstatic.com/media/394025_84c7aa3b9d51481eb3d041e3f25ad140~mv2.gif/v1/fit/w_1000,h_577,al_c,q_80/file.png"  ></figure><h2>Conclusion</h2><p>The sample ReactJS Component used in this post is <a href="https://github.com/wix-incubator/rich-content" target="_blank" rel="noopener"><u>Wix Rich Content</u></a>. This is one of my favorite React Component for rich text editing. For Control add-ins, you can create your own React Component, or use any existing component. To match the Business Central UI you can use the Fluent UI framework (Business Central also uses this framework internally).</p>

<p>References: </p><ul>
  <li><a href="https://github.com/brillout/awesome-react-components" target="_blank" rel="noopener"><u>Absolutely Awesome React Components & Libraries </u></a></li>
  <li><a href="https://developer.microsoft.com/en-us/fluentui#/controls/web" target="_blank" rel="noopener"><u>Fluent UI</u></a></li>
</ul><h2>Source Code</h2><p>You can download the complete source code at <a href="https://github.com/msnraju/control-add-in-samples/tree/master/content-editor" target="_blank" rel="noopener"><u>GitHub</u></a></p>

<p>Happy Coding!!!</p>

<p>#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #ReactJS </p>]]></content:encoded></item><item><title><![CDATA[How to connect SharePoint with Business Central]]></title><description><![CDATA[Learn how to read, upload, download, and delete files from SharePoint Document Library using AL Code in Microsoft D365 Business Central.
]]></description><link>https://www.msnjournals.com/post/how-to-connect-sharepoint-with-business-central</link><guid isPermaLink="false">5f45a78db3800500177c58e5</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[OAuth]]></category><category><![CDATA[SharePoint]]></category><pubDate>Wed, 26 Aug 2020 03:27:29 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_9da9692b821f4254901877b04737052b~mv2.gif/v1/fit/w_1000,h_652,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>SharePoint integration with Business Central is one of the most common requirement. SharePoint Document Management System can be used to manage files for Business Central online. Sometimes for various reasons developer needs to handle files in SharePoint from Business Central AL Code programatically.  This post explains how to integrate SharePoint with Business Central and handling Document Libraries and files using Microsoft Graph API.</p>

<p>The following topics are covered in this post:</p><ul>
  <li>Retrieve Document Libraries from a SharePoint site.</li>
  <li>Retrieve folders and files from a Document Library.</li>
  <li>Upload and download files to and from a Document Library.</li>
  <li>Delete files and folders from a Document Library.</li>
</ul><h2>Prerequisits</h2><p>Some of the topics are already covered in other posts. To avoid repetition, links are provided to the related posts.</p>

<p>Following are the prequisits:</p><ul>
  <li><strong>An app registration in azure active directory</strong></li>
</ul><p>How to setup app registration in <a href="https://portal.azure.com/#home" target="_blank" rel="noopener">,<u>Azure Portal</u></a> is explained in <a href="https://www.msnjournals.com/post/how-to-use-microsoft-graph-api-in-business-central" target="_blank" rel="noopener">,<u>How to use Microsoft Graph API in Business Central</u></a> post. </p><ul>
  <li><strong>Generic OAuth2 Library</strong></li>
</ul><p>Using this library OAuth Access Token can be acquired from Azure AD. How to setup "OAuth 2.0 Application" is explained in <a href="https://www.msnjournals.com/post/generic-oauth2-library-for-business-central" target="_blank" rel="noopener">,<u>Generic OAuth2 Library for Business Central</u></a> post. Source code can be downloaded from <a href="https://github.com/msnraju/BC-OAuth-2.0-Authorization" target="_blank" rel="noopener"><u>GitHub</u></a>,</p>
<h2>API Functions</h2><p>The following API functions are required to handle SharePoint Document Libraries.</p><ol>
  <li>Get Access Token</li>
  <li>Fetch Drives (Document Libraries)</li>
  <li>Fetch Drive's Items (Folders and Files from a Document Library)</li>
  <li>Fetch Drive's Child Items (Folders and Files from a Folder in a Document Library)</li>
  <li>Upload a File</li>
  <li>Download a File</li>
  <li>Create a Folder</li>
  <li>Delete a Drive Item (File or Folder)</li>
</ol><h2>Get Access Token</h2><p>The following code returns Access Token using "Generic OAuth2 Library". "OAuth 2.0 Application" table, "OAuth 2.0 App. Helper" codeunit objects are from "Generic OAuth2 Library".</p><pre><code>procedure GetAccessToken(AppCode: Code[20]): Text
var
	OAuth20Application: Record "OAuth 2.0 Application";
	OAuth20AppHelper: Codeunit "OAuth 2.0 App. Helper";
	MessageText: Text;
begin
	OAuth20Application.Get(AppCode);
	if not OAuth20AppHelper.RequestAccessToken(OAuth20Application, MessageText) then
		Error(MessageText);

	exit(OAuth20AppHelper.GetAccessToken(OAuth20Application));
end;
</code></pre><h3>Setup OAuth 2.0 Application</h3><p>Setup "OAuth 2.0 Application" to acquire Access Token from Azure AD using <a href="https://www.msnjournals.com/post/generic-oauth2-library-for-business-central" target="_blank" rel="noopener">,<u>Generic OAuth2 Library</u></a>.</p>

<p>1) Search for "OAuth 2.0 Applications" in Business Central</p><figure><img src="https://static.wixstatic.com/media/394025_25822f3d408b42598f1e6fd4dec68cbe~mv2.png/v1/fit/w_1000,h_301,al_c,q_80/file.png"  ></figure><p>2) Create a new "OAuth 2.0 Application" from the list</p><figure><img src="https://static.wixstatic.com/media/394025_f5dfb74b14c5476a8b0161f324893ff2~mv2.png/v1/fit/w_1000,h_577,al_c,q_80/file.png"  ></figure><p>The above screenshot contains sample configuration to get Access Token from Azure AD using Generic OAuth2 Library.</p>
<h2>Fetch Drives (Document Libraries)</h2><p>The following code retrived Drives (Document Libraries) and saves it in "Online Drive" table.</p><pre><code>var
	DrivesUrl: Label 'https://graph.microsoft.com/v1.0/drives', Locked = true;
	
procedure FetchDrives(AccessToken: Text; var Drive: Record "Online Drive"): Boolean
var
	JsonResponse: JsonObject;
	JToken: JsonToken;
begin
	if HttpGet(AccessToken, DrivesUrl, JsonResponse) then begin
		if JsonResponse.Get('value', JToken) then
			ReadDrives(JToken.AsArray(), Drive);

		exit(true);
	end;
end;
</code></pre><h3>Online Drive (table 50115)</h3><pre><code>table 50115 "Online Drive"
{
    DataClassification = CustomerContent;

    fields
    {
        field(1; id; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(2; name; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(3; description; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(4; driveType; Text[80])
        {
            DataClassification = CustomerContent;
        }
        field(5; createdDateTime; DateTime)
        {
            DataClassification = CustomerContent;
        }
        field(6; lastModifiedDateTime; DateTime)
        {
            DataClassification = CustomerContent;
        }
        field(7; webUrl; Text[250])
        {
            DataClassification = CustomerContent;
        }
    }

    keys
    {
        key(PK; id)
        {
            Clustered = true;
        }
    }
}</code></pre><h3>HttpGet method</h3><p>Following is a helper function to get json object response from the given Url using AccessToken.</p><pre><code>local procedure HttpGet(AccessToken: Text; Url: Text; var JResponse: JsonObject): Boolean
var
	Client: HttpClient;
	Headers: HttpHeaders;
	RequestMessage: HttpRequestMessage;
	ResponseMessage: HttpResponseMessage;
	RequestContent: HttpContent;
	ResponseText: Text;
	IsSucces: Boolean;
begin
	Headers := Client.DefaultRequestHeaders();
	Headers.Add('Authorization', StrSubstNo('Bearer %1', AccessToken));

	RequestMessage.SetRequestUri(Url);
	RequestMessage.Method := 'GET';

	if Client.Send(RequestMessage, ResponseMessage) then
		if ResponseMessage.IsSuccessStatusCode() then begin
			if ResponseMessage.Content.ReadAs(ResponseText) then
				IsSucces := true;
		end else
			ResponseMessage.Content.ReadAs(ResponseText);

	JResponse.ReadFrom(ResponseText);
	exit(IsSucces);
end;</code></pre>
<h3>ReadDrives method</h3><p>This function reads JsonArray and inserts data into "Online Drive" table.</p><pre><code>local procedure ReadDrives(JDrives: JsonArray; var Drive: Record "Online Drive")
var
	JDriveItem: JsonToken;
	JDrive: JsonObject;
	JToken: JsonToken;
begin
	foreach JDriveItem in JDrives do begin
		JDrive := JDriveItem.AsObject();

		Drive.Init();
		if JDrive.Get('id', JToken) then
			Drive.Id := JToken.AsValue().AsText();
		if JDrive.Get('name', JToken) then
			Drive.Name := JToken.AsValue().AsText();
		if JDrive.Get('description', JToken) then
			Drive.description := JToken.AsValue().AsText();
		if JDrive.Get('driveType', JToken) then
			Drive.driveType := JToken.AsValue().AsText();
		if JDrive.Get('createdDateTime', JToken) then
			Drive.createdDateTime := JToken.AsValue().AsDateTime();
		if JDrive.Get('lastModifiedDateTime', JToken) then
			Drive.lastModifiedDateTime := JToken.AsValue().AsDateTime();
		if JDrive.Get('webUrl', JToken) then
			Drive.webUrl := JToken.AsValue().AsText();
		Drive.Insert();
	end;
end;</code></pre><h2>Fetch Drive's Items </h2><p>The following code can fetch files and folders from a Drive (Document Library). DriveID is the value of the id property in Drive JsonObject (saved in "Online Drive" table).</p><pre><code>var
	DrivesItemsUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/root/children', Comment = '%1 = Drive ID', Locked = true;
	
procedure FetchDrivesItems(AccessToken: Text; DriveID: Text; var DriveItem: Record "Online Drive Item"): Boolean
var
	JsonResponse: JsonObject;
	JToken: JsonToken;
	IsSucces: Boolean;
begin
	if HttpGet(AccessToken, StrSubstNo(DrivesItemsUrl, DriveID), JsonResponse) then begin
		if JsonResponse.Get('value', JToken) then
			ReadDriveItems(JToken.AsArray(), DriveID, '', DriveItem);

		exit(true);
	end;
end;
</code></pre><h3>Online Drive Item (table 50115)</h3><p>To store Files and Folder information</p><pre><code>table 50116 "Online Drive Item"
{
    DataClassification = CustomerContent;

    fields
    {
        field(1; id; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(2; driveId; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(3; parentId; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(4; name; Text[250])
        {
            DataClassification = CustomerContent;
        }
        field(5; isFile; Boolean)
        {
            DataClassification = CustomerContent;
        }
        field(6; mimeType; Text[80])
        {
            DataClassification = CustomerContent;
        }
        field(7; size; BigInteger)
        {
            DataClassification = CustomerContent;
        }
        field(8; createdDateTime; DateTime)
        {
            DataClassification = CustomerContent;
        }
        field(9; webUrl; Text[250])
        {
            DataClassification = CustomerContent;
        }
    }

    keys
    {
        key(PK; id)
        {
            Clustered = true;
        }
    }
}</code></pre>
<h3>ReadDriveItems</h3><p>Read JDriveItems JsonArray and saves in "Online Drive Item" table.</p><pre><code>local procedure ReadDriveItems(
	JDriveItems: JsonArray;
	DriveID: Text;
	ParentID: Text;
	var DriveItem: Record "Online Drive Item")
var
	JToken: JsonToken;
begin
	foreach JToken in JDriveItems do
		ReadDriveItem(JToken.AsObject(), DriveID, ParentID, DriveItem);
end;

local procedure ReadDriveItem(
	JDriveItem: JsonObject;
	DriveID: Text;
	ParentID: Text;
	var DriveItem: Record "Online Drive Item")
var
	JFile: JsonObject;
	JToken: JsonToken;
begin

	DriveItem.Init();
	DriveItem.driveId := DriveID;
	DriveItem.parentId := ParentID;

	if JDriveItem.Get('id', JToken) then
		DriveItem.Id := JToken.AsValue().AsText();
	if JDriveItem.Get('name', JToken) then
		DriveItem.Name := JToken.AsValue().AsText();

	if JDriveItem.Get('size', JToken) then
		DriveItem.size := JToken.AsValue().AsBigInteger();

	if JDriveItem.Get('file', JToken) then begin
		DriveItem.IsFile := true;
		JFile := JToken.AsObject();
		if JFile.Get('mimeType', JToken) then
			DriveItem.mimeType := JToken.AsValue().AsText();
	end;

	if JDriveItem.Get('createdDateTime', JToken) then
		DriveItem.createdDateTime := JToken.AsValue().AsDateTime();
	if JDriveItem.Get('webUrl', JToken) then
		DriveItem.webUrl := JToken.AsValue().AsText();
	DriveItem.Insert();
end;
</code></pre><h2>Fetch Drive's Child Items</h2><p>Following is the code to read Folders and Files from a Folder in a Document Library. ItemID is the value of the id property in DriveItem JsonObject (saved in "Online Drive Item" table).</p>
<pre><code>var
	DrivesChildItemsUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/items/%2/children', Comment = '%1 = Drive ID, %2 = Item ID', Locked = true;

procedure FetchDrivesChildItems(
	AccessToken: Text;
	DriveID: Text;
	ItemID: Text;
	var DriveItem: Record "Online Drive Item"): Boolean
var
	JsonResponse: JsonObject;
	JToken: JsonToken;
	IsSucces: Boolean;
begin
	if HttpGet(AccessToken, StrSubstNo(DrivesChildItemsUrl, DriveID, ItemID), JsonResponse) then begin
		if JsonResponse.Get('value', JToken) then
			ReadDriveItems(JToken.AsArray(), DriveID, ItemID, DriveItem);

		exit(true);
	end;
end;</code></pre>
<h2>Upload a File</h2><p>The following code uploads a file into a Document Library and save newly created Drive Item (file) details in "Online Drive Item" table.</p>

<p>ParentID: Folder's Drive Item ID (should be blank for a file to be uploaded to the root drive)</p>
<p>FolderPath: Name of the target folder (ex: "/personal/documents"  means file will be saved in documents folder which is a subfolder of personal folder)</p>
<p>FileName: Name of the file with extension (ex: "readme.pdf")</p>
<p>Stream: File Content</p>
<pre><code>var
	UploadUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/items/root:/%2:/content', Comment = '%1 = Drive ID, %2 = File Name', Locked = true;

procedure UploadFile(
	AccessToken: Text;
	DriveID: Text;
	ParentID: Text;
	FolderPath: Text;
	FileName: Text;
	var Stream: InStream;
	var OnlineDriveItem: Record "Online Drive Item"): Boolean
var
	HttpClient: HttpClient;
	Headers: HttpHeaders;
	RequestMessage: HttpRequestMessage;
	RequestContent: HttpContent;
	ResponseMessage: HttpResponseMessage;
	JsonResponse: JsonObject;
	IsSucces: Boolean;
	ResponseText: Text;
begin
	Headers := HttpClient.DefaultRequestHeaders();
	Headers.Add('Authorization', StrSubstNo('Bearer %1', AccessToken));

	RequestMessage.SetRequestUri(
		StrSubstNo(
			UploadUrl,
			DriveID,
			StrSubstNo('%1/%2', FolderPath, FileName)));
	RequestMessage.Method := 'PUT';

	RequestContent.WriteFrom(Stream);
	RequestMessage.Content := RequestContent;

	if HttpClient.Send(RequestMessage, ResponseMessage) then
		if ResponseMessage.IsSuccessStatusCode() then begin
			if ResponseMessage.Content.ReadAs(ResponseText) then begin
				IsSucces := true;
				if JsonResponse.ReadFrom(ResponseText) then
					ReadDriveItem(JsonResponse, DriveID, ParentID, OnlineDriveItem);
			end;
		end else
			if ResponseMessage.Content.ReadAs(ResponseText) then
				JsonResponse.ReadFrom(ResponseText);

	exit(IsSucces);
end;</code></pre><h2>Download a File</h2><p>The following code downloads a file from a Document Library. ItemID is the value of the id property in DriveItem JsonObject (saved in "Online Drive Item" table).</p><pre><code>var
	DownloadUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/items/%2/content', Comment = '%1 = Drive ID, %2 = Item ID', Locked = true;

procedure DownloadFile(AccessToken: Text; DriveID: Text; ItemID: Text; var Stream: InStream): Boolean
var
	TempBlob: Codeunit "Temp Blob";
	OStream: OutStream;
	JsonResponse: JsonObject;
	Content: Text;
	NewDownloadUrl: Text;
begin
	NewDownloadUrl := StrSubstNo(DownloadUrl, DriveID, ItemID);
	if GetResponse(AccessToken, NewDownloadUrl, Stream) then
		exit(true);
end;</code></pre><h2>Create a Folder</h2><p>The following code creates a new Folder in a Document Library.</p>
<p>ItemID: id of the Parent Folder (optional)</p><pre><code>var
	CreateFolderUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/items/%2/children', Comment = '%1 = Drive ID, %2 = Item ID', Locked = true;
	CreateRootFolderUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/root/children', Comment = '%1 = Drive ID', Locked = true;

procedure CreateDriveFolder(
	AccessToken: Text;
	DriveID: Text;
	ItemID: Text;
	FolderName: Text;
	var OnlineDriveItem: Record "Online Drive Item"): Boolean
var
	HttpClient: HttpClient;
	Headers: HttpHeaders;
	ContentHeaders: HttpHeaders;
	RequestMessage: HttpRequestMessage;
	RequestContent: HttpContent;
	ResponseMessage: HttpResponseMessage;
	ResponseText: Text;
	JsonBody: JsonObject;
	RequestText: Text;
	EmptyObject: JsonObject;
	JsonResponse: JsonObject;
begin
	Headers := HttpClient.DefaultRequestHeaders();
	Headers.Add('Authorization', StrSubstNo('Bearer %1', AccessToken));
	if ItemID = '' then
		RequestMessage.SetRequestUri(StrSubstNo(CreateRootFolderUrl, DriveID))
	else
		RequestMessage.SetRequestUri(StrSubstNo(CreateFolderUrl, DriveID, ItemID));
	RequestMessage.Method := 'POST';

	// Body
	JsonBody.Add('name', FolderName);
	JsonBody.Add('folder', EmptyObject);
	JsonBody.WriteTo(RequestText);
	RequestContent.WriteFrom(RequestText);

	// Content Headers
	RequestContent.GetHeaders(ContentHeaders);
	ContentHeaders.Remove('Content-Type');
	ContentHeaders.Add('Content-Type', 'application/json');

	RequestMessage.Content := RequestContent;

	if HttpClient.Send(RequestMessage, ResponseMessage) then
		if ResponseMessage.IsSuccessStatusCode() then begin
			if ResponseMessage.Content.ReadAs(ResponseText) then begin
				if JsonResponse.ReadFrom(ResponseText) then
					ReadDriveItem(JsonResponse, DriveID, ItemID, OnlineDriveItem);

				exit(true);
			end;
		end;
end;</code></pre>
<h2>Delete a Drive Item</h2><p>The following code deletes a folder or a file from a Document Library. ItemID is the id of the Drive Item (file or folder) saved in "Online Drive Item" table.</p>
<pre><code>var
	DeleteUrl: Label 'https://graph.microsoft.com/v1.0/drives/%1/items/%2', Comment = '%1 = Drive ID, %2 = Item ID', Locked = true;

procedure DeleteDriveItem(AccessToken: Text; DriveID: Text; ItemID: Text): Boolean
var
	HttpClient: HttpClient;
	Headers: HttpHeaders;
	RequestMessage: HttpRequestMessage;
	ResponseMessage: HttpResponseMessage;
begin
	Headers := HttpClient.DefaultRequestHeaders();
	Headers.Add('Authorization', StrSubstNo('Bearer %1', AccessToken));

	RequestMessage.SetRequestUri(StrSubstNo(DeleteUrl, DriveID, ItemID));
	RequestMessage.Method := 'DELETE';

	if HttpClient.Send(RequestMessage, ResponseMessage) then
		if ResponseMessage.IsSuccessStatusCode() then
			exit(true);
end;
</code></pre><h2>Conclusion</h2><p>So this post has covered all basic operations that can be performed to handle documents in SharePoint within Business Central. Publishing, Version control etc. SharePoint features are not covered. </p>

<p><strong>Tip:</strong> The same code with little changes can be used to integrate with OneDrive. Basically, <a href="https://docs.microsoft.com/en-us/graph/overview" target="_blank" rel="noopener"><u>Microsoft Graph API</u></a> offers the same API for SharePoint and OneDrive to handle files. </p>
<h2>Code in Action</h2><figure><img src="https://static.wixstatic.com/media/394025_9da9692b821f4254901877b04737052b~mv2.gif/v1/fit/w_1000,h_652,al_c,q_80/file.png"  ></figure><p>Complete source code with working examples can be downloaded from <a href="https://github.com/msnraju/sp-document-explorer" target="_blank" rel="noopener"><u>GitHub</u></a>. Issues and recommendations can be posted at <a href="https://github.com/msnraju/sp-document-explorer/issues" target="_blank" rel="noopener"><u>GitHub Issue Tracker</u></a>.</p>

<p>Happy Coding!!!</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral #SharePoint #DynamicsNAV <a href="https://twitter.com/hashtag/MSGraphAPI?src=hashtag_click" target="_blank" rel="noopener">#MSGraphAPI</a></p>]]></content:encoded></item><item><title><![CDATA[Generic OAuth2 Library for Business Central]]></title><description><![CDATA[How to get OAuth Access Token from Azure AD, Google, Facebook etc in Business Central. Download source code from GitHub.]]></description><link>https://www.msnjournals.com/post/generic-oauth2-library-for-business-central</link><guid isPermaLink="false">5f41e0c657cc36001795f9f6</guid><category><![CDATA[AL Language]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[OAuth]]></category><pubDate>Sun, 23 Aug 2020 06:14:35 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_fae71f4347ae4752a7be2fef3c1cb3e7~mv2.gif/v1/fit/w_1000,h_629,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Generic OAuth2 Library for Business Central is to acquire Access Token from Azure AD, Google, Facebook etc. OAuth is most commonly used authorization method across all platforms. Acquiring Access Token is a little difficult in Business Central, though there is a Codeunit called OAuth2 available in the system. To help the Business Central developers' community, I thought of creating this generic library for OAuth2, so that developers can use this in their applications.</p>
<h2>Supported Types</h2><p>The following grant types are supported:</p><ul>
  <li>Authorization Code<ul>
  <li>Tested with Azure AD, Google, Facebook </li>
</ul></li>
  <li>Password Credentials<ul>
  <li>Tested with Azure AD</li>
</ul></li>
  <li>Client Credentials <ul>
  <li>Not yet tested</li>
</ul></li>
</ul><p>Please refer <a href="https://oauth.net/2/" target="_blank" rel="noopener"><u>oauth.net</u></a> to understand OAuth and grant types.</p>
<h2>Setup OAuth 2.0 Applications</h2><p>First you need to setup a client application in the provider's website. </p>
<p>example: portal.azure.com, console.developers.google.com, developers.facebook.com/apps etc. </p>

<p><em>Note: Please refer to the </em><a href="https://www.msnjournals.com/post/how-to-use-microsoft-graph-api-in-business-central" target="_blank" rel="noopener"><em><u>Graph API</u></em></a><em> post to learn how to register and setup an application in azure portal.</em></p>
<h3>OAuth 2.0 Application (Page 50101)</h3><p>Client ID, Client Secret, Scope / Permissions, Endpoints etc. inputs need to be updated in this page. Most of the inputs are available in the application that you have registered in the provider's website.</p><figure><img src="https://static.wixstatic.com/media/394025_42d4c42749b94fd9a432b83ccad39989~mv2.png/v1/fit/w_1000,h_610,al_c,q_80/file.png"  ></figure><p>You can test the configuration by clicking on the "Request Access Token" action button.</p>
<h3>OAuth 2.0 Applications (Page 50100)</h3><p>You can see list of Applications with the status. Using this, you can create a new application.</p>
<figure><img src="https://static.wixstatic.com/media/394025_b3d12b7ad83b4a76b5a522b9680521cc~mv2.png/v1/fit/w_973,h_284,al_c,q_80/file.png"  ></figure><h2>How to Use</h2><p>After updating the application details in "OAuth 2.0 Application" page, you can use the following Codeunit to get Access Token.</p>
<h3>OAuth 2.0 App. Helper (Codeunit 50101)</h3>
<p>RequestAccessToken method will update Access Token in "OAuth 2.0 Application" record. It will return false with an error message if it is failed. </p>

<p>The following code acquires access token from Google and displays in a message box.</p><pre><code>procedure GetGoogleAccessToken()
var
	OAuth20Appln: Record "OAuth 2.0 Application";
	OAuth20AppHelper: Codeunit "OAuth 2.0 App. Helper";
	MessageText: Text;
begin
	OAuth20Appln.Get('GOOGLE');
	if not OAuth20AppHelper.RequestAccessToken(OAuth20Appln, MessageText) then
		Error(MessageText);

	Message('%1', OAuth20AppHelper.GetAccessToken(OAuth20Appln));
end;</code></pre>
<h2>Conclusion</h2><p>You can download and use these objects in your application. But I recommend testing all integration scenarios thoroughly before using in production environment. If you have any questions you can ask by writing comments below this post.</p>
<h2>Code in Action</h2><p>Access Token from Facebook:</p><figure><img src="https://static.wixstatic.com/media/394025_fae71f4347ae4752a7be2fef3c1cb3e7~mv2.gif/v1/fit/w_1000,h_629,al_c,q_80/file.png"  ></figure><p>You can download the complete source code from <a href="https://github.com/msnraju/BC-OAuth-2.0-Authorization" target="_blank" rel="noopener"><u>GitHub</u></a>, and you can report issues on <a href="https://github.com/msnraju/BC-OAuth-2.0-Authorization/issues" target="_blank" rel="noopener"><u>GitHub Issue Tracker</u></a>. </p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral #DynamicsNAV #OAuth2</p>]]></content:encoded></item><item><title><![CDATA[Cryptography support in Business Central]]></title><description><![CDATA[In AL there is a codeunit called Cryptography Management (Codeunit 1266) which provides helper functions for encryption and hashing....]]></description><link>https://www.msnjournals.com/post/cryptography-support-in-business-central</link><guid isPermaLink="false">5f3a11c4ef93210017c77216</guid><pubDate>Mon, 17 Aug 2020 22:30:09 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_92b9f894be08404f98a00362f9ce66dd~mv2.png/v1/fit/w_1000,h_353,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>In AL there is a codeunit called Cryptography Management (Codeunit 1266) which provides helper functions for encryption and hashing. Cryptography is used to keep the data safe by encrypting and depcrypting the data so that others canot misuse the data. </p>

<p>This post covers only basic usege of Cryptography Management codeunit. The following are the topics convered in this post.</p><ul>
  <li>Encryption Key</li>
  <li>Importing / Exporting Encryption Key</li>
  <li>Encrypt  / Decrypt data</li>
</ul><h2>Encryption Key</h2><p>Encryption Key is a unique string used by Cryptography Management for data encryption. This is maintained at the tenent level. This key is generated by using .NET Framework Data Protection API interally by enabling Encryption Key. </p>

<p>The following code enables Encryption programatically:</p><pre><code>local procedure EnableEncryptionKey()
var
	CryptographyManagement: Codeunit "Cryptography Management";
begin
	CryptographyManagement.EnableEncryption(true);
end;</code></pre>
<p>Encryption can be enabled using "Data Encryption Management" page, by clicking Enable Encryption action button.</p>
<p>In Business Central online this is by default Enabled, and it can not be disabled. </p><figure><img src="https://static.wixstatic.com/media/394025_92b9f894be08404f98a00362f9ce66dd~mv2.png/v1/fit/w_1000,h_353,al_c,q_80/file.png"  ></figure><p>After clicking Enable Encryption action button, it ask you to download a copy of encryption key so that you can keep it in a safe location.</p>
<h2>Importing / Exporting Encryption Key</h2><p>In this page there are few more options like Export Encryption Key, Import Encryption Key and Change Encryption Key.</p><ul>
  <li>Export Encryption Key - To backup exiting encryption key.</li>
  <li>Import Encryption Key -  Existing encrytion key can be restored / imported using this option.</li>
  <li>Change Encryption Key - When encryption is already enabled, you can use this option to change encryption key.</li>
</ul><h2>Encrypt  / Decrypt data</h2><p>You can encrypt secret data using Encrypt method, and when it is need you can decrypt the data using Decrypt method.</p>

<p>The following code encrypts the text 'Hello':</p><pre><code>local procedure TryEncrypt()
var
	CryptographyManagement: Codeunit "Cryptography Management";
	EncryptedText: Text;
begin
	EncryptedText := CryptographyManagement.Encrypt('Hello');
	// pIuF3czIJLvv/KRQUKoGyXa2h2TEBonmxtlHu5lNJo4irzb5srQQl5isHuw182aL+op2FuehEq5/o0/8Nr3N1B34E8pbXdXRcC77sL+EfrXxZ2szebHNaQ47W6bTfLdLE4qYIQgcXx5s0VtFc6yLwvVe7bKHG02o8bYh6kiKrDIWdgwZyN5lOpbOrwMhl+ISAZo3iFrhR1OSnWF2uhCn4yyvGjhgA0Yp+9akLZqi8KycECRyIxhiGtSlg99be2aklDMWhGnNNTkL/BCxcEGPqp8ccXjMr/k5tRyw4VZsHSwYKIjnSUiVawh+1GqryG18vHV7JVpXyb1JFRDIkQ3uCQ==
end;</code></pre>
<p>The following code decrypts the encryped text to 'Hello':</p><pre><code>local procedure TryDecrypt()
var
	CryptographyManagement: Codeunit "Cryptography Management";
	EncryptedText: Text;
	Data: Text;
begin
	EncryptedText := 'pIuF3czIJLvv/KRQUKoGyXa2h2TEBonmxtlHu5lNJo4irzb5srQQl5isHuw182aL+op2FuehEq5/o0/8Nr3N1B34E8pbXdXRcC77sL+EfrXxZ2szebHNaQ47W6bTfLdLE4qYIQgcXx5s0VtFc6yLwvVe7bKHG02o8bYh6kiKrDIWdgwZyN5lOpbOrwMhl+ISAZo3iFrhR1OSnWF2uhCn4yyvGjhgA0Yp+9akLZqi8KycECRyIxhiGtSlg99be2aklDMWhGnNNTkL/BCxcEGPqp8ccXjMr/k5tRyw4VZsHSwYKIjnSUiVawh+1GqryG18vHV7JVpXyb1JFRDIkQ3uCQ==';
	Data := CryptographyManagement.Decrypt(EncryptedText);
end;</code></pre>
<h2>Conclusion</h2><p>In the early versions on Business Central / NAV, to achieve this functionality we had to use .Net variables. Now its part of System. There are many other methods in "Cryptography Management" codeunit you must try.</p>

<p>Happy Coding!!!</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral #DynamicsNAV</p>]]></content:encoded></item><item><title><![CDATA[How to use Microsoft Graph API in Business Central]]></title><description><![CDATA[Learn How to use Microsoft Graph API in Business Central. This post explains how to register and configure Applications in Azure Portal.]]></description><link>https://www.msnjournals.com/post/how-to-use-microsoft-graph-api-in-business-central</link><guid isPermaLink="false">5f3940878aad1900179d1a2f</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><pubDate>Sun, 16 Aug 2020 17:27:42 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_807031c16a3e4e9182cb095576987139~mv2.png/v1/fit/w_1000,h_464,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Microsoft Graph API is very powerful Restful Web API, and a gateway to data in Microsoft 365. Data from diffrent applications in Microsoft 365 like SharePoint, Teams, Calenders, OneDrive etc. can be accessed using very simple web queries. </p>

<p>Now Business Central data can also be accessed by using Microsoft Graph API from other applications using the same API. More information can be found at <a href="https://docs.microsoft.com/en-us/graph/api/resources/dynamics-graph-reference?view=graph-rest-beta" target="_blank" rel="noopener"><u>Working with the Dynamics 365 Business Central API in Microsoft Graph</u></a></p>
<h2>How ?</h2><p>We can use Microsoft Graph API by following 5 simple steps.</p><ol>
  <li>Register your Application in <a href="https://portal.azure.com/" target="_blank" rel="noopener"><u>Azure Portal</u></a>.</li>
  <li>Add Redirect URI(s)</li>
  <li>Add Permissions to the Application</li>
  <li>Add Client Secret to the Application</li>
  <li>Get Authetication Token</li>
  <li>Quering Data</li>
</ol><h2>Register your application</h2><p>1) Open <a href="https://portal.azure.com/" target="_blank" rel="noopener"><u>www.portal.azure.com</u></a> and sign-in using your organization credientials. </p>
<p>2) Search for App Registrations in the search bar and select App registrations.</p><figure><img src="https://static.wixstatic.com/media/394025_6f308ad4982b45a1af7bca849181e35b~mv2.png/v1/fit/w_722,h_148,al_c,q_80/file.png"  ></figure><p>3) Click on New registration button.</p><figure><img src="https://static.wixstatic.com/media/394025_8bdba97342a848358a3c83a143c27b4d~mv2.png/v1/fit/w_491,h_180,al_c,q_80/file.png"  ></figure><p>4) Enter your Application Name, and click on Register button.</p><figure><img src="https://static.wixstatic.com/media/394025_828e0c31415b4fb6be607a0e37485cb0~mv2.png/v1/fit/w_893,h_637,al_c,q_80/file.png"  ></figure><p>5) You should be in the following screen after clicking the Register button.</p><figure><img src="https://static.wixstatic.com/media/394025_b5761d401f5641ebbb12d9db61691756~mv2.png/v1/fit/w_786,h_296,al_c,q_80/file.png"  ></figure><h2>Add Redirect URI(s)</h2><p>1) Click on Add a Redirect URI link</p><figure><img src="https://static.wixstatic.com/media/394025_0834ba0cc7ec4a28860bb534330d80bb~mv2.png/v1/fit/w_1000,h_260,al_c,q_80/file.png"  ></figure><p>2) Click on Add a platform and Select the Web option in the Configure platforms panel</p><figure><img src="https://static.wixstatic.com/media/394025_fc4fc67394274acf8ed0c0512a736540~mv2.png/v1/fit/w_1000,h_304,al_c,q_80/file.png"  ></figure><p>3) Update the Redirect URI. You should replace localhost:8080 with your web servers host name. Then click the Configure button.</p><figure><img src="https://static.wixstatic.com/media/394025_a1704bc77e8b46689ea725a93cfdee19~mv2.png/v1/fit/w_588,h_617,al_c,q_80/file.png"  ></figure><h2>Add Client Secret to the Application</h2><p>1) Select Certificates & secrets button in the left navigation menu and click the New client secret button in right panel.</p><figure><img src="https://static.wixstatic.com/media/394025_ba39672fb9fa43198eec8be322efac4d~mv2.png/v1/fit/w_681,h_482,al_c,q_80/file.png"  ></figure><p>2) You will get the following dialouge. Enter Description, and select Expires option. Click the Add button.</p>
<figure><img src="https://static.wixstatic.com/media/394025_fb7be0e35aa740f4ba3a4721cce21f2e~mv2.png/v1/fit/w_524,h_277,al_c,q_80/file.png"  ></figure><p>3) The above step creates a client secret. You need to copy this value and save it somewhere. Once this page is closed, you cannot copy this value again.</p><figure><img src="https://static.wixstatic.com/media/394025_11739e878e8944e4aa661f89d9f5de96~mv2.png/v1/fit/w_1000,h_199,al_c,q_80/file.png"  ></figure><h2>Add Permissions</h2><p>1) Click on API permissions button from the left navigation menu</p><figure><img src="https://static.wixstatic.com/media/394025_4f63f542b04642b09c06929ccbfec318~mv2.png/v1/fit/w_827,h_200,al_c,q_80/file.png"  ></figure><p>2)You will see the following screen on the right side of the navigation menu. Click on Add a Permission button under Configure permissions.</p><figure><img src="https://static.wixstatic.com/media/394025_334ecb94461d4eb0b9c1709f23207214~mv2.png/v1/fit/w_622,h_221,al_c,q_80/file.png"  ></figure><p>3) Select the Microsoft Graph option in Request API Permissions.</p><figure><img src="https://static.wixstatic.com/media/394025_7d542ce014dd4735a7357fafef917d3c~mv2.png/v1/fit/w_799,h_320,al_c,q_80/file.png"  ></figure><p>4) Select the Delegated permissions option.</p>
<figure><img src="https://static.wixstatic.com/media/394025_b1705058a9794160b102af8cc527a46f~mv2.png/v1/fit/w_827,h_266,al_c,q_80/file.png"  ></figure><p>5) Search for "Files" in the search box, and select Files.ReadWrite.All option in the list and click on the Add Permissions button.</p><figure><img src="https://static.wixstatic.com/media/394025_df7c64da80b24578b1781c7e669322d0~mv2.png/v1/fit/w_526,h_492,al_c,q_80/file.png"  ></figure><p>6) After completing the above steps you should be able to see the following permissions.</p><figure><img src="https://static.wixstatic.com/media/394025_4dc1b26ed2e5486bb3a37b57f265040f~mv2.png/v1/fit/w_1000,h_271,al_c,q_80/file.png"  ></figure><h2>API End Point</h2>
<p>API Endpoints can be found after selecting Overview, by clicking on Endpoints button. we are going use OAuth 2.0 authorization endpoint (v2) for get AuthToken.</p><figure><img src="https://static.wixstatic.com/media/394025_1d0b8c6aa0504c1287966f669c1879e2~mv2.png/v1/fit/w_1000,h_326,al_c,q_80/file.png"  ></figure><h2>Get Authetication Token</h2>
<p>We are using OAuth2 codeunit to get Autherization Token, this codeunit also has some useful functions related to OAuth2 authentication. </p>

<p>In the below code, the GetAccessToken function gets the AuthToken using OAuth2 codeunit. </p><pre><code>codeunit 50115 "Graph API Helper"
{
    var
        OAuth2: Codeunit OAuth2;
        ClientIdTxt: Label '96e2efa1-a6fb-4f04-97d9-1f9ac9c15917', Locked = true;
        ClientSecret: Label '44Qi99bD4EC4S27~_.5htAp1o_lLd7tfBg', Locked = true;
        ResourceUrlTxt: Label 'https://graph.microsoft.com', Locked = true;
        OAuthAuthorityUrlTxt: Label 'https://login.microsoftonline.com/67c5a58a-7424-4d4d-b6c2-ddc89830cf74/oauth2/authorize', Locked = true;
        RedirectURLTxt: Label 'http://localhost:8080/BC160/OAuthLanding.htm', Locked = true;
        OneDriveRootQueryUri: Label 'https://graph.microsoft.com/v1.0/me/drive/root/children', Locked = true;

    procedure GetAccessToken(): Text
    var
        PromptInteraction: Enum "Prompt Interaction";
        AccessToken: Text;
        AuthCodeError: Text;
    begin
        OAuth2.AcquireTokenByAuthorizationCode(
            ClientIdTxt,
            ClientSecret,
            OAuthAuthorityUrlTxt,
            RedirectURLTxt,
            ResourceURLTxt,
            PromptInteraction::Consent,
            AccessToken,
            AuthCodeError);

        if (AccessToken = '') or (AuthCodeError <> '') then
            Error(AuthCodeError);

        exit(AccessToken);
    end;
}
</code></pre>
<h2>Quering Data</h2>
<p>In the below code, the GetOneDriveFiles function is quering data from OneDrive's root folders and files using Microsoft Graph API.</p><pre><code>    procedure GetOneDriveFiles(): JsonObject
    var
        Client: HttpClient;
        RequestMessage: HttpRequestMessage;
        ResponseMessage: HttpResponseMessage;
        JsonResponse: JsonObject;
        AccessToken: Text;
        JsonContent: Text;
    begin
        AccessToken := GetAccessToken();

        RequestMessage.Method('GET');
        RequestMessage.SetRequestUri(OneDriveRootQueryUri);
        Client.DefaultRequestHeaders().Add('Authorization', StrSubstNo('Bearer %1', AccessToken));
        Client.DefaultRequestHeaders().Add('Accept', 'application/json');

        if Client.Send(RequestMessage, ResponseMessage) then
            if ResponseMessage.HttpStatusCode() = 200 then begin
                ResponseMessage.Content.ReadAs(JsonContent);
                JsonResponse.ReadFrom(JsonContent);
                exit(JsonResponse);
            end;
    end;</code></pre><h2>Conclusion</h2>
<p>This is a very simple example to retrieve data from OneDrive. Using the Microsft Graph API you can access almost any data in Microsoft 365. For example, using this Microsoft Graph API we can integrate SharePoint documents with Sales Order, Purchase Order pages, show e-mail communications in the Customer page etc.</p>

<p>Happy Coding!!!</p>

<p>You can download complete sample code from <a href="https://github.com/msnraju/OneDrive-Integration" target="_blank" rel="noopener"><u>GitHub</u></a>.</p>

<p>#MsDyn365 #MsDyn365 #BusinessCentral #DynamicsNAV #ALLanguage</p>]]></content:encoded></item><item><title><![CDATA[How to solve Table Locking Issues in Business Central]]></title><description><![CDATA[Learn how to solve Table Locking issues in Microsoft Dynamics 365 Business Central / NAV with code examples. ]]></description><link>https://www.msnjournals.com/post/how-to-solve-table-locking-issues-in-business-central</link><guid isPermaLink="false">5f2c0f2b9216880017b74551</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><category><![CDATA[Performace]]></category><pubDate>Sun, 09 Aug 2020 14:13:29 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_a4b9142e0f2e44348ab9961065a52b22~mv2.gif/v1/fit/w_1000,h_629,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Table Locking is a very common problem in Microsoft Dynamics 365 NAV / Business Central. If the problem is understood correctly and the right technique is used, Locking Errors can be solved very easily. This post explains different techniques that can be used to solve Locking errors.</p>
<h2>What is a Transaction?</h2><p>Transaction is a unit of work which contains one or more operations. </p>
<p>The following are the some of the events that creates a new Transaction in Business Central: </p><ul>
  <li>Saving a record in a Page - Logic written in the OnInsert, OnModify, and OnRename triggers.</li>
  <li>Executing Action Button in a Page – Logic written in Run Object’s OnRun trigger, and OnAction trigger.</li>
  <li>On Execution of a Web Service – Logic written in Codeunits, XMLports, and Report</li>
</ul><p>Transaction is automatically closed (committed) on completion of executing the logic. When a COMMIT statement is executed, the current Transaction is closed, and a new Transaction will begin. When an ERROR statement is executed, the current Transaction is terminated and all the changes made by the Transaction are rolled back.</p>
<h2>About Locks</h2><p>Database Locks are for your good only. Like a parent protecting siblings fighting with each other, Database Lock protects the data from being overwritten by the other Transactions executing at the same time. </p>

<p>When a Transaction tries to access the data that is being Locked by another Transaction, SQL Server blocks that Transaction. And when a Transaction is blocked for more than 10 seconds (Lock Time-out), it will be terminated automatically with the "Table was Locked by another User" error. </p>

<p>There is a setting called Lock Time-out in Alter Database / Advanced tab. Default value for the Lock Time-out is</p>
<p>10 seconds. Lock Time-out feature can be turned off for long running processes by calling Database.LockTimeout(false) function in the CAL / AL code. </p>

<p>In Business Central / Dynamics NAV, the SQL Server uses Row Level Locking, which means two Users can modify the data in the same table simultaneously but cannot modify the same data (rows). </p>

<p><em>Note: LockTable, FindSet (ForUpdate: true) statements also apply Row Level Locks.</em></p>
<h3>Row Level Locking is not effective for Ledger table</h3><p>Most of the Ledger Entry tables in Business Central / Dynamics NAV has "Entry No." as the Primary Key. To generate Primary Key in most of the places, following algorithm is used.</p>
<pre><code>LedgerEntry.LOCKTABLE;
IF LedgerEntry.FINDLAST THEN
 NextEntryNo := LedgerEntry.”Entry No.” + 1
ELSE
 NextEntryNo := 1;</code></pre>
<p>For some reason this algorithm is used everywhere. This algorithm will not allow "Row Level Locking" to work effectively. When this type of code is being executed simultaneously, only first Transaction gains access to the last row of the table. All other Transactions which need the last row will be Blocked until the first Transaction finished.</p>

<p><em>Note: Wherever possible, it is recommended to use the Auto increment property to generate Primary Key (Entry No.) instead of using the above algorithm. </em></p>
<h2>Common Errors</h2><p>Business Central / Dynamics NAV throws the following type of errors related to this topic.</p><ul>
  <li>Another user has modified the record</li>
  <li>A record was locked by another user</li>
  <li>The activity was deadlocked with another user</li>
</ul><h3>Another user has modified the record</h3>
<p>This error message is related to concurrency control, and it has nothing to do with Database Locks.</p>

<p>Business Central / Dynamics NAV maintains a Timestamp column in each table in the SQL Server to maintain row versions. Whenever a record is being Inserted or Modified, system will automatically update a new version number in the Timestamp column. And when a record is being modified, system will check the Timestamp of the current record with the previously saved Record. If it doesn’t match, the system will throw the given error message.</p>

<p>It is very very rare that it happens because another user has actually modified the record. It is usually because of the bad code. </p><blockquote><strong>Business Central:</strong> The changes to the Customer record cannot be saved because some information on the page is not up-to-date.</blockquote>
<blockquote><strong>Dynamics NAV:</strong> Another user has modified the record for this Customer</blockquote><p>The following code generates "Another user has modified the record" error message:</p><pre><code>procedure ModifyCustomer()
var
	Customer: Record Customer;
begin
	Customer.FindFirst();
	SetName(Customer);
	SetEMail(Customer);
	Message('Name: %1, E-Mail: %2', Customer.Name, Customer."E-Mail");
end;

local procedure SetName(Customer: Record Customer)
begin
	Customer.Name := StrSubstNo('Customer - %1', Format(Random(10000)));
	Customer.Modify();
end;

local procedure SetEMail(Customer: Record Customer)
begin
	Customer."E-Mail" := StrSubstNo('cust.%1@test.com', Format(Random(10000)));
	Customer.Modify();
end;</code></pre><p><em>In the above code, the SetEMail function is not aware that the row version of Customer record has changed in the SetName function, and it is trying to change the old version of the Customer record. Concurrency Control doesn't allow to modify the record of an older version.</em></p>

<p>This problem can be fixed by changing Customer parameter to reference type (<em>var Cust: Record Customer</em>), so that all these functions are working on a single record variable instance. </p>
<pre><code>local procedure SetName(var Customer: Record Customer)
begin
	Customer.Name := StrSubstNo('Customer - %1', Format(Random(10000)));
	Customer.Modify();
end;

local procedure SetEMail(var Customer: Record Customer)
begin
	Customer."E-Mail" := StrSubstNo('cust.%1@test.com', Format(Random(10000)));
	Customer.Modify();
end;</code></pre>
<h3>A record was locked by another user</h3><p>When a Transaction is blocked for more than 10 seconds (Lock Time-out), it will be terminated automatically with the following error. </p><blockquote><strong>Business Central:</strong> The operation could not complete because a record in the Sales Line table was locked by another user. Please retry the activity.</blockquote>
<blockquote><strong>Dynamics NAV:</strong> The operation could not complete because a record was locked by another user</blockquote><p>If the below code is executed in two clients simultaneously, it will generate the above error.  </p>
<pre><code>procedure First()
var
	SalesLine: Record "Sales Line";
	Window: Dialog;
	x: Integer;
begin
	Window.Open('#1##########');
	Window.Update(1, 'Waiting ...');
	SalesLine.FindSet();
	repeat
		x += 1;
		Window.Update(1, StrSubstNo('Processing - %1', SalesLine."No."));
		SalesLine.Modify();
		Sleep(1000);
	until (SalesLine.Next() = 0) or (x > 15);
	Window.Close();
	Message('Execution completed successfully.');
end;
</code></pre>
<p><em>This code is going to be executed for more than 20 seconds. 2nd client will exceed the Lock Timeout limit, and it will get the Locking error.</em></p>

<p>The below code can be executed in two clients simultaneously without any Locking errors: </p>
<pre><code>procedure Second()
var
	SalesLine: Record "Sales Line";
	Window: Dialog;
	x: Integer;
begin
	Window.Open('#1##########');
	Window.Update(1, 'Waiting ...');
	SalesLine.FindSet();
	repeat
		x += 1;
		Window.Update(1, StrSubstNo('Processing - %1', SalesLine."No."));
		SalesLine.Modify();
		Sleep(1000);

		Commit();             // To generate a new Transaction
		Sleep(10);            // Allow blocked Transactions to get priority
	until (SalesLine.Next() = 0) or (x > 15);
	Window.Close();
	Message('Execution completed successfully.');
end;</code></pre>
<p>There is a big difference between the first and the second code, though only two lines of code is different. The Code written in the first function creates a Transaction at FindSet() statement and it is going to last till the end for more than 20 seconds. Whereas the Code written in the second function is releasing the Transaction in each iteration, though it is creates a Transaction at FindSet() statement.</p>

<p>Transaction that takes more than 10 seconds can cause locking errors. To avoid locking errors, Transaction should be finished as quickly as possible.</p>
<h3>The activity was deadlocked with another user</h3><p>This error occurs when two transactions are blocking each other. </p>

<p>If LockingSequence1 and LockingSequence2 functions from the below code are executed simultaneously in two different clients, one of the Transactions will be terminated with the deadlock error. </p>
<pre><code>procedure LockingSequence1()
var
	SalesHeader: Record "Sales Header";
	SalesLine: Record "Sales Line";
begin
	Counter1 += 1;
	SalesLine.LockTable();
	SalesLine.FindFirst();

	Sleep(5000);
	if (Counter1 > 1) then
		Message('The code with the "Sales Line, Sales Header" locking sequance has been executed more than once.');

	SalesHeader.LockTable();
	SalesHeader.FindFirst();

	Counter1 := 0;
	Message('Executed successfully.');
end;

procedure LockingSequence2()
var
	SalesHeader: Record "Sales Header";
	SalesLine: Record "Sales Line";
begin
	Counter2 += 1;
	SalesHeader.LockTable();
	SalesHeader.FindFirst();
	Sleep(5000);
	if (Counter2 > 1) then
		Message('The code with the "Sales Header, Sales Line" locking sequance has been executed more than once.');
	SalesLine.LockTable();
	SalesLine.FindFirst();

	Counter2 := 0;
	Message('Executed successfully.');
end;
</code></pre>
<p><em>Note: I have just noticed, if deadlock occurs in Business Central, it is automatically reattempting the failed transaction.</em></p>

<p>Deadlock errors can be fixed by correcting the sequence of Table Locking. The above code can be fixed by following the same Locking sequence in both functions. </p>

<p>Though it is not a good practice, the below code will fix the problem by forcefully locking the tables in the needed sequence.</p>
<pre><code>procedure LockingSequence2Fixed()
var
	SalesHeader: Record "Sales Header";
	SalesLine: Record "Sales Line";
begin
	Counter3 += 1;
	SalesLine.LockTable();
	SalesLine.FindFirst();

	SalesHeader.LockTable();
	SalesHeader.FindFirst();
	Sleep(5000);

	if (Counter3 > 1) then
		Message('The code with the "Sales Line*, Sales Header" locking sequance has been executed more than once.');

	SalesLine.LockTable();
	SalesLine.FindFirst();

	Counter3 := 0;
	Message('Executed successfully.');
end;</code></pre><h2>Job Queues</h2><p>Job Queues can be used to solve Locking Errors. The Transactions that can be executed in the background, can be queued to Job Queue. Job Queue Entry table holds the parameters in order to execute the Transaction and Job Queue processes the Job Queue Entries sequentially.</p>

<p>Locking Errors only occur, when more than one Transactions are executing simultaneously. No Locking Errors would occur, if they are executed sequentially. If the Transactions are delegated to Job Queue to process through Job Queue Entry, the Locking Error problem is almost solved.</p>
<h2>Code Samples in Action</h2><figure><img src="https://static.wixstatic.com/media/394025_a4b9142e0f2e44348ab9961065a52b22~mv2.gif/v1/fit/w_1000,h_629,al_c,q_80/file.png"  ></figure><p>You can download and try the source code from <a href="https://github.com/msnraju/bc-performance-tests" target="_blank" rel="noopener"><u>GitHub</u></a>. </p>
<h2>Conclusion</h2><p>This post is covering some of the problems related to Table Locking issues, but not all of them. There are various other factors that impacts performance and creates locking issues. The key thing to resolve Locking Errors is by optimizing the time to complete the Transactions. This can be done by optimizing the Code, the SQL Queries, the SQL Indexes, and the process.</p>

<p>Happy Coding!!!</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral #DynamicsNAV</p>]]></content:encoded></item><item><title><![CDATA[How to add Custom Filter Tokens in Business Central]]></title><description><![CDATA[Learn How to add Custom Filter Tokens Functionality in Business Central. You use Custom Filter Tokens in page advanced filters to apply filt]]></description><link>https://www.msnjournals.com/post/how-to-add-filter-tokens-in-microsoft-dynamics-365-business-central</link><guid isPermaLink="false">5f2553d877b9a30017080515</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><pubDate>Sat, 01 Aug 2020 12:31:09 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_fe27d4dd900240c4a0fa11132991f8a7~mv2.gif/v1/fit/w_1000,h_652,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Custom Filter Tokens is an interesting functionality in Business Central. Many of us do not know about this functionality. Using this functionality, you can add Filter Tokens like Tomorrow, Yesterday, ThisMonth, PrevMonth, FiscalYear, PrevFiscalYear, NextFiscalYear etc. so that User can use them just like Today and WorkDate Filter Tokens to apply date filters.</p>

<p>You can add Filter Tokens for Date, Time, DateTime and Text data types.</p>
<h2>Filter Tokens [Codeunit 41]</h2>
<p>This exposes functionality that allow users to specify pre-defined filter tokens that get converted to the correct values for various data types when filtering records.</p>

<p>It has the following events you can subscribe:</p><ul>
  <li>OnResolveDateFilterToken</li>
  <li>OnResolveDateTokenFromDateTimeFilter</li>
  <li>OnResolveTextFilterToken</li>
  <li>OnResolveTimeFilterToken</li>
  <li>OnResolveTimeTokenFromDateTimeFilter</li>
</ul><h2>Sample Code</h2>
<p>For demonstration purposes, we are using OnResolveDateFilterToken event to add Tomorrow, Yesterday, ThisMonth, PrevMonth, FiscalYear, PrevFiscalYear and NextFiscalYear tokens.</p>
<h3>Date Filter Tokens Impl [DateFilterTokensImpl.Codeunit.al]</h3><pre><code>codeunit 50103 "Date Filter Tokens Impl"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Filter Tokens", 'OnResolveTimeTokenFromDateTimeFilter', '', false, false)]
    local procedure OnResolveDateFilterToken(DateToken: Text; var FromDate: Date; var ToDate: Date; var Handled: Boolean)
    begin
        case UpperCase(DateToken) of
            'YESTERDAY', 'YD':
                begin
                    FromDate := CalcDate('-1D', Today);
                    ToDate := FromDate;
                    Handled := true;
                end;
            'TOMORROW', 'TO':
                begin
                    FromDate := CalcDate('-1D', Today);
                    ToDate := FromDate;
                    Handled := true;
                end;
            'THISMONTH', 'TM':
                begin
                    FromDate := CalcDate('CM - 1M + 1D', Today);
                    ToDate := CalcDate('CM', Today);
                    Handled := true;
                end;
            'PREVMONTH', 'PM':
                begin
                    FromDate := CalcDate('CM - 2M + 1D', Today);
                    ToDate := CalcDate('CM - 1M', Today);
                    Handled := true;
                end;
            'FISCALYEAR', 'FY':
                begin
                    FromDate := GetFiscalYear();
                    ToDate := CalcDate('+12M - 1D', FromDate);
                    Handled := true;
                end;
            'PREVFISCALYEAR', 'PFY':
                begin
                    FromDate := CalcDate('-1Y', GetFiscalYear());
                    ToDate := CalcDate('+12M - 1D', FromDate);
                    Handled := true;
                end;
            'NEXTFISCALYEAR', 'NFY':
                begin
                    FromDate := CalcDate('+1Y', GetFiscalYear());
                    ToDate := CalcDate('+12M - 1D', FromDate);
                    Handled := true;
                end;
        end;
    end;

    local procedure GetFiscalYear(): Date
    var
        AccountingPeriod: Record "Accounting Period";
    begin
        AccountingPeriod.Reset();
        AccountingPeriod.SetRange("New Fiscal Year", true);
        AccountingPeriod.SetRange(Closed, false);
        if AccountingPeriod.FindFirst() then
            exit(AccountingPeriod."Starting Date");
    end;
}
</code></pre><h2>Filter Tokens in Action:</h2><figure><img src="https://static.wixstatic.com/media/394025_fe27d4dd900240c4a0fa11132991f8a7~mv2.gif/v1/fit/w_1000,h_652,al_c,q_80/file.png"  ></figure>,Conclusion:<p>Users will definitely like this functionality because you are saving lot of his / her time. For example, If User can enter FiscalYear as Date Filter instead of recollecting the start and the end date of the fiscal year and typing it manually, he can save a lot of his time.</p>
<p> </p>
<p>You can download the source code from <a href="https://github.com/msnraju/filter-tokens" target="_blank" rel="noopener"><u>GitHub</u></a>.</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral</p>]]></content:encoded></item><item><title><![CDATA[How to Create Assisted Setups in Business Central]]></title><description><![CDATA[Assisted Setup is a wizard page to help Business Central User setting up a module. When you are creating an extension for Business...]]></description><link>https://www.msnjournals.com/post/how-to-create-assisted-setup-for-your-business-central-extension</link><guid isPermaLink="false">5f22a8adcc80c50017f71ac3</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><pubDate>Thu, 30 Jul 2020 11:33:57 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_f57643252bbe4d6faad91bc432ffc01e~mv2.png/v1/fit/w_1000,h_492,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>Assisted Setup is a wizard page to help Business Central User setting up a module. When you are creating an extension for Business Central, you must consider creating one or more Assisted Setups for your extension. </p>
<h2>Wizard Page:</h2><p>Assisted Setup is nothing but a wizard page (guided page) containing one or more steps to help Business Central User to configure setups.</p>
<h3>Page layout:</h3><p>Page contains at least two steps, start and finish (welcome and thanks) steps. Each step is a field group and visibility which are controlled by a boolean variable or a step number condition.</p>

<p>For example, VAT Setup Wizard contains the following steps:</p><ul>
  <li>Welcome to VAT Setup</li>
  <li>VAT Business Posting Groups</li>
  <li>VAT Product Posting Setup</li>
  <li>Assign VAT Setup to Customer, Vendor, and Item Templates</li>
  <li>Manual setup required</li>
  <li>That''s it!</li>
</ul><h3>Actions:</h3><p>There will be Back, Next, Finish actions to navigate between steps, again actions are enabled or disabled based on the state of some variables. </p>
<p>For example, Back is disabled in the first step, and Next is disabled in the last step.</p>
<h3>Assisted Setup [Codeunit 3725]</h3><p>Assisted Setup is system codeunit which allows you to register your wizard page and execute your wizard page when the User opens it from Assisted Setup.</p>

<p>Assisted Setup codeunit has the following events:</p><ul>
  <li>OnRegister - Triggered when Assisted Setup list page is being opened. </li>
  <li>OnReRunOfCompletedSetup - Triggered if the previously completed Assisted Setup is being run again.</li>
  <li>·OnBeforeOpenRoleBasedSetupExperience - Triggered when Assisted Setup is opened from Navigation bar >> Settings >> Assisted setup</li>
  <li> OnAfterRun - Triggered after the Assisted Setup has finished.</li>
</ul>,Best Practices:<ul>
  <li>First step should contain Welcome message, and explanation of the wizard page.</li>
  <li>All actions (Back, Next, Finish) should validate User Inputs before navigating it to the next or back step.</li>
  <li>Only the Finish step should write data into actual tables. This is very critical because User can close the wizard page any time. Also, you should keep data in intermediate tables so that if the User opens the wizard page again, he doesn’t need to enter the same data again.</li>
</ul>,,,Code Sample<p>Let us take a basic example; we will create an Assisted Setup to setup Sales Order, and Sales Invoice document Number Series.</p>
<p>We need minimum two objects; a codeunit to subscribe Assisted Setup codeunit events, and a page for the wizard page.</p><h3>Assisted Setup Subscribers [NoSeriesSetupSubscribers.Codeunit.al]</h3><p>We are subscribing Assisted Setup events to register our “No. Series Setup Wizard” page.</p>
<pre><code>codeunit 50120 "No. Series Setup Subscribers"
{
    var
        Info: ModuleInfo;
        SetupWizardTxt: Label 'Set up Sales No. Series';
        x: Codeunit "Assisted Setup Subscribers";

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Assisted Setup", 'OnRegister', '', false, false)]
 local procedure Initialize()
 var
        AssistedSetup: Codeunit "Assisted Setup";
        Language: Codeunit Language;
        CurrentGlobalLanguage: Integer;
 begin
        CurrentGlobalLanguage := GlobalLanguage;
        AssistedSetup.Add(
            GetAppId(),
 Page::"No. Series Setup Wizard",
            SetupWizardTxt,
            "Assisted Setup Group"::GettingStarted,
 '',
            "Video Category"::Uncategorized,
 '');

        GlobalLanguage(Language.GetDefaultApplicationLanguageId());

 //Adds the translation for the name of the setup.
        AssistedSetup.AddTranslation(
 Page::"No. Series Setup Wizard",
            Language.GetDefaultApplicationLanguageId(),
            SetupWizardTxt);
        GlobalLanguage(CurrentGlobalLanguage);
 end;

 local procedure GetAppId(): Guid
 var
        EmptyGuid: Guid;
 begin
 if Info.Id() = EmptyGuid then
            NavApp.GetCurrentModuleInfo(Info);

 exit(Info.Id());
 end;
}
</code></pre>
<h3>Wizard Page [NoSeriesSetupWizard.Page.al]</h3><p>This page contains 3 steps, first step contains introduction, second step takes inputs from the User, and the third setup will finish the wizard by updating fields in Sales & Receivable Setup table.</p>
<pre><code>page 50120 "No. Series Setup Wizard"
{
    Caption = 'Sales No. Series Setup';
    PageType = NavigatePage;
    SourceTable = "Sales & Receivables Setup";
    SourceTableTemporary = true;

    layout
    {
        area(content)
        {
            group(FirstStep)
            {
                Visible = CurrentPage = 1;
                group("Welcome to Email Setup")
                {
                    Caption = 'Welcome to Sales Number Series Setup';
                    Visible = CurrentPage = 1;
                    ;
                    group(Control18)
                    {
                        InstructionalText = 'You can setup Sales Order, Sales Invoice document Number Series.';
                        ShowCaption = false;
                    }
                }
                group("Let's go!")
                {
                    Caption = 'Let''s go!';
                    group(Control22)
                    {
                        InstructionalText = 'Choose Next so you can configure Number Series for Sales Orders, Sales Invoices.';
                        ShowCaption = false;
                    }
                }
            }

            group(Step2)
            {
                Caption = '';
                Visible = CurrentPage = 2;
                group("Para2.1")
                {
                    Caption = 'Select Number Series for Sales Orders';
                    field("Order Nos."; Rec."Order Nos.")
                    {
                        ApplicationArea = Basic, Suite;
                        ShowCaption = false;
                    }
                }
                group("Para2.2")
                {
                    Caption = 'Select Number Series for Sales Invoices';
                    field("Invoice Nos."; Rec."Invoice Nos.")
                    {
                        ApplicationArea = Basic, Suite;
                        ShowCaption = false;
                    }
                }
            }
            group(Step3)
            {
                ShowCaption = false;
                Visible = CurrentPage = 3;
                group("That's it!")
                {
                    Caption = 'That''s it!';
                    group(Control25)
                    {
                        InstructionalText = 'To update Number Series for sale documents, choose Finish.';
                        ShowCaption = false;
                    }
                }
            }
        }
    }

    actions
    {
        area(processing)
        {
            action(BackAction)
            {
                ApplicationArea = Basic, Suite;
                Caption = '&Back';
                Enabled = (CurrentPage > 1) AND (CurrentPage < 3);
                Image = PreviousRecord;
                InFooterBar = true;
                Promoted = true;

                trigger OnAction()
                begin
                    CurrentPage := CurrentPage - 1;
                    CurrPage.Update;
                end;
            }
            action(NextAction)
            {
                ApplicationArea = Basic, Suite;
                Caption = '&Next';
                Enabled = (CurrentPage >= 1) AND (CurrentPage < 3);
                Image = NextRecord;
                InFooterBar = true;
                Promoted = true;

                trigger OnAction()
                begin
                    case CurrentPage of
                        2:
                            begin
                                Rec.TestField("Order Nos.");
                                Rec.TestField("Invoice Nos.");
                            end;
                    end;

                    CurrentPage := CurrentPage + 1;
                    CurrPage.Update(false);
                end;
            }
            action(FinishAction)
            {
                ApplicationArea = Basic, Suite;
                Caption = '&Finish';
                Enabled = CurrentPage = 3;
                Image = Approve;
                InFooterBar = true;
                Promoted = true;

                trigger OnAction()
                var
                    AssistedSetup: Codeunit "Assisted Setup";
                begin
                    SalesSetup.Get();
                    SalesSetup."Order Nos." := Rec."Order Nos.";
                    SalesSetup."Invoice Nos." := Rec."Invoice Nos.";
                    SalesSetup.Modify();

                    AssistedSetup.Complete(PAGE::"No. Series Setup Wizard");
                    CurrPage.Close;
                end;
            }
        }
    }

    trigger OnInit()
    begin
        SalesSetup.Get();
        Rec := SalesSetup;
        CurrentPage := 1;
    end;

    trigger OnOpenPage()
    begin
        Insert;
    end;

    trigger OnQueryClosePage(CloseAction: Action): Boolean
    var
        AssistedSetup: Codeunit "Assisted Setup";
        Info: ModuleInfo;
    begin
        if CloseAction = Action::OK then
            if AssistedSetup.ExistsAndIsNotComplete(Page::"No. Series Setup Wizard") then
                if not Confirm(NAVNotSetUpQst, false) then
                    Error('');
    end;

    var
        SalesSetup: Record "Sales & Receivables Setup";
        CurrentPage: Integer;
        NAVNotSetUpQst: Label 'The Sales No. Series Setup has not been set up.\Are you sure you want to exit?';
}</code></pre><h2>Assisted Setup in Action</h2><p> </p>
<figure><img src="https://static.wixstatic.com/media/394025_f57643252bbe4d6faad91bc432ffc01e~mv2.png/v1/fit/w_1000,h_492,al_c,q_80/file.png"  ></figure><figure><img src="https://static.wixstatic.com/media/394025_9627797d9eb048ee9710ae678442de3c~mv2.gif/v1/fit/w_576,h_521,al_c,q_80/file.png"  ></figure><h2>Conclusion</h2><p>Assisted Setup are the best way to ask your User to configure setup data for your extension. You don’t need to provide training to your Users, because all the steps in the Assisted Setup are intuitive and guided with instructions.</p>
<p> </p>
<p>You can download source code from <a href="https://github.com/msnraju/assisted-setup-sample" target="_blank" rel="noopener"><u>GitHub</u></a>.</p>

<p>#MsDyn365 #MsDyn365BC #BusinessCentral</p>]]></content:encoded></item><item><title><![CDATA[How to Create Control Add-in in Business Central]]></title><description><![CDATA[Createing Controls Add-in is not that difficult. Just read this post you will understand how easy it is.]]></description><link>https://www.msnjournals.com/post/how-to-create-control-add-in-in-microsoft-dynamics-365-business-central</link><guid isPermaLink="false">5f20d57cc2c5090017ec8315</guid><category><![CDATA[Microsoft Dynamics 365]]></category><category><![CDATA[Dynamics NAV | Business Central ]]></category><category><![CDATA[AL Language]]></category><pubDate>Wed, 29 Jul 2020 03:02:46 GMT</pubDate><enclosure url="https://static.wixstatic.com/media/394025_b631276c67ed46f78e51031e6415b2e3~mv2.gif/v1/fit/w_1000,h_628,al_c,q_80/file.png" length="0" type="image/png"/><dc:creator>MSN Raju</dc:creator><content:encoded><![CDATA[<p>This post explains you what is Control Add-in and you will learn how to create Control Add-in using existing resources on the internet. Sample code contains Carousel Control Control Add-in using Bootstrap resources. You can download source code from <a href="https://github.com/msnraju/control-add-in-samples" target="_blank" rel="noopener"><u>github</u></a>. In the conclusion talked about why to use Control Add-in, what advantage Business Central User gains.</p>
<h2>Introduction</h2><p>Control Add-in is a special type of object in Business Central to create User Control. In other words Control Add-in (User Control) is a web component developed using JavaScript, HTML and CSS that can interact with Business Central. You need User Control when you do not have the required control available in standard Business Central. Also you need User Control when you want to simplify User Interface (UI) for your Business Central User.</p>
<h2>Required Skills</h2><p>You need to have the following skills to develop Control Add-ins:</p><p>• AL - Good
• HTML - Basic
• CSS - Basic
• JavaScript - Basic</p>

<p>If you are smart enough, you just need to know how to copy paste :)</p>
<p>If you are new to <a href="https://www.w3schools.com/html/" target="_blank" rel="noopener">HTML</a>, <a href="https://www.w3schools.com/css/" target="_blank" rel="noopener">CSS</a>, and <a href="https://www.w3schools.com/js/" target="_blank" rel="noopener">JavaScript</a>, in my opinion <a href="https://www.w3schools.com/html/" target="_blank" rel="noopener">w3shools.com</a> is the best place to learn quickly.</p>
<h2>Best Practices</h2><p>You should always try to create reusable Control Add-ins. You should be able to use the same User Control in more than one page.</p>
<p>If you do not hard-code:</p><ul>
  <li>A 'Map Control' can be used, in Customer, Vendor, Bank, Location pages.</li>
  <li>A 'Status Indicator Control' can be used in Sales, Purchase, and Service Orders, may also be in Bank Reconciliation page.</li>
</ul><p>In the example given below, if you hard-code slides data in Control Add-in instead of sending as a parameter, you can not use this in other pages.</p>
<h2>Let's create a Control Add-in</h2><p>We don’t need to develop Control Add-in from scratch. There are tons of open source web components available on the web. We just need to understand the component and convert it to Control Add-in.</p>

<p>We are to using the following resources from bootstrap to create Control Add-in:</p>

<p><strong>JavaScript</strong></p><pre><code>https://code.jquery.com/jquery-3.5.1.slim.min.js
https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js
https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js</code></pre><p><strong>CSS</strong></p><pre><code>https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css</code></pre><p><a href="https://getbootstrap.com/docs/4.5/components/carousel/#with-captions" target="_blank" rel="nofollow">,<u><strong>HTML</strong></u></a></p><pre><code><div id="carouselExampleCaptions" class="carousel slide" data-ride="carousel">
  <ol class="carousel-indicators">
    <li data-target="#carouselExampleCaptions" data-slide-to="0" class="active"></li>
    <li data-target="#carouselExampleCaptions" data-slide-to="1"></li>
    <li data-target="#carouselExampleCaptions" data-slide-to="2"></li>
  </ol>
  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="..." class="d-block w-100" alt="...">
      <div class="carousel-caption d-none d-md-block">
        <h5>First slide label</h5>
        <p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
      </div>
    </div>
    <div class="carousel-item">
      <img src="..." class="d-block w-100" alt="...">
      <div class="carousel-caption d-none d-md-block">
        <h5>Second slide label</h5>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
      </div>
    </div>
    <div class="carousel-item">
      <img src="..." class="d-block w-100" alt="...">
      <div class="carousel-caption d-none d-md-block">
        <h5>Third slide label</h5>
        <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur.</p>
      </div>
    </div>
  </div>
  <a class="carousel-control-prev" href="#carouselExampleCaptions" role="button" data-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="sr-only">Previous</span>
  </a>
  <a class="carousel-control-next" href="#carouselExampleCaptions" role="button" data-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="sr-only">Next</span>
  </a>
</div></code></pre>
<p>Now we are creating Carousel Control using the above resources.</p>
<h3>1. Control Add-in Object [ CarouselControl.ControlAddin.al ]</h3><p>This Carousel Control has OnStartup trigger, which will be triggered once the User Control is loaded.</p>
<p>SetCarouselData is a function that will send the slides data to Control Add-in.</p><pre><code>controladdin "Carousel Control"
{
    HorizontalStretch = true;
    RequestedHeight = 200;

    // JS files required for Bootstrap
    Scripts = 'https://code.jquery.com/jquery-3.5.1.slim.min.js',
        'https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js',
        'https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js';

    StartupScript = 'src/startup.js';

    // Bootstrap css
    StyleSheets = 'https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css';

    procedure SetCarouselData(Data: JsonObject);
    event OnStartup();
}</code></pre><h3>2. Startup JavaScript file [ startup.js ]</h3><p>This JavaScript file will be loaded once the User Control is ready. We are invoking OnStartup trigger so that the hosting page can receive the event and send the data back to the User Control.</p><pre><code>Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('OnStartup')

function carouselIndicators(slides) {
    var indicators = [];
    for (var i = 0; i < slides.length; i++) {
        var cssClass = i === 0 ? 'class="active"' : '';
        indicators.push(`<li data-target="#carouselExampleCaptions" data-slide-to="${i}" ${cssClass}></li>`);
    }

    return `<ol class="carousel-indicators">${indicators.join('')}</ol>`;
}

function carouselItems(slides) {
    var carouselItems = [];
    for (var i = 0; i < slides.length; i++) {
        var slide = slides[i];
        var cssClass = i === 0 ? 'active' : '';

        carouselItems.push(`
<div class="carousel-item ${cssClass}">
    <img src="${slide.image}" class="d-block w-100" alt="${slide.title}" style="height: 200px">
        <div class="carousel-caption d-none d-md-block">
        <h5>${slide.title}</h5>
        <p>${slide.description}</p>
    </div>
</div>`);
    }

    return `<div class="carousel-inner">${carouselItems.join('')}</div>`;
}

function carouselMarkup(data) {
    return `
<div id="carouselExampleCaptions" class="carousel slide" data-ride="carousel">
    ${carouselIndicators(data.slides)}
    ${carouselItems(data.slides)}
    <a class="carousel-control-prev" href="#carouselExampleCaptions" role="button" data-slide="prev">
        <span class="carousel-control-prev-icon" aria-hidden="true"></span>
        <span class="sr-only">Previous</span>
    </a>
    <a class="carousel-control-next" href="#carouselExampleCaptions" role="button" data-slide="next">
        <span class="carousel-control-next-icon" aria-hidden="true"></span>
        <span class="sr-only">Next</span>
    </a>
</div>`;
}

window.SetCarouselData = function (data) {
    try {
        var markup = carouselMarkup(data);
        document.getElementById('controlAddIn').innerHTML = markup;
        $('#carouselExampleCaptions').carousel();
        console.log(markup);
    } catch (err) {
        console.log(err);
    }
}</code></pre><h3>3. Page [ CustomerListExt.PageExt.al ]</h3><p>We are extending the Customers page to add Carousel Control at the top of the customers list. When the OnStartup event is triggered, we are preparing the Json Object that contains the slides data and sending it to Carousel Control.</p><pre><code>pageextension 50100 CustomerListExt extends "Customer List"
{
    layout
    {
        addbefore(Control1)
        {
            usercontrol(Carousel; "Carousel Control")
            {
                trigger OnStartup()
                var
                    JObject: JsonObject;
                    Slides: JsonArray;
                begin
                    Slides.Add(AddSlide('Keep your promises', 'check before you make a promise', '//unsplash.it/1024/200'));
                    Slides.Add(AddSlide('Never forget', 'always register your conversations to ensure you follow-up promptly', '//unsplash.it/1025/200'));
                    Slides.Add(AddSlide('Qualify', 'be picky about which opportunities to spend time on', '//unsplash.it/1024/201'));
                    JObject.Add('slides', Slides);
                    CurrPage.Carousel.SetCarouselData(JObject);
                end;
            }
        }
    }

    local procedure AddSlide(Title: Text; Description: Text; Image: Text): JsonObject
    var
        Slide: JsonObject;
    begin
        Slide.Add('title', Title);
        Slide.Add('description', Description);
        Slide.Add('image', Image);
        exit(Slide);
    end;
}</code></pre><h2>Carousel Control in Action</h2><figure><img src="https://static.wixstatic.com/media/394025_b631276c67ed46f78e51031e6415b2e3~mv2.gif/v1/fit/w_1000,h_628,al_c,q_80/file.png"  ></figure><h2>Conclusion</h2>
<p>If we think about it, The Users don't actually need training to work on portals just because they are intuitive. But in the case of Business Applications, we definitely need to provide training to the Business Users, where they already know their business more than us. Isn't it funny! It is simply because they are not provided proper and intuitive User Interfaces. I think we should try to provide proper UI to the Users so that it is convenient for their use. Proper UI means that we should not restrict our self only to the available controls. Where ever required, we should definitely try to create Control Add-ins / User Controls for better user experience.</p>

<p>Use Control Add-ins - Help customers!!</p>

<p>You can download the source code from <a href="https://github.com/msnraju/control-add-in-samples" target="_blank" rel="noopener">Github</a></p>

<p>#msdynd365 #msdyn365bc #businesscentral</p>]]></content:encoded></item></channel></rss>