Stephen Smith's Blog

Musings on Machine Learning…

Posts Tagged ‘server programming

Sage 300 Server Side Programming Examples

with 4 comments

Introduction

Now that the official rebranding announcement has happened, I’m going to start using the new branding name of Sage 300 ERP. Perhaps now and then referring to it as the product formerly known as Sage ERP Accpac or perhaps formerly known as CA-ACCPAC/2000.

Last week I blogged on the structure of our new Sage 300 server side programming framework. This week I’ll present a few examples of the sorts of things you can do. Of course from this framework you can do anything you can do from Java as well as have easy full access to all the Sage 300 Business Logic, this gives you a lot of power to program what you need.

The Java classes in this article have all been shortened to keep the length of this posting down and so that the main points don’t get lost in a lot of comments or other incidental functionality. So all the imports’, most comments and then all methods which don’t represent the main point, have been removed.

Fun with Fields

You don’t actually need to create a new class for every field you want to adapt the behavior of. You can actually use the same field class for all the fields, or a group of fields. So for example in the Order Header, there are many fields to do with source currency where we need to set the correct number of decimal places and similarly there are many fields to do with PJC, many fields to do with tax reporting currency, etc. So in the Order Header view we can get all these processed by one class for all the header fields (except for one):

   protected SDataViewField createField(int index, int id, String name)
   {
      switch (id)
      {
         case OEORDH.IDX_NONINVABLE:
            return new PJCProjectInvoicingField(this, index, id, name);
         default: // All the fields except for the one above.
            return new OEOrdersHeaderViewField(this, index, id, name);
      }
   }

We set one special field to do with PJC and then we have the OEOrdersHeaderViewField class extend all other fields in the Order Header View.

public class OEOrdersHeaderViewField extends SDataViewField
{
   private static final List<Integer> MONEY_FIELDS_SOURCE_CURRENCY;
   static
   {
      MONEY_FIELDS_SOURCE_CURRENCY = new ArrayList<Integer>();
      MONEY_FIELDS_SOURCE_CURRENCY.add(OEORDH.IDX_TERMTTLDUE);
      MONEY_FIELDS_SOURCE_CURRENCY.add(OEORDH.IDX_DISCAVAIL);
      MONEY_FIELDS_SOURCE_CURRENCY.add(OEORDH.IDX_APPROVELMT);
      MONEY_FIELDS_SOURCE_CURRENCY.add(OEORDH.IDX_ORDTOTAL);
 ...
   }
   private static final List<Integer> MONEY_FIELDS_TAXREPORTING_CURRENCY;
   static
   {
                  MONEY_FIELDS_TAXREPORTING_CURRENCY = new ArrayList<Integer>();
                  MONEY_FIELDS_TAXREPORTING_CURRENCY.add(OEORDH.IDX_OTREAMNT1);
...
   }  
   private static final List<Integer> NON_FRACTION_FIELDS;
   static
   {
      NON_FRACTION_FIELDS = new ArrayList<Integer>();
      NON_FRACTION_FIELDS.add(OEORDH.IDX_ORDLINES);
...
   }
   private static Short               OIP_PROCESSCMD_INSERT_OF = 1;
   protected OEOrdersHeaderViewField(OEOrdersHeaderView view,
       int index, int id, String name)
   {
      super(view, index, id, name);
   }
   public boolean isEditable()
   {
      switch (getID())
      {
         case OEORDH.IDX_TEMPLATE:
            return !super.getView().getExists();
         default:
            return super.isEditable();
      }
   }  
   public int getDecimalPlaces()
   {
      int decimals;
      Integer fieldIdx = getID();
      if (MONEY_FIELDS_SOURCE_CURRENCY.contains(fieldIdx))
      {
         final String currencyCode = (String)getView().get(OEORDH.IDX_ORSOURCURR);
         decimals = getView().getParent().getCurrency(currencyCode).getDecimals();
      }
      else if (MONEY_FIELDS_TAXREPORTING_CURRENCY.contains(fieldIdx))
      {
         final String currencyCode = (String)getView().get(OEORDH.IDX_OTRCURRNCY);
         decimals = getView().getParent().getCurrency(currencyCode).getDecimals();
      }
      else if (NON_FRACTION_FIELDS.contains(fieldIdx))
      {
         decimals = 0;
      }
      else
      {
         decimals = super.getDecimalPlaces();
      }
      return decimals;
   }
   public int setValue(Object newValue, boolean verify)
   {
      Integer fieldIdx = getID();
      final View view = getView();
      int retVal = super.setValue(newValue, verify);
      if (retVal == 0)
      {
         if ((fieldIdx == OEORDH.IDX_CUSTOMER) || (fieldIdx == OEORDH.IDX_SHIPTO))
         {
            try
            {
               //Set optional field defaults coming from the item or miscellaneous charge.
               view.set(OEORDH.IDX_PROCESSCMD, OIP_PROCESSCMD_INSERT_OF);
               view.process();
            }
            catch (ViewException ex)
            {
               throw new RuntimeException(
                  "OEORDH returned from a view call with error code " +
                  ex.getReason(), ex);
            }
         }
         else if ((fieldIdx == OEORDH.IDX_INVDISCPER) || (fieldIdx == OEORDH.IDX_INVDISCAMT)
               || (fieldIdx == OEORDH.IDX_IDISONMISC))
         {
            retVal = view.getFields().get(OEORDH.IDX_RECALCTAX).setValue(true, false);
            if (retVal == 0)
            {
               retVal = view.getFields().get(OEORDH.IDX_GOCALCTAX).setValue(true, false);
               if (retVal == 0)
                  retVal = view.process();
            }
         }
      }
      return retVal;
   }
}

