Sync Improv

Improvising Integration

Creating a Business Process (in LynX Business Integrator)

2 Comments

In this post, I’ll describe how to create a business process (in .Net) that integrates with EnterpriseOne. The development process consists of the following steps:

Determine/Design the input for business process

The input for the business process plays an important role in creating the XML schema for the business process. As indicated in my previous post, the XML schema is used to validate the input document. The following factors come into play in determining the input:

  • Can the external app create XML documents (in Unicode)? If it cannot send data in XML, what format can it send the data in (text, binary)?
  • Can you dictate the format/structure of the input to the external app? (i.e. it is flexible enough to create a document in your format or do you have to conform to the external app’s format)
  • Any other restrictions? Such as: Cannot send dates in XML format (YYYY-MM-DD).

Create the Class Library

The class library is created in Visual Studio using the LynX Business Process template. This template creates a class library project with references to the Lynx.Net libraries.
Identify the EnterpriseOne objects
This depends on the area of integration. For example, if you want to create address book records in EnterpriseOne, you will need the Address Book and/or Customer, Supplier master business functions (N0100041, N0100042, N0100043).

Use dnetgen to create typed EnterpriseOne classes

In addition to creating typed classes, dnetgen creates the following:

XML Schema snippet

The schema snippet can be used to extend the XML schema for the business process. When you include an EnterpriseOne object in dnetgen, you intend to use it in your business process. For example, if you included F0005, you probably want to do a SELECT, INSERT or UPDATE on F0005. If you included the Address Book Master Business Function (MBF), you probably want to call it in your business process. The input for these operations may come from hard/soft coded values or from the input document. In other words, there is a pretty good chance that your input document’s format is determined by the EnterpriseOne objects in your project. And that format is determined by the EnterpriseOne’s data dictionary. Consider the following scenario:

  • You are creating a business process to create address book records in E1. To do that, you need to call the Address Book MBF.
  • The Address Book MBF has upwards of 80 input parameters, depending on your E1 version.
  • Your input source does not have all these 80 input values, but has the most common ones used to create an address book record – the name and address fields.
  • In E1, the name field is restricted to 40 characters and the address line fields are restricted to 30 characters, so your XML schema for the input document should probably reflect this restriction.

That’s where the schema snippet comes into play. The schema snippet created by dnetgen includes all the parameters of the Address Book MBF. You can now use this to customize the schema for your input document. Here is how the schema snippet looks like (shortened and modified for clarity):

<!--
		Complex Type for Business Function:
			Name: AddressBookMasterMBF
			Description: Address Book - MBF
			Module: N0100041
			Library: CALLBSFN  
-->
<xs:complexType name="AddressBookMasterMBF">
  <xs:sequence>
    <!--Parameter: mnAddressBookNumber Alias: AN8 -->
    <xs:element name="AddressNumber" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="AN8" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:decimal">
          <xs:fractionDigits value="0" />
          <xs:totalDigits value="8" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szLongAddressNumber Alias: ALKY -->
    <xs:element name="AlternateAddressKey" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="ALKY" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="20" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szTaxId Alias: TAX -->
    <xs:element name="TaxId" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="TAX" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="20" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szSearchType Alias: AT1 -->
    <xs:element name="AddressType" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="AT1" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="3" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szAlphaName Alias: ALPH -->
    <xs:element name="AlphaName" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="ALPH" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szMailingName Alias: MLNM -->
    <xs:element name="MailingName" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="MLNM" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szSecondaryMailingName Alias: MLN1 -->
    <xs:element name="SecondaryMailingName" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="MLN1" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szAddressLine1 Alias: ADD1 -->
    <xs:element name="AddressLine1" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="ADD1" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szAddressLine2 Alias: ADD2 -->
    <xs:element name="AddressLine2" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="ADD2" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szAddressLine3 Alias: ADD3 -->
    <xs:element name="AddressLine3" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="ADD3" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    <!--Parameter: szAddressLine4 Alias: ADD4 -->
    <xs:element name="AddressLine4" maxOccurs="1" minOccurs="0">
      <xs:annotation>
        <xs:appinfo source="ADD4" />
      </xs:annotation>
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="0" />
          <xs:maxLength value="40" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
  </xs:sequence>
</xs:complexType>

XML Sample Document Fragment

dnetgen also creates sample document fragment that correspond to the input values for the EnterpriseOne object. For example, if you included a table object, the sample fragment has fields in the table. For business functions, the sample fragment has all input parameters. Here is a sample fragment for the Address Book MBF (shortened for clarity):

<AddressBookMasterMBF>
  <AddressNumber>0</AddressNumber>
  <AlternateAddressKey> </AlternateAddressKey>
  <TaxId> </TaxId>
  <AddressType1> </AddressType1>
  <AlphaName> </AlphaName>
  <MailingName> </MailingName>
  <SecondaryMailingName> </SecondaryMailingName>
  <AddressLine1> </AddressLine1>
  <AddressLine2> </AddressLine2>
  <AddressLine3> </AddressLine3>
  <AddressLine4> </AddressLine4>
  <ZipCodePostal> </ZipCodePostal>
  <City> </City>
</AddressBookMasterMBF>

You can use this to build your sample document: the fragment saves you a lot typing.

Sample C# code

It can take a while to write code that sets the input values of a business function that has many parameters (like the Address Book MBF) or code that sets insert values of a large table like F0911Z1. dnetgen creates sample code that you can cut-and-paste into your C# class/function. Here is the generated sample code for the Address Book MBF object (shortened for clarity):

   /*******************************************************************************/
      // Business function | Module: N0100041; Library: CALLBSFN; Description: Address Book - MBF
      // create the business function object
      AddressBookMasterMBF bsfn = new AddressBookMasterMBF();

      bsfn.DpmnAddressBookNumber.InValue = (long)entity_object.AddressNumber;
      bsfn.DpszLongAddressNumber.InValue = entity_object.AlternateAddressKey;
      bsfn.DpszTaxId.InValue = entity_object.TaxId;
      bsfn.DpszSearchType.InValue = entity_object.AddressType1;
      bsfn.DpszAlphaName.InValue = entity_object.NameAlpha;
      bsfn.DpszSecondaryAlphaName.InValue = entity_object.Kanjialpha;
      bsfn.DpszMailingName.InValue = entity_object.NameMailing;
      bsfn.DpszSecondaryMailingName.InValue = entity_object.SecondaryMailingName;
      bsfn.DpszDescriptionCompressed.InValue = entity_object.DescripCompressed;
      bsfn.DpszBusinessUnit.InValue = entity_object.CostCenter;
      bsfn.DpszAddressLine1.InValue = entity_object.AddressLine1;
      bsfn.DpszAddressLine2.InValue = entity_object.AddressLine2;
      bsfn.DpszAddressLine3.InValue = entity_object.AddressLine3;
      bsfn.DpszAddressLine4.InValue = entity_object.AddressLine4;
      bsfn.DpszPostalCode.InValue = entity_object.ZipCodePostal;
      //
      // code for remaining parameters ...
      //

      // execute the business function
      BusinessFunctionResult result = bsfn.Execute();

      if (result != BusinessFunctionResult.Error)
      {
        // get the output parameters
        // get the output parameters
        //bsfn.DpcActionCode.OutValue
        //bsfn.DpcUpdateMasterFile.OutValue
        //bsfn.DpcProcessEdits.OutValue
        //bsfn.DpcSuppressErrorMessages.OutValue
      }
      else
      {
        // set document's status to false (optional)
        DocumentContext.DocumentStatus = false;		      
      }
      
      // collect errors (optional)
      DocumentContext.AddError(bsfn, true);
      success = (result == BusinessFunctionResult.Success);

Create the logic for your process in the class library

As indicated in my previous post, the LynX.Net infrastructure calls the ProcessDocument method in the class that implements the IIntegrationProcess interface. You can have any number of classes that implement this interface (in the class library). So, how does LynX.Net know which class’s method to call for a given document? All input documents have an integration ID, which uniquely identifies a business process in LynX. It looks for the class that is decorated with the BusinessProcess attribute whose IntegrationID property is set to the one in the input document. For example, if the Integration ID of the input document is ERP.EOne.Demo, you must have a class that is coded this way:
[BusinessProcess(IntegrationID = “ERP.EOne.Demo”, Description = “This is a demo”)]
public class DemoClass : Aellius.Sandhi.LynX.IIntegrationProcess

In my previous post, I described the following coding steps, so I won’t repeat myself here.

  1. Retrieve the data from the XML document.
  2. Use LynX.Net API to access EnterpriseOne objects (detail below)
  3. Create the output document. This document will be returned to the caller through the web service.

The following steps are common to all EnterpriseOne objects:

  1. Include the object in dnetgen (and reference the generated library in your project)
  2. Extended the XML schema based on your requirements
  3. Extend the sample document for testing

Calling a Business Function

To call a business function:

  • Create an instance of the business function
  • Set the input parameters (use the generated sample as needed)
  • Call Execute
  • Retrieve errors.

Sample code:

   private bool CallAddressBookBsfn(BusinessDocument businessDocument, Transaction transaction)
    {
      AddressBookMaster abm = businessDocument.document.input.AddressBook;

      // create an instance of the Address Book Master Business function
      // note the use of JDE Transactions
      AddressBookMasterMBF bsfn = new AddressBookMasterMBF(transaction);

      // set parameters - most of this code is auto-generated
      bsfn.DpmnAddressBookNumber.InValue = (long)abm.AddressNumber;
      bsfn.DpszSearchType.InValue = abm.AddressType;
      bsfn.DpszAlphaName.InValue = abm.Name;
      bsfn.DpszAddressLine1.InValue = abm.AddressLine1;
      bsfn.DpszAddressLine2.InValue = abm.AddressLine2;
      bsfn.DpszAddressLine3.InValue = abm.AddressLine3;
      bsfn.DpszAddressLine4.InValue = abm.AddressLine4;
      bsfn.DpszPostalCode.InValue = abm.ZipCodePostal;
      bsfn.DpszCity.InValue = abm.City;
      bsfn.DpszState.InValue = abm.State;
      bsfn.DpszCountry.InValue = abm.Country;
      bsfn.DpcActionCode.InValue = 'A';
      bsfn.DpcUpdateMasterFile.InValue = '1';

      // execute the business function
      if (bsfn.Execute() != BusinessFunctionResult.Success)
      {
        DocumentContext.AddError(bsfn.GetExceptions(), true);
        return false;
      }

      // assign output
      businessDocument.document.output.AddressNumber = bsfn.DpmnAddressBookNumber.OutValue;
      businessDocument.document.output.AddressNumberSpecified = true;
      return true;
    }

Database Operations

You can do SELECT on table and view objects, and INSERT, UPDATE and DELETE database operations on table objects. Keep in mind that E1 row and business unit security are in effect during these operations.

SELECT operations:
  1. Create an instance of the table or view.
  2. Set the list of columns to be selected by setting the SelectColumn property of the table column. For views, all columns are selected, so this property does not exist.
  3. Call AddCriteria to specify your WHERE clause
  4. Call Select, then Fetch to fetch the data
  5. Close the table

Sample Code:

   private List<string> GetStates()
    {
      F0005 f0005 = new F0005();
      f0005.SelectAllColumns();
      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.ProductCode, SingleValueOperator.EqualTo,
        "00");
      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.UserDefinedCodes, SingleValueOperator.EqualTo,
        "ST");

      List<string> states = new List<string>();
      while (f0005.Fetch())
      {
        states.Add(f0005.TcUserDefinedCode.SelectedValue.Trim());
      }
      f0005.Close();

      return states;
    }
INSERT
  • Create an instance of the table.
  • Set the InsertValue of all table columns.
  • Close the table

Sample code:

private void AddUDCCode()
    {
      GetAuditInfo auditinfo = new GetAuditInfo();
      if (auditinfo.Execute() != BusinessFunctionResult.Success) return;

      F0005 f0005 = new F0005();
      // these values are hard coded here, but typically
      // they will come from the input document and/or a configuration file
      // except for the values, this code was cut/paste from the sample file
      // that dnetgen generated
      f0005.TcProductCode.InsertValue = "59";
      f0005.TcUserDefinedCodes.InsertValue = "CC";
      f0005.TcUserDefinedCode.InsertValue = "01";
      f0005.TcDescription001.InsertValue = "Test";
      f0005.TcDescription01002.InsertValue = "Code";
      f0005.TcSpecialHandlingCode.InsertValue = " ";
      f0005.TcUdcOwnershipflag.InsertValue = ' ';
      f0005.TcHardCodedYN.InsertValue = 'N';
      f0005.TcUserId.InsertValue = auditinfo.DpszUserName.OutValue;
      f0005.TcProgramId.InsertValue = "LYNX";
      f0005.TcDateUpdated.InsertValue = auditinfo.DpjdDate.OutValue;
      f0005.TcWorkStationId.InsertValue = auditinfo.DpszWorkstation_UserId.OutValue;
      f0005.TcTimeLastUpdated.InsertValue = auditinfo.DpmnTime.OutValue;

      f0005.Insert();

      f0005.Close();
    }
UPDATE
  • Create an instance of the table.
  • Call AddCriteria to specify your WHERE clause
  • Set the UpdateValue of all table columns
  • Call Delete
  • Close the table

Sample code:

   private void UpdateUdcCode()
    {
      GetAuditInfo auditinfo = new GetAuditInfo();
      if (auditinfo.Execute() != BusinessFunctionResult.Success) return;

      F0005 f0005 = new F0005();

      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.ProductCode, SingleValueOperator.EqualTo,
        "59");
      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.UserDefinedCodes, SingleValueOperator.EqualTo,
        "CC");
      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.UserDefinedCode, SingleValueOperator.EqualTo,
        "01");

      // these values are hard coded here, but typically
      // they will come from the input document and/or a configuration file
      f0005.TcDescription001.UpdateValue = "Updated";
      f0005.TcDescription01002.UpdateValue = "Value";
      f0005.TcUserId.InsertValue = auditinfo.DpszUserName.OutValue;
      f0005.TcProgramId.InsertValue = "LYNX";
      f0005.TcDateUpdated.InsertValue = auditinfo.DpjdDate.OutValue;
      f0005.TcWorkStationId.InsertValue = auditinfo.DpszWorkstation_UserId.OutValue;
      f0005.TcTimeLastUpdated.InsertValue = auditinfo.DpmnTime.OutValue;

      f0005.Update();

      f0005.Close();
    }
DELETE
  • Create an instance of the table
  • Call AddCriteria to specify your WHERE clause
  • Call Delete
  • Close the table

Sample code:

   private void DeleteUDCCode()
    {
      F0005 f0005 = new F0005();

      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.ProductCode, SingleValueOperator.EqualTo,
        "59");
      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.UserDefinedCodes, SingleValueOperator.EqualTo,
        "CC");
      f0005.AddCriteria(Parenthesis.None, Conjunction.And, F0005.Column.UserDefinedCode, SingleValueOperator.EqualTo,
        "01");

      f0005.Delete();

      f0005.Close();
    }

Running Reports

To run/execute a report:

  • Create an instance of the report
  • Set the input parameters (optional)
  • Call Execute

Sample code:

    public void CallR0008P()
    {
      R0008P r0008p = new R0008P("XJDE0001");
      r0008p.DpcFiscalDatePattern.InValue = 'R';
      r0008p.Execute();
      
      // yes, this can retrieve errors from work center!
      Exception[] exceptions = r0008p.GetExceptions();
    }

Attaching Media Objects

LynX allows you to upload text/rft and file media objects. To attach a media object:

  • Retrieve an array of attachments from the DocumentContext object. Attachments are uploaded through the web service. All attachments have a key, which uniquely identifies which entity the attachment is associated with
  • Create an instance of the media object
  • Set the input parameter
  • Call UpdateAll

Sample code:

  public void AttachMediaObject()
    {
      // assumes that media objects were uploaded for this key through the web service
      MediaObjectAttachment[] attachments = DocumentContext.AttachmentSet.GetAttachmentsByKey("ADDRESS_BOOK");
      if (attachments.Length &amp;lt;= 0) return;

      ABGT abgt = new ABGT();
      // this is for address #500
      abgt.DpmnAddressNumber.InValue = 500;
      abgt.UpdateAll(attachments);
    }

Transaction Control

You can wrap the following operations in a JDE database transaction:

  • Business Function execute
  • Database operations (SELECT, INSERT, UPDATE DELETE)
  • Media object upload

To use transaction control:

  • Create a Transaction object
  • Call Begin to start the transaction
  • Pass the object in the constructor of the E1 object (business function, table, etc.)
  • Call Commit or RollBack based on the outcome of the work

Sample code:

  public void TransactionTest()
    {
      Transaction tran = new Transaction("my transaction");
      try
      {
        tran.Begin();
        
        // do something - call a bsfn, table i/o etc
        F0101 f0101 = new F0101(tran);
        
        // everything was successful, commit
        tran.Commit();
      }
      catch (Exception ex)
      {
        // something went wrong, rollbalk
        tran.Rollback();
        throw ex;
      }
    }

Conclusion

As you can see, LynX.Net is very powerful – you can create integrations in a familiar development environment without losing any E1 functionality. Please reply to this post if you have any questions.

2 thoughts on “Creating a Business Process (in LynX Business Integrator)

  1. Cool stuff. How is this published as a web service? Does this library have to be copied to the IIS server that hosts your web service?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s