Accessing Sage 300c’s Business Logic from the Web UIs

6 minute read time.

Introduction

In the Sage 300 VB UIs, a user would do something in the UI (press a button or tab out of a field) and then the VB UI would be notified of this and would possibly execute a number of Sage 300 Business Logic (View) calls and based on their results update various other fields and possibly provide user feedback via a message box.

In the Web UIs we want to do similar processing since we want to re-use the tried and true Sage 300 Business Logic, but we have to be careful since now the Web UI is half running as JavaScript in the Browser and half running as .Net assemblies on the server. We have to be careful of the communication between the Browser and the server since there will be quite a bit of latency in each call over the Internet. Generally, we never want one user action to result in more than one call to the server (and ideally most user actions shouldn’t result in any calls to the server).

This blog post talks about where you put your code to access the Sage 300 Business Logic and how a UI interaction in the Browser flows through the system to execute this business logic.

Architecture

In the new Web UI architecture, we access the Sage 300 Business logic from our Business Repository classes. The base classes for these provide a wrapper of the Sage 300 .Net API to actually access the Views, but hiding the details of things like session and database link management. Then above this layer are the usual ASP.Net MVC Models and Controllers.

Generally, we want to put all this logic in the Business Repository so it can be used by multiple higher level clients including the Web UIs, our new RESTful WebAPI and services which are available for other applications to utilize.

Some of the layering is in place ready for additional functionality like customization. We need provide the common interfaces that can act as the basis for programmatic customization by inserting custom modules into the processing flow via Unity Interception.

Moving VB Code

In VB we often make lots of Business Logic (View) calls all interspersed with lots of interactions with various UI controls. This code has to be separated where the Business Logic (View) calls will go in the Business Repository which runs on the server and then the part that interacts with the controls has to move to the JavaScript code running in the Browser. The Business Repository has to provide the necessary data in a single payload which the mode/controller will transport to the Browser for processing.

The easiest way for the repository to transfer data is to have the model provide extra fields for this communication. This way no extra layers need to be involved, the business repository just populates these fields and the JavaScript layers pull them out of the returned JSON object and use them.

But you only want to add so much to the model, since you don’t want it to be too cumbersome to move around and you might want more focused calls. For these we usually define special calls in the controller and these go through a services layer to execute the code in the repository. The service call only passes the exact data needed (like parameters to a function) and knows what data to expect back.

Example

Adding extra fields to the model is fairly straight forward, so let’s trace through the logic of making a services call. In this example we’ll look at the simple case of checking a customer’s credit limit in A/R Invoice Entry (which is using a stateful business repository). We’ll start up in the JavaScript code and work our way down through the layers to get an idea of who does what.

So let’s start near the top. In the A/R Invoice Entry UI there are various times when the credit limit needs to be looked up. So the JavaScript code in the InvoiceEntryBehaviour.js file has a routine to initiate this process. Note that server calls are asynchronous so the response is handled in a callback function.

    showCreditLimit: function (result) {

        // Open Credit Check pop up window

        if (result) {

            var jsonResult = JSON.parse(result);

            if (jsonResult.ShowCreditCheck) {

                arInvoiceEntryRepository.getCreditCheck(jsonResult.id,

                    sg.utls.kndoUI.getFormattedDate(jsonResult.docDate),

                    sg.utls.kndoUI.getFormattedDate(jsonResult.dueDate),

                    "n" + invoiceEntryUI.CurrencyDecimals, jsonResult.totalPaymentAmountScheduled,

                    jsonResult.prepaymentAmount);

            } else {

                onSuccess.onCreditClose();

            }

        }

        invoiceEntryUI.ModelData.isModelDirty.reset();

    },

This calls a function in the InvoiceEntryRepository.js file to actually make the call to the server:

    getCreditCheck: function (customerNumber, documentDate, dueDate, decimals, invoiceAmount, prepaymentAmount) {

        var data = {

            id: customerNumber,

            docDate: documentDate,

            dueDate: dueDate,

            decimals: decimals,

            totalPaymentAmountScheduled: invoiceAmount,

            prepaymentAmount: prepaymentAmount

        };

        sg.utls.ajaxPostHtml(sg.utls.url.buildUrl("AR", "InvoiceEntry", "GetCreditLimit"), data, onSuccess.loadCreditLimit);

    },

This will initiate the call to the server. The URL will be built something like servername/Sage300/AR/InvoiceEntry/GetCreditLimit. The ASP.Net MVC infrastructure will use configuration by convention to look for a matching entry point in a loaded controller and hence call the  GetCreditLimit method in the InvoiceEntryController.cs file:

        [HttpPost]

        public virtual ActionResult GetCreditLimit(string id, string docDate, string dueDate, string decimals,decimal totalPaymentAmountScheduled, decimal prepaymentAmount)

        {

            try

            {

                return PartialView(AccountReceivable.ARInvoiceCreditCheck, ControllerInternal.GetCreditLimit(id, docDate, dueDate, decimals, totalPaymentAmountScheduled, prepaymentAmount));

            }

            catch (BusinessException businessException)

            {

                return JsonNet(BuildErrorModelBase(CommonResx.NotFoundMessage, businessException, InvoiceEntryResx.Entity));

            }

        }

Which will call the InvoiceControllerInternal.cs GetCreditLimit method:

        internal ViewModelBase<CustomerBalance> GetCreditLimit(string customerNumber, string documentDate, string dueDate, string decimals

            , decimal totalPaymentAmountScheduled, decimal prepaymentAmount)

        {

            var creditBalance = Service.GetCreditLimit(customerNumber, totalPaymentAmountScheduled, prepaymentAmount);

 

            if (creditBalance.CalcCustomerOverdue == CalcCustomerOverdue.Yes &&

                creditBalance.CustomerBalanceOverdue > creditBalance.CustomerAmountOverdue)

            {

                creditBalance.CustomerCreditMessage = string.Format(InvoiceEntryResx.CustCreditDaysOverdue,

                        creditBalance.CustomerDaysOverdue, creditBalance.CustomerBalanceOverdue.ToString(decimals),

                        creditBalance.CustomerAmountOverdue.ToString(decimals));

            }

 

            if (creditBalance.CalcNatAcctOverdue == CalcNatAcctOverdue.Yes &&

                creditBalance.NatAcctBalanceOverdue > creditBalance.NatAcctAmountOverdue)

            {

                creditBalance.NationalCreditMessage = string.Format(InvoiceEntryResx.NatCreditDaysOverdue,

                        creditBalance.NatAcctDaysOverdue, creditBalance.NatAcctBalanceOverdue.ToString(decimals),

                        creditBalance.NatAcctAmountOverdue.ToString(decimals));

            }

 

            creditBalance.CustomerCreditLimit = Convert.ToDecimal(creditBalance.CustomerCreditLimit.ToString(decimals));

            creditBalance.CustomerBalanceVal = Convert.ToDecimal(creditBalance.CustomerBalanceVal.ToString(decimals));

            creditBalance.PendingARAmount = Convert.ToDecimal(creditBalance.PendingARAmount.ToString(decimals));

            creditBalance.PendingOEAmount = Convert.ToDecimal(creditBalance.PendingOEAmount.ToString(decimals));

            creditBalance.PendingOtherAmount = Convert.ToDecimal(creditBalance.PendingOtherAmount.ToString(decimals));

            creditBalance.CurrentARInvoiceAmount = Convert.ToDecimal(creditBalance.CurrentARInvoiceAmount.ToString(decimals));

            creditBalance.CurrentARPrepaymentAmount = Convert.ToDecimal(creditBalance.CurrentARPrepaymentAmount.ToString(decimals));

            creditBalance.CustomerOutstanding = Convert.ToDecimal(creditBalance.CustomerOutstanding.ToString(decimals));

            creditBalance.CustomerLimitExceeded = Convert.ToDecimal(creditBalance.CustomerLimitExceeded.ToString(decimals));

            creditBalance.NatAcctCreditLimit = Convert.ToDecimal(creditBalance.NatAcctCreditLimit.ToString(decimals));

            creditBalance.NationalAccountBalance = Convert.ToDecimal(creditBalance.NationalAccountBalance.ToString(decimals));

            creditBalance.NatAcctOutstanding = Convert.ToDecimal(creditBalance.NatAcctOutstanding.ToString(decimals));

            creditBalance.NatAcctLimitLeft = Convert.ToDecimal(creditBalance.NatAcctLimitLeft.ToString(decimals));

            creditBalance.NatAcctLimitExceeded = Convert.ToDecimal(creditBalance.NatAcctLimitExceeded.ToString(decimals));

 

            return new ViewModelBase<CustomerBalance> { Data = creditBalance };

        }

This routine first calls the GetCreditLimit service in InvoiceEntryEntityService.cs:

        public virtual CustomerBalance GetCreditLimit(string customerNumber, decimal totalPaymentAmountScheduled, decimal prepaymentAmount)

        {

            var repository = Resolve<IInvoiceEntryEntity<TBatch, THeader, TDetail, TPayment, TDetailOptional>>();

            return repository.GetCreditLimit(customerNumber, totalPaymentAmountScheduled, prepaymentAmount);

        }

Who then calls the repository GetCreditLimit routine in InvoiceEntryRepository.cs. This routine then appears to do regular View processing using the base repository wrapper routines that insulate us from the session/dblink handling logic as well as do some basic error processing:

        public virtual CustomerBalance GetCreditLimit(string customerNum, decimal totalPaymentAmountScheduled, decimal prepaymentAmount)

        {

            _header.Read(false);

            _creditCheck.SetValue(CustomerBalance.Fields.CustomerNumber, customerNum);

            _creditCheck.SetValue(CustomerBalance.Fields.CurrentARInvoiceAmount, totalPaymentAmountScheduled);

            _creditCheck.SetValue(CustomerBalance.Fields.CurrentARPrepaymentAmount, prepaymentAmount);

            _creditCheck.Process();

            return _creditCheckMapper.Map(_creditCheck);

        }

Finally, down in the business repository, the code should look fairly familiar to anyone you has done any C# coding using our Sage 300 .Net API. Further this code should also appear somewhere in the matching VB code, and besides being translated to using the .Net API, its become quite separated from the UI control code (in this case the JavaScript).

At the end of this all the calls return propagating the returned data back to the Browser in answer to the AJAX call that it made.

It might look like a lot of code here, but remember the business repository and JavaScript bits have corresponding VB code. Then the other layers are there to make all the code more re-usable so that it can be used in contexts like WebAPIs and allow interfaces to provide the hooks needed for customization.

Summary

This article is intended to give you an idea of where to put your code that accesses the Sage 300 Business Logic and then how to call that from the Web UIs. There are a lot of layers but individually most of the layers are fairly simple and most of the code will appear in the Business Repository and the JavaScript behavior code.