If you are familiar with the Sage 300 business logic, you know that all monetary fields have 3 decimals. This then holds all money amounts since the most decimals a currency has is 3 (like the Bahraini Dinar). There are other currencies with 0 decimals (like the Japanese Yen), and others with 2 (like the Canadian Dollar). The only currency, that I know of, with 1 decimal is the Macanese Pataca. So in the database we store 3 decimals to hold any of these. However in the UI you don’t want to allow people to enter the wrong number of decimal places, but the View always tells you 3. So typically the UI needs to know which the corresponding currency field is, look up its number of decimals and then set the numeric edit control to only allow these. In the Web world we certainly don’t want to do a whole lot of server calls to look up currency information, so we override the number of decimals in the server logic and set it to the number of decimals for the corresponding currency code. This is the main purpose of the listed class. There is a getDecimalPlaces() method that we override and in which we calculate the correct decimal places for the field and return that. If we actually put too many decimals to a view field for the given source currency code, it will truncate them to keep things correct, but this would be very misleading to the user.

We also override the isEditable() method which lets us adjust the View field attributes. Hopefully the View tells correctly when fields are editable or not. However there are exceptions and this gives us a place to fix these without changing the View. In this case to make the template code field disabled after the order is first posted.

We then override the setValue() method. We could override this method to change or massage the data before it goes to the View. However, in this case we put the value to the View and if that is successful, for any fields that affect the optional fields, like customer and ship-to, we then re-initialized the optional fields.

Fun with View Operations

The next example is of the OEOrdersQuoteView which handles processing for quotes. I included this example, because it does lots of other View processing, so you can see how to work with Views inside this framework. These View calls are accessed via our SAJava API layer. There is complete JavaDoc for this API included with the Sage 300 SDK. Notice that using this layer is very similar to both the AccpacCOMAPI and the Accpac .Net Interface.

public class OEOrdersQuoteView extends SDataView
{
   private SDataView           oeordq              = null;
   private View                oeordh              = null;
   private SDataView           header              = null;
   private List<String>        selectedQ           = null;
   private List<Short>         selectedL           = null;
   private String              customer            = null;
   private final SDataViewSet  viewSet;
   private boolean             inCreateOrder       = false;
   private boolean             bHasJob             = false;
   /**
    * Restore Quotes Table after order creation failed.
    */
   public void restoreQuotesTable()
   {
      oeordq.recordClear();
      oeordq.browse("", true);
      while (oeordq.goNext())
         oeordq.delete();
      selectedL.clear();

      copyQuotes();
      //re-populate the selected line no list as it has changed after copyQuotes.
      String quote;
      Short lineNo;
      oeordq.recordClear();
      oeordq.browse("", true);
      while (oeordq.goNext())
      {
         quote = (String)oeordq.get(OEORDQ.IDX_QUONUMBER);
         if (selectedQ.contains(quote))
         {
            lineNo = (Short)oeordq.get(OEORDQ.IDX_LINENUM);
            selectedL.add(lineNo);
         }
      }
   }
   /**
    * Copy quotes if there is no record in the quote detail table (OEORDQ)
    */
   public boolean goTop()
   {
      boolean result = super.goTop();
      if (!result)
      {
         inCreateOrder = true;
         selectedQ.clear();
         selectedL.clear();
         if (copyQuotes())
            result = this.goTop();
         return result;
      }
      return result;
   }
   /**
    * copyQuotes copy quotes to table OEORDQ
    *
    * @return true indicate that there are quotes available for the customer
    *         false indicate that there is no quote available for the customer
    */
   private boolean copyQuotes()
   {
      if (null == oeordh)
         oeordh = viewSet.getProgram().openView(OEORDH.VIEW, OpenModes.None, 0, OpenDirectives.InstanceOpen);
      header = viewSet.get("OE0520");
      customer = (String)header.get(OEORDH.IDX_CUSTOMER);
      if (customer.isEmpty())
         return false;
      customer = customer.trim();
      boolean exist = false;
      oeordh.setOrder(OEORDH.KEY5);
      oeordh.recordClear();
      oeordh.set(OEORDH.IDX_ORDNUMBER, "ZZZZZZZZZZZZZZZZZZZZZZ");
      try
      {
         oeordh.set(OEORDH.IDX_CUSTOMER, customer);
      }
      catch (RuntimeException e)
      {
         //Setting customer here always throws exception(error)with existing view behavior.
         //For this case, we just want to set customer to position the current record so the error can safely ignored.
         ;
      }
      String filter = "CUSTOMER = \"&\" AND TYPE = 4 AND (COMPLETE = 1 OR COMPLETE = 2)";
      filter = filter.replaceAll("&", customer);
      oeordh.browse(filter, false);
      while (oeordh.goNext())
      {
         this.recordClear();
         this.recordGenerate(RecordGenerateMode.NoInsert);
         this.set(OEORDQ.IDX_QUONUMBER, oeordh.get(OEORDH.IDX_ORDNUMBER));
         this.insert();
         exist = true;
      }
      return exist;
   }
}

Another Calculated Field

Here is a virtual field. This one has no corresponding View field; it implements the whole field itself. Its value is a fairly simple calculation as the sum of two other fields.

public class OEOrderHeaderSumOfDiscountsField extends VirtualFieldAdapter
{
   private final SDataView     view;
   private static final String OEORDH_DTLDISCTOT           = "DTLDISCTOT";
   private static final String OEORDH_INVDISCAMT           = "INVDISCAMT";
   public static final String  OEORDH_CALC_SUMOF_DISCOUNTS = "CALC_SUMOF_DISCOUNTS";
   public OEOrderHeaderSumOfDiscountsField(SDataView view, ResourceVirtualField resourceField)
   {
      super(view, resourceField);
      this.view = view;
   }
   public PropertyType getType()
   {
      return PropertyType.DECIMAL;
   }
   public boolean specifiesFractionDigits()
   {
      return true;
   }
   public int getFractionDigits()
   {
      final String currencyCode = (String)getView().getFields().get(OEORDH.IDX_ORSOURCURR).getValue();
      return view.getParent().getCurrency(currencyCode).getDecimals();
   }
   public Object getValue()
   {
      BigDecimal detailDiscount = (BigDecimal)view.getFields().get(OEORDH_DTLDISCTOT).getValue();
      BigDecimal orderDiscount = (BigDecimal)view.getFields().get(OEORDH_INVDISCAMT).getValue();
      return detailDiscount.add(orderDiscount);
   }
   public boolean setValue(Object value)
   {
      return true; //Do nothing
   }
   public boolean isReadOnly()
   {
      return true;
   }
}

Notice that this field sets the number of decimals, field type and controls the attributes like whether its read-only (which it always is).

Summary

This blog posting provides a few examples of doing server side programming in the Web version of Sage 300 ERP. Besides being in Java, most of the concepts and type of activities should be fairly familiar to experienced Sage 300 developers.

Advertisements

Written by smist08

October 22, 2011 at 4:18 pm

Posted in sage 300

Tagged with , ,

The Accpac Server Side Programming Model

with 7 comments

Introduction

I previously blogged on server side programming for the new Web version of Sage ERP Accpac here. However we’ve improved how you do this quite a bit making it much easier to interact with the Accpac Views (Business Logic) and to hook in your logic where you need it.

We are currently in the process of moving all our VB6 based UI forms to the web. I described this process here. The VB UI Forms talked directly to our Accpac Business Logic using our View API via the Accpac COM API. This interface worked well since it gave complete control over the Business Logic for both our UI Forms as well as for ISV’s integrating to Accpac. However this API doesn’t work well in the Web, it is too fine grained and results in far too much communication between the client and server. Over the Internet this then becomes quite slow.

To address the problem with the Accpac View API, we have adopted SData as our exclusive client/server communications protocol for our Web version. This protocol is a true REST based modern Web Services interface that will have the same role that the Accpac COM API had in the VB world. I’ve blogged on SData  a few times: here, here, here and here. Basically we want to limit ourselves to at most one server call per user interaction. The VB UIs would often make quite a few. So what do we do with all the VB logic that does all this Business Logic communication?

Basically if you find a number of Accpac COM API calls in a VB program, then this code needs to be moved to the server and programmed there. Then the client should be setup so it can call this code either as a function using an SData service operation, or accessed via a virtual field (calculated field) in the SData feed. JavaScript and AJAX actually make it hard to make several calls in a row to the server, since due to the asynchronous nature, you have to send a request, set a callback and then in that callback make the next call and so on. Chaining asynchronous calls in a row this way is rather tedious and error prone programming. In a way, this is a good thing since it discourages making more than one call at a time, which is what you want for maximum efficiency.

You might ask why we don’t just move all this code into the Views? The main reason for Sage is that we are maintaining both the Web UIs and VB UIs in parallel for quite some time. As a result of this we don’t want to have to change or re-test the VB UIs, so we don’t want to change the Views at all. Plus a lot of this logic that exists in the VB UIs, really doesn’t belong in the Views, but it doesn’t belong in the Web browser either. Hence we have this solution. It’s also nice because it moves all new programming out of C into the nice object oriented world of Java.

Also remember that your SData feeds aren’t only used by your UI forms. They are also a general programming interface for any third party ISV or integrator to use to tie into Accpac. So when designing your SData feeds keep third parties integrating via SData in mind as well as your Web UI programmers.

Server Side Architecture

There are basically a couple of ways to structure this. You can return one or more virtual fields which are calculated on the server by doing regular Accpac View calls and other programming logic, these are good if the data is always relevant, or relevant when the other fields are relevant. The other way is to access the code via an SData service call. An SData service call is basically a custom call to the server to do some processing, you can send in any number of parameters you may require and you can receive back a number of fields with the results. You can add service calls to regular data SData feeds, then these operate in a similar manner to how Views like OEORDH behave. The OEORDH view is a data view on the order header database tables, but additionally it can do processing like calculate taxes which is invoked by its viewProcess entry point. So if you have a data feed you can add some virtual fields and/or you can add some service processing calls. The nice things about these processing calls is that they can use any existing view fields and view state automatically. Additionally you can also create standalone SData service calls which have no data feed associated with them.

Inside our SData processing program, at one end SData requests come in. These are translated into View operations and then at the other end, the Views are called through our Java View API (SAJava). SData feeds correspond to a whole set of header/detail Views, so each SData request could potentially be operating on a whole set of Accpac Views via the normal header/detail View protocols.

So we have the SData request come and this is processed by the ResourceKind. The ResourceKind operates on an SDataViewSet, think of this as all the header/detail views composed together. In the SDataViewSet are a number of SDataViews and in each SDataView there are a whole bunch of SDataViewFields. Then the SDataView and SDataViewField talk to the Java class versions of our View API in the SAJava layer to do the real work. Basically we allow you to either extend or create any of the classes in the middle and give an organized way to have your classes used either in conjunction with our classes or instead of our classes. Since we are in the object oriented world of Java, when you extend one of our classes you only need to do the bare minimum work required to accomplish your goal, there is no extra overhead like we had in the C and VB world with our template approach to things. Typically you would create brand new SDataViewFields with no associated ViewField for virtual or calculated fields. If you want to just slightly change the behavior of the existing ViewField then you can extend that existing one. Similarly you can extend the SDataView class for a View to change or enhance the behavior of the base View. Generally this is much easier than sub-classing Views in C.

Running Your Classes

How do we get our classes to be run in this framework from the SData service running under Tomcat? It all starts with the classmap/resourcemap files. All the SData feeds for an application are defined in its classmap.xml file which is located in C:\”Program Files (x86)\Common Files\Sage\Sage Accpac\Tomcat6\portal\sageERP\application” where application is something like oe61a. The classmap.xml file provides the name of the SData feed, an additional configuration file for the feed called a resoucemap.xml file and the name of the class to run for this feed. This class is then loaded dynamically by our SData service when this feed is first accessed. Since it is Tomcat that loads the class for us, it must be located somewhere in the classpath for Tomcat which is typically the Tomcat\lib folder (usually in a jar file containing all your classes).

<?xml version="1.0" encoding="UTF-8"?>
    <classMap>
        <contracts>
            <contract name="accpac">
                 <resources>
                     <resource name="oeorders"
                         className="com.sage.accpac.oe61a.oe1100.server.OEOrdersResourceKind">
                         <parameters>
                             <parameter name="ResourceMapFile" value="OEOrderViewMapping.xml" />
                         </parameters>
                      </resource>
                   </resources>
            </contract>
      </contracts>
</classMap>

This is then your resourceKind. This will be a Java class that extends one of our standard resourceKinds in our SDK (such as ViewResourceKind). From here you can then load other classes to act as your SDataViewSets, SDataViews and SDataFields. So for instance to provide your own SDataViewSet you would override the createSDataViewSet method:

   /**
    * provides a {@link OEOrdersSDataViewSet} subclass of the required class as
    * described by the superclass documentation:
    * <p>
    *
    * {@inheritDoc}
    */
   @Override
   protected SDataViewSet createSDataViewSet(Program program, Resource resource)
   {
      if (null == oeordhQ)
         oeordhQ = getResourceContext().getAccpacProgram().openView(OEORDH.VIEW,
               OpenModes.None, 0, OpenDirectives.InstanceOpen);
      return new OEOrdersSDataViewSet(program, resource, oeordhQ);
   }

Then in the SDataViewSet (OEOrdersSDataViewSet in this case), you can override any individual SDataView components you like:

   /**
    * provides application-specific subclasses of the {@link SDataView} class as
    * described by the superclass documentation:
    * <p>
    * {@inheritDoc}
    */
   @Override
   protected SDataView createView(final String rid, final Resource resource)
   {
      switch (A4wapi.utlObjStrToID(rid))
      {
         case OEORDH.VIEWNUMBER:
            return new OEOrdersHeaderView(this, rid, resource);
         case OEORDD.VIEWNUMBER:
            return new OEOrdersDetailView(this, rid, resource);
         case OEORDDD.VIEWNUMBER:
            return new OEOrdersKittingDetailView(this, rid, resource);
         case OEORDDB.VIEWNUMBER:
            return new OEOrdersBOMDetailView(this, rid, resource);
         case OEORDDS.VIEWNUMBER:
            return new OEOrdersDetailSerialView(this, rid, resource);
         case OEORDQ.VIEWNUMBER:
            return new OEOrdersQuoteView(this, rid, resource, oeordhQ);
         default:
            return super.createView(rid, resource);
      }
   }

Similarly from the SDataView you can create all your virtual SDataViewFields:

   @Override
   public synchronized VirtualField createVirtualField(ResourceVirtualField field)
   {
      if (PMConstants.EntryDetailPropertyNames.CALC_PJC_RESOURCE_DESC.equals(field.getName()))
      {
         pjcResourceDescriptionAdapter = new PJCResourceDescriptionAdapter(getSet().getProgram(),
                this, field);
         return pjcResourceDescriptionAdapter;
      }
      else if (PMConstants.EntryDetailPropertyNames.UFMTCONTNO.equals(field.getName()))
      {
         //We have to manually calculated the unformatted contract number
         // - because the UFMTCONTNO in the Order
         //Details view only gets calculated by the view when the Contract field is PUT
         // - not when you are browsing through the details.
         return new PJCUnformattedContractAdapter(getSet().getProgram(), this, field);
      }
      else if (VIRTUALFLD_ITEMNO_MISCCHARGE.equals(field.getName()))
         return new CalcItemNoMiscCharge(this, field);
      else if (CALC_COMPLETE.equals(field.getName()))
      {
         return new OEOrdersDetailCompleteVirtualField(this, field);
      }
…
      else
         return super.createVirtualField(field);
   }

For any virtual fields that you add, you also need to add then to the <virtualFields> section of the resourceMap.xml file for the SData feed. Otherwise they won’t be included in the feed.

SDataView extends the SAJava View object which is a complete rendition of the View API as a Java class. This means you have full use of that View automatically. Plus you can then open and use any other Views you wish, plus use any other methods in the SAJava interface. Plus use any other Java programming libraries or facilities you like.

This blog posting is already getting quite long and all we’ve done so far is show how to setup the framework so you can start to do your work. I planned to include a number of examples of some things you can do here, but I think I’ll leave that for another posting so I can give a few more varieties of examples.

Summary

This server framework is intended to allow you a great deal of power to extend and customize the Accpac Business Logic as it is fed into SData feeds. It gives you very fine grained control over the existing Business Logic allowing you to change and extend the behavior. The previous VB UIs were architected using a standard three-tier client/server model; however, on the Web, much of the previous VB UI programming moves to the server. We don’t want to put this logic in the Views since it really is UI logic and not true business logic, so now we have the UI layer split between the server and client and then the Business Logic and Database Layers the same as before.

In the Web world, now that the Browsers have become quite powerful rich client environments, there is a lot of debate as to how much logic should run in the Browser and how much on the server. Generally we’ve kept quite a bit of UI logic in the Browser, but we move anything to the server that would require more than one round trip to the server to do the job.

Written by smist08

October 15, 2011 at 9:10 pm