How to crud 3 levels

Requirements Database Classes created Representation Scripts used Validation and deletion

This example explains how to create a persistent entity based on a header, lines and sub-lines, and how to manage the CRUD related operations.
Some data types, which are references to other Sage X3 tables are used.
The naming rules are used for vertical elements (names starting with 'Y').
A customized development made for a unique customer should be coded with 'Z'.

Description of requirements

The following requirements need to be managed:

The unit price is entered at the sub-detail level, cumulated on the line, and finally at the header level. In the user interface, the user displays the cumulated price when a modification has been sent to the server. As it is done, the line price must not be modified; it is recomputed by cumulating the sub-lines.

Database structure

The database structure used for the persistence of the entity is as follows:

TableDescriptionColumnData typeExplanation
YORDERH [YOH]Specific sales order YORDNUMA 10 (character string)Sales order number
YBPCBPC (customer code)Order customer
YREPREP (sales rep code)Order sales rep
YORDDATDateSales order date
YTOTALDCB 9.2(Decimal)Total amount of the order
YCURCUR (currency code)Order currency
YSTATUSLocal menu (6520)Order status
YORDERLINE [YOD]Sales order detail YORDNUMYOD (sales order reference)Order number
YORDLC 4(short integer)Line order number (for join)
YSORTL 8(long integer)Line order number (order in which the lines appear on the document)
YITMITM (item reference)Ordered item
YPRICEDCB 9.2 (decimal)Line price
YORDERSUBLIN [YOL]Sales order line sub-detail YORDNUMYOD (line order reference)Order number
YORDLC 4 (short integer)Order Line number
YORDSLC 4 (short integer)Order sub-line number
YUNITUOM (unit of measure)Unit for quantity
YQTYQTY (quantity)Sub-line quantity
YUNITPRICEDCB 9.2 (Decimal)Unit price for sub-line

Every table also has the usual technical columns (CREDATTIM, UPDDATTIM, CREUSR, UPDUSR, AUUID).

The indexes needed on the tables are as follows:

TableDescriptionIndexDefinitionDescription
YORDERH [YOH]Specific sales order YOH0YORDNUM (no duplicates)Main index used to access the sales order by its reference
YOH1YBPC+YORDNUMIndex per customer
YOH2YREP+YORDNUMIndex per sales rep
YOH1YORDDAT+YORDNUMIndex per date
YORDERLINE [YOD]Sales order detail YOD0YORDNUM+YORDL (no duplicates)Main index used to access the sales order line
YOH1YORDNUM+YSORTIndex used to display lines in the right order
YORDERSUBLIN [YOL]Sales order sub-line YOD0YORDNUM+YORDL+YORDSL (no duplicates)Main index used to access the sales order sub-line

The local menu 6520 used has two values: Entered and Validated.

Description of the created classes

Three classes are needed:

The 'YORDER' persistent class also has the following characteristics:

The Mapping tab includes the following join description grid:

ReferenceClassReference tableOrigin tableMain index Sorting indexTypeJoin expression
YODYORDERLINEYORDERLINEYORDERYOD0YOD11,n[F:YOH]YORDNUM
YOD.YOLYORDERSUBLINYORDERSUBLINYORDERLINEYOL01,n[F:YOD]YORDNUM;[F:YOD]YORDL

Description of the 'YORDER' representation

This is the representation used for the CRUD management of the 'YORDER' class. It has the following characteristics:

The sections and blocks are organized as follows:

Section definitionBlocks definition
CodeDescriptionOrderCodeDescriptionSectionOrder
YHEADHeader10 YMAINMain infoYHEAD10
YSTATStatusYHEAD20
YLINESLines20 YLINESLinesYLINES30
YFOOTFooter30 YFOOTFooterYFOOT40

The following properties are available in the representation:

AliasPropertyCollectionDescriptionFacets
YORDNUMYOH.YORDNUMOrder NumberAll
YBPCYOH.YBPCCustomerQuery, Detail, Edit, Lookup
YREPYOH.YREPSales repQuery, Detail, Edit
YORDDATYOH.YORDDATOrder DateQuery, Detail, Edit
YCURYOH.YCURCurrencyDetail, Edit
YSTATUSYOH.YSTATUSStatusDetail, Edit (Not inputted)
YITMYOH.YOD.YITMYOH.YODItem RefDetail, Edit
YPRICEYOH.YOD.YPRICEYOH.YODLine PriceDetail, Edit
YUNITYOH.YOD.YOL.YUNITYOH.YOD.YOLUnitDetail, Edit
YQTYYOH.YOD.YOL.YQTYYOH.YOD.YOLQuantituDetail, Edit
YUNITPRICEYOH.YOD.YOL.YUNITPRICEYOH.YOD.YOLUnit PriceDetail, Edit
YTOTALYOH.YTOTALOrder totalDetail, Edit (not inputted)

The script associated with the representation is called 'YORDER_RSTD'.

The different scripts used

All scripts are starting with '$METHODS' or '$PROPERTIES' label. To know the exact context, the following variables need to be checked:

The scripts used on every class or representation have the following pattern:

$METHODS
 Case CURPTH
   When "" : # Methods called for the instance of the class associated with the script
     Case ACTION
         ... here are the methods
     Endcase
   When ... : # Methods called for child instances of the class associated with the script 
     Case ACTION
         ... here are the methods
     Endcase
Endcase
Return

$PROPERTIES
  Case CURPRO
    When ... : # The property is defined here
      Case ACTION
         # Here ACTION can have the values CONTROL, INIT, PROPAGATE...
      Endcase
  Endcase
Return

In the following example code, we will skip these lines and mention where the code is written.

At sub-line level

This script associated with 'YORDSUBLIN' is used at the sub-line level to manage the events, control, and default values only. By default, this script defined as specific is called 'YORDERSUBLIN_CSPE'.

As nothing is required, there is no action to be done in this script.

If the properties in the sub-lines have to be filled, no code is necessary, the Mandatory check box will be activated in the class definition.

At line level

This script associated with 'YORDLINE' is called 'YORDLINE_CSPE'. The only code written is used when a line is modified to propagate to the sub-lines some of the modifications that have been done.

Two things happen:

EventACTION valueCURPTHCURPRO"this" valueActions to be done
When a sub-line is added to a lineADD_LINE_AFTER"""YOL"YORDELINE instance1. Propagate YORDNUM and YORDL from line to the sub-line
2. Compute YORDSL with a new value greater than the existing line values
When a key property (YORDNUM or YORDL) is modified in the line"PROPAGATE" """YORDNUM"YORDELINE instance1. Propagate YORDNUM on all the sub-lines
"""YORDL"YORDELINE instance2. Propagate YORDL on all the sub-lines
The first case gives the corresponding code (after the '$METHOD' label):
$METHODS
 Case CURPTH
   When "YOL"
     Case ACTION
       When "ADDLINE_AFTER" : # A line has been added
         this.YORDNUM = this.APARENT.YORDNUM : # "this" is the sub-line, this.APARENT is the line
         this.YORDL   = this.APARENT.YORDL
         # To assign YORDSL to a new value, compute the maximum of the YORDSL property
         #  for all the YOL child instance of the line, and add 1 to the result
         this.YORDSL  = 1+max(this.APARENT.YOL(1..maxtab(this.APARENT.YOL)).YORDSL)
     Endcase
 Endcase
Return
The second case gives the corresponding code (after the $PROPERTIES label):
$PROPERTIES
Case CURPTH
  When "" : # We are here on the properties of the instance associated with the current class (line)
    Case CURPRO
      When "YORDNUM" : # Order number at line level
        Case ACTION
          When "PROPAGATE" : # The value has been modified
               Local Integer I
               # Loop on all existing sub-lines of the line : "this" is the line modified
               For I=1 To maxtab(this.YOL)
                 If this.YOL(I)<>null : this.YOL(I).YORDNUM=this.YORDNUM : Endif
               Next I
        Endcase
      When "YORDL" : # Line number at line level
        Case ACTION
          When "PROPAGATE" : # The value has been modified
               Local Integer I
               # Loop on all existing sub-lines of the line : "this" is the line modified
               For I=1 To maxtab(this.YOL)
                 If this.YOL(I)<>null : this.YOL(I).YORDL=this.YORDL : Endif
               Next I
        Endcase
    Endcase
Endcase
Return

At header level

This script associated with YORDER is called YORDER_CSPE. There is more code to write because several cases need to be managed:

On properties, the propagation code is the following:

$PROPERTIES
  Case CURPRO
    When "YORDNUM"
      Case ACTION
        When "PROPAGATE"
          # Loop on all the lines present to assign YORDNUM
          # The propagation rule present in YORDLINE_CSPE script will update the sub-line values
          Local Integer I
          For I=1 To maxtab(this.YOD)
            If this.YOD(I)<>null : this.YOD(I).YORDNUM=this.YORDNUM : Endif
          Next I
      Endcase
  Endcase
Return

On methods, the code is the following:
$METHODS
 Case CURPTH
   When "" : # Method on the order class
     Case ACTION
       When "YTOTAMOUNT" : # Total amount computation : ARET_VALUE returns the result, I is a loop variable
           Local Integer I
             ARET_VALUE=0
             # Loop done on all the YOD values if the pointer is not null and if the line is not marked as deleted
             For I=1 To maxtab(this.YOD)
               If this.YOD(I)<>null
                 If find (this.YOD(I).ASTALIN,[V]CST_ADEL, [V]CST_ANEWDEL)=0
                   this.YOD(I).YPRICE=fmet this.YOD(I).LINEPRICE
                   ARET_VALUE+=this.YOD(I).YPRICE
                 Endif
               Endif
             Next I

       # When an insertion or an update method is called, call the previous computation method before database persistence
       When "AINSERT_CONTROL_BEFORE", "AUPDATE_CONTROL_BEFORE"
             this.YTOTAL= fmet this.YTOTAMOUNT

     Endcase

   When "YOD" : # method on the line
      Case ACTION
       When "LINEPRICE" : # Total line computation : ARET_VALUE returns the result, I is a loop variable
           Local Integer I
             ARET_VALUE=0
             # Loop done on all the YOL values if the pointer is not null and if the line is not marked as deleted
             For I=1 To maxtab(this.YOL)
               If this.YOL(I)<>null
                 If find (this.YOL(I).ASTALIN,[V]CST_ADEL, [V]CST_ANEWDEL)=0
                   ARET_VALUE+=this.YOL(I).YUNITPRICE*this.YOL(I).YQTY
                 Endif
               Endif
             Next I

         When "ADDLINE_AFTER" : # A line is added
            Gosub ADD_LINE_YOD : # We need to manage the numbering of the line added

      Endcase
Endcase
Return

The last part of the code is now the 'ADD_LINE_YOD' sub-program. This code assigns two properties on the YOD collection:

For example, we start with an empty order. Lines A, B, and C are added first. Then line D is inserted between A and B, line E is inserted between lines B and C, line F is inserted between A and D, line G is inserted before A, and line H is added at the end. The final result will be the following, as it appears on the page when all updates have been done:

line insertedAORDER propertyYORDSL valueYSORT value
G1750
A21100
F36125
D44150
B52200
E65250
C73300
H88400

As you can see, 'YORDSL' gives the input order, 'AORDER' handles the index for displaying the lines, and 'YSORT' allows you to sort the lines without having to reassign other number lines when a limited number of insertions have been done.

The following algorithm manages this operation:
$ADD_LINE_YOD

# Increment value for dichotomy algorithm
  Local Integer MY_STEP
  MY_STEP = 100

# Manage key identifiers
  this.YORDNUM  = this.APARENT.YORDNUM
  this.YORDL    = 1 + max(this.APARENT.YOD(1..maxtab(this.APARENT.YOD)).YORDL)

# Manage the SORT property
  Local Integer I, BEF_LINE, AFT_LINE, OFFSET_LINE : # Positions in collection (they have a 0 value here)

# Where are the previous and next lines ? Loop on YOD values
# "this" is here the current line, we need to loop on the YOD in the header instance (APARENT)
  For I = 1 To maxtab(this.APARENT.YOD)
    If this.APARENT.YOD(I) <> null
      If this.APARENT.YOD(I).AORDER = this.AORDER - 1
        BEF_LINE=I
      Elsif this.APARENT.YOD(I).AORDER = this.AORDER + 1
        AFT_LINE=I
      Endif
    Endif
  Next I

Local Integer BEF_LINE_SORT, AFT_LINE_SORT: # Positions in the sort order

# If the new line is not added at the end of the collection
  If AFT_LINE <> 0
    # Assign the SORT properties (BEF_LINE_SORT is 0 if insertion at the beginning of the collection)
    AFT_LINE_SORT = this.APARENT.YOD(AFT_LINE).YSORT
    If BEF_LINE <> 0 : BEF_LINE_SORT = this.APARENT.YOD(BEF_LINE).YSORT : Endif
    # If there is enough space then we insert using a dichotomy method
    If BEF_LINE_SORT <= AFT_LINE_SORT - 2
      this.YSORT = int((BEF_LINE_SORT + AFT_LINE_SORT)/2)
    Else
      # If not enough space, we reassign the SORT property of all lines from the new inserted line
      For I=1 To maxtab(this.APARENT.YOD)
        If this.APARENT.YOD(I) <> null
          OFFSET_LINE=this.APARENT.YOD(I).AORDER-this.AORDER
          If OFFSET_LINE>=0
            # Assign the SORT property          
            this.APARENT.YOD(I).YSORT = BEF_LINE_SORT + MY_STEP + (OFFSET_LINE * MY_STEP)
          Endif
        Endif
      Next I
    Endif

  # If the new inserted line is at the last position in the array
  Else #If AFT_LINE = 0
    # If a previous line exist, we know the last value used, otherwise it is 0
    If BEF_LINE <> 0 : BEF_LINE_SORT = this.APARENT.YOD(BEF_LINE).YSORT : Endif
    # Assign the SORT property
    this.SORT = BEF_LINE_SORT + MY_STEP
  Endif

Return

At representation level

At representation level, the only code that has been added is the one that triggers a computation of the total every time a unit price is modified or a line is added. This code does not have to be inserted in the class code; otherwise, when running in service mode, the computation is repeated several times.

There is no guarantee that the total price will be displayed every time a modification is made on the price due to a bad web connection, especially with mobile devices, or data sent by group. Every time an exchange is made between the client and the server, the client returns the modifications made on prices since the last time, and the server sends back the computed total price accordingly.

The implemented code executes the following rules:

The computation of the price is done by calling the 'YTOAMOUNT' method on the order header instance, which provides the following code:
$METHODS
  Case CURPTH
    When "YOH.YOD" : # Here we are at the line level, "this" is the line, "this.APARENT" is the header
      Case ACTION
        When "PROPAGATE"
          this.APARENT.YTOTAL= fmet this.APARENT.YTOTAMOUNT
      Endcase
    When "YOH.YOD.YOL" : # Here we are at the sub-line level, "this" is the sub-line, "this.APARENT.APARENT" is the header
      Case ACTION
        When "PROPAGATE"
          this.APARENT.APARENT.YTOTAL= fmet this.APARENT.APARENT.YTOTAMOUNT
      Endcase
  Endcase
Return

$PROPERTIES
  Case CURPRO
    When "YOH.YOD.YPRICE"
      Case ACTION
        When "PROPAGATE"
          this.APARENT.YTOTAL= fmet this.APARENT.YTOTAMOUNT
      Endcase
    When "YOH.YOD.YOL.YQTY", "YOH.YOD.YOL.YUNITPRICE"
      Case ACTION
        When "PROPAGATE"
          this.APARENT.APARENT.YTOTAL= fmet this.APARENT.APARENT.YTOTAMOUNT
      Endcase
  Endcase
Return

Validation and deletion management

In a second step of this development, a validation operation has been added. Being an operation, it is considered as stateless. When the event is caught, 'this.YORDNUM' (the key) is filled but the instance is not. A complex operation needs to perform an 'AREAD' method first. In this case, the only operation to perform is to update the 'YSTATUS' value to 2 if it is not already done. If it is already the case, a warning is returned.

An important point is that for the moment, using error handling classes and methods is useless in an operation context because the instance in which the operation runs is volatile and will disappear with the corresponding 'AERROR' child class. The only solution is to return a single status, which is done through the 'ARET_VALUE' parameter.

The code of the operation has been defined in the 'YORDER_CSPE' script, just after the '$METHODS' label. If 'CURPTH' is empty, the code should be added after the lines managing the "AINSERT_CONTROL_BEFORE" and "AUPDATE_CONTROL_BEFORE" methods.

The code is as follows:

       When "YVALIDATE"
            Local File YORDER

            # Execute the update transaction
            Trbegin [YOH]
            Update [YOH] Where YORDNUM=[L]YORDNUM and YSTATUS=1 With YSTATUS=2
            Commit

            # If no record updated, it can be because the order does not exist or because already validated
            If adxuprec=0
              Read [YOH]YOH0=[L]YORDNUM
              # Send a warning status if order already validated, and error status if order doesn't exist
              If fstat=[V]CST_AOK
                ARET_VALUE=[V]CST_AWARNING
              Else
                ARET_VALUE=[V]CST_AERROR
              Endif
            Endif
     Endcase

When a deletion is requested, the system also controls that the 'YSTATUS' is not equal to 2; otherwise, the deletion is rejected. This will be added in sequence to the previous lines. When 'CURPTH' is empty it is also done at the header level in the '$METHODS' section.

The code is the following:

       # The order can only be deleted if YSTATUS is 1
       When "ADELETE_BEFORE"
            If this.YSTATUS<>1
              VAL_OK=fmet this.ASETERROR('','Order validated ',[V]CST_AERROR)
            Endif