Function Scheduling_Services(@Service, @Params)
/***********************************************************************************************************************

    This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
    permission from SRP Computer Solutions, Inc.

    Name        :   Scheduling_Services

    Description :   Handler program for all module related services.

    Notes       :   The generic parameters should contain all the necessary information to process the services. Often
                    this will be information like the data Record and Key ID.

    Parameters  :
        Service         [in] -- Name of the service being requested
        Param1-10   [in/out] -- Additional request parameter holders
        Response       [out] -- Response to be sent back to the Controller (MCP) or requesting procedure

    Metadata    :

    History     :   (Date, Initials, Notes)
        06/01/17    dmb     Original programmer. - [EPIOI-8]

***********************************************************************************************************************/

#pragma precomp SRP_PreCompiler

$insert APP_INSERTS
$insert SERVICE_SETUP
$insert REACTOR_EQUATES
$insert SCHED_DET_EQUATES
$insert WO_LOG_EQUATES
$insert COMPANY_EQUATES
$insert PROD_VER_EQUATES
$insert PRS_LAYER_EQU
$insert SCHEDULE_EVENT_SUMMMARY_EQUATES
$insert WO_SCHEDULE_EQUATES
$insert SCHED_DET_KEY_IDS_EQUATES
$insert WORK_ORDER_CHILD_KEY_IDS_EQUATES
$insert SCHEDULER_EQUATES

Equ NOTIFICATION_PERIOD$    to 12
Equ SECONDS_PER_DAY$        to 86400
Equ MIDNIGHT_AM$            to 0        ; // Midnight represented as the beginning of the day.
Equ MIDNIGHT_PM$            to 86400    ; // Midnight represented as the end of the day.
Equ SIX_AM$                 to 21600
Equ NOON$                   to 43200
Equ SIX_PM$                 to 64800

Declare subroutine  Error_Services, Scheduling_Services, Memory_Services, RList, Database_Services, SRP_JSON
Declare function    SRP_Array, Scheduling_Services, Memory_Services, Database_Services, SRP_Sort_Array, Work_Order_Services
Declare function    Epi_Part_Services, SRP_Math, SRP_Hash, obj_Prod_Spec, Reactor_Services, SRP_JSON

GoToService else
    Error_Services('Set', Service : ' is not a valid service request within the ' : ServiceModule : ' services module.')
end

Return Response else ''


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Services
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//----------------------------------------------------------------------------------------------------------------------
// GetScheduleEvents
//
// Returns a JSON formatted array of schedule event objects. Schedule events will be filtered according to the search
// arguments passed in.
//----------------------------------------------------------------------------------------------------------------------
Service GetScheduleEvents(StartDate, EndDate, ReactorNo, WorkOrderNo, BlockOut)
Debug
    If Not(Num(StartDate)) then StartDate = Iconv(StartDate, 'D')
    If Not(Num(EndDate)) then EndDate = Iconv(EndDate, 'D')
    If BlockOut NE True$ then BlockOut = False$

    ScheduleEvents = ''

    ServiceKeyID   := '*' : StartDate : '*' : EndDate : '*' : ReactorNo : '*' : WorkOrderNo : '*' : BlockOut
    ServiceKeyID    = SRP_Hash(ServiceKeyID, 'SHA-1')

    If (StartDate NE '') then
        If Memory_Services('IsValueCurrent', ServiceKeyID, 15, True$) then
            ScheduleEvents  = Memory_Services('GetValue', ServiceKeyID)
        end else
            If SRP_JSON(objScheduleEvents, 'NEW', 'OBJECT') then
                If SRP_JSON(objScheduleEventArray, 'NEW', 'ARRAY') then
                    SchedulerKeyIDs = Scheduling_Services('SearchScheduler', StartDate, EndDate, ReactorNo, WorkOrderNo, BlockOut)
                    If Error_Services('NoError') then
                        If SchedulerKeyIDs NE '' then
                            SchedulerKeyIDs = SRP_Array('SortRows', SchedulerKeyIDs, 'AR1' : @FM : 'AR2' : @FM : 'AR3', 'LIST', @FM, '*')
                            For Each SchedulerKeyID in SchedulerKeyIDs using @FM setting fPos
                                ReactorNo       = SchedulerKeyID[1, '*']
                                StartDate       = SchedulerKeyID[Col2() + 1, '*']
                                EventID         = SchedulerKeyID[Col2() + 1, '*']
                                SRP_Stopwatch('Start', 'GetScheduleEvent')
                                ScheduleEvent   = Scheduling_Services('GetScheduleEvent', ReactorNo, StartDate, EventID, False$)
                                SRP_Stopwatch('Stop', 'GetScheduleEvent')
                                If SRP_JSON(objScheduleEvent, 'PARSE', ScheduleEvent) EQ '' then
                                    SRP_JSON(objScheduleEventArray, 'ADD', objScheduleEvent)
                                    SRP_JSON(objScheduleEvent, 'RELEASE')
                                end
                            Next SchedDetKeyID
                            SRP_JSON(objScheduleEvents, 'SET', 'ScheduleEvents', objScheduleEventArray)
                            ScheduleEvents  = SRP_JSON(objScheduleEvents, 'STRINGIFY', 'FAST')
                            Memory_Services('SetValue', ServiceKeyID, ScheduleEvents)
                        end
                    end
                    SRP_JSON(objScheduleEventArray, 'RELEASE')
                end else
                    Error_Services('Add', 'Error creating objScheduleEventArray in the ' : Service : ' service.')
                end
                SRP_JSON(objScheduleEvents, 'RELEASE')
            end else
                Error_Services('Add', 'Error creating objScheduleEvents in the ' : Service : ' service.')
            end
        end
    end else
        Error_Services('Add', 'StartDate argument was missing from the ' : Service : ' service.')
    end

    Response    = ScheduleEvents

end service


//----------------------------------------------------------------------------------------------------------------------
// SearchScheduler
//
// Returns an array of SCHEDULER Key IDs based on the search parameters provided.
//----------------------------------------------------------------------------------------------------------------------
Service SearchScheduler(StartDate, EndDate, ReactorNo, WorkOrderNo, BlockOut)

    SchedulerKeyIDs = ''

    If Not(Num(StartDate)) then StartDate = Iconv(StartDate, 'D')
    If Not(Num(EndDate)) then EndDate = Iconv(EndDate, 'D')
    If BlockOut NE True$ then BlockOut = False$

    ServiceKeyID   := '*' : StartDate : '*' : EndDate : '*' : ReactorNo : '*' : WorkOrderNo : '*' : BlockOut
    ServiceKeyID    = SRP_Hash(ServiceKeyID, 'SHA-1')

    If (StartDate NE '') then
        If EndDate NE '' then
            DateRange       = Oconv(StartDate - 1, 'D4/') : '~' : Oconv(EndDate + 1, 'D4/')
            DateRangeMatch  = Database_Services('SearchIndex', 'SCHEDULER', 'START_DATE', DateRange, True$)
        end else
            DateRangeMatch  = Database_Services('SearchIndex', 'SCHEDULER', 'START_DATE', '>=' : StartDate, True$)
        end
        Transfer DateRangeMatch to SchedulerKeyIDs
        If ReactorNo NE '' then
            ReactorMatch    = Database_Services('SearchIndex', 'SCHEDULER', 'REACTOR_NO', ReactorNo, True$)
            SchedulerKeyIDs = SRP_Array('Join', SchedulerKeyIDs, ReactorMatch, 'AND', @FM)
        end
        If WorkOrderNo NE '' then
            WOChildKeyIDs   = Database_Services('ReadDataRow', 'WORK_ORDER_CHILD_KEY_IDS', WorkOrderNo, True$, 5)
            WorkOrderMatch  = WOChildKeyIDs<WORK_ORDER_CHILD_KEY_IDS.SCHEDULER_KEY_IDS$>
            Convert @VM to @FM in WorkOrderMatch
            SchedulerKeyIDs = SRP_Array('Join', SchedulerKeyIDs, WorkOrderMatch, 'AND', @FM)
        end
        // Only filter out or return Block Out events if the Reactor No is specified. Otherwise, all Block Out
        // will be included.
        If ReactorNo NE '' then
            BlockOutMatch   = Database_Services('SearchIndex', 'SCHEDULER', 'BLOCK_OUT', 'Yes', True$)
            If BlockOut then
                SchedulerKeyIDs = SRP_Array('Join', SchedulerKeyIDs, BlockOutMatch, 'AND', @FM)
            end else
                SchedulerKeyIDs = SRP_Array('Join', SchedulerKeyIDs, BlockOutMatch, 'NOT', @FM)
            end
        end
    end else
        Error_Services('Add', 'StartDate argument was missing from the ' : Service : ' service.')
    end

    Response    = SchedulerKeyIDs

end service


//----------------------------------------------------------------------------------------------------------------------
// AdjustScheduleEvents
//
// Adjusts the schedule detail rows for the indicated reactor. This service will add or remove events for the indicated
// work order based on the number of events specified. This means other events for other work orders will be shifted
// as needed to allow the specified work order to have continuous events as well as eliminating gaps in the scheduler
// calendar. Note: blocked dates will need to be respected.
//----------------------------------------------------------------------------------------------------------------------
Service AdjustScheduleEvents(ReactorNo, WorkOrderNo, NumberOfDays, Date, Description)
Debug
    NumberOfEvents      = SRP_Math('CEILING', NumberOfDays, '', 0)
    
    If Date EQ '' then Date = Date()
    If Not(Num(Date)) then Date = Iconv(Date, 'D')
    AllWorkOrderEvents  = ''
    PreviousEvents      = ''    ; // Used to track events that already exist on the schedule for this date that
                                ; // should be left alone.
    RemoveEventKeyIDs   = ''    ; // List of Scheduler Key IDs that will be removed when the service is finished.
    AddEvents           = ''    ; // List of Scheduler Events (Appointments) that will be added when the service is finished.

    If (ReactorNo NE '') AND (WorkOrderNo NE '') AND (NumberOfEvents NE '') AND (NumberOfEvents NE 0) then
        // Get the block out events for this reactor.
        BlockOutEvents  = Scheduling_Services('GetScheduleEvents', Date - 1, '', ReactorNo, '', True$)
        BlockOutKeys    = SRP_Array('Rotate', BlockOutEvents, @FM, @VM)
        BlockOutKeys    = BlockOutKeys<2>
        BlockOutDates   = BlockOutKeys
        Convert @VM to @FM in BlockOutDates
        Convert '*' to @VM in BlockOutDates
        BlockOutDates   = SRP_Array('Rotate', BlockOutDates, @FM, @VM)
        BlockOutDates   = BlockOutDates<4>

        // Get the indicated work order events for this reactor.
        WorkOrderEvents = Scheduling_Services('GetScheduleEvents', Date - 1, '', ReactorNo, WorkOrderNo, False$)
        WorkOrderKeys   = SRP_Array('Rotate', WorkOrderEvents, @FM, @VM)
        WorkOrderKeys   = WorkOrderKeys<2>
        Convert @VM to @FM in WorkOrderKeys
        Convert '*' to @VM in WorkOrderKeys
        WorkOrderKeys   = SRP_Array('Rotate', WorkOrderKeys, @FM, @VM)
        WorkOrderKeys   = Delete(WorkOrderKeys, 1, 0, 0)   ; // Remove the work orders
        WorkOrderKeys   = Delete(WorkOrderKeys, 1, 0, 0)   ; // Remove the EPI Part
        WorkOrderKeys   = SRP_Array('Rotate', WorkOrderKeys, @FM, @VM)
        Convert @VM to '*' in WorkOrderKeys

        // Get the other work order events for this reactor.
        OtherEvents     = Scheduling_Services('GetScheduleEvents', Date - 1, '', ReactorNo, '', False$)
        OtherEvents     = SRP_Array('Join', OtherEvents, WorkOrderEvents, 'NOT', @FM)
        OtherKeys       = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        OtherKeys       = OtherKeys<2>
        Convert @VM to @FM in OtherKeys
        Convert '*' to @VM in OtherKeys
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the work orders
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the EPI Part
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        Convert @VM to '*' in OtherKeys

        // Initialize variables.
        EventCount      = 0

        If OtherKeys NE '' then
            // There are other schedule events for this reactor. Check to see if any of them fall on the same day the date assigned to the
            // service (either the passed in date or the default date). If any exist, a determination needs to be made to see which one
            // (if any) is the tail end of an existing work order series.

            // Get all existing events for the previous and current dates.
            CompareEvents   = Scheduling_Services('GetScheduleEvents', Date - 1, Date, ReactorNo, '', False$)

            If CompareEvents NE '' then
                // There are other events already scheduled for the start date. Loop through each event to see if it is the tail end of an
                // existing work order series.
                For Each CompareEvent in CompareEvents using @FM
                    CompareSchedulerKeyID   = CompareEvent<0, 2>
                    CompareWorkOrder        = CompareSchedulerKeyID[1, '*']
                    CompareScheduleKeyID    = Field(CompareSchedulerKeyID, '*', 3, 3)
                    // Check to see if this work order has an event in the past.
                    Events                  = Scheduling_Services('GetScheduleEvents', Date - 1, Date - 1, ReactorNo, CompareWorkOrder, False$)
                    If Events NE '' then PreviousEvents := Events : @FM
                Next ScheduleEvent
                PreviousEvents[-1, 1]   = ''
                If PreviousEvents NE '' then
                    // Since there is one or more work orders already on the schedule that should not be adjusted, find the one with the date furthest
                    // out into the future. If its date is after the proposed start date, adjust the start date to occur on the same date.
                    LatestDate  = ''
                    For Each PreviousEvent in PreviousEvents using @FM
                        PreviousSchedulerKeyID  = PreviousEvent<0, 2>
                        PreviousWorkOrder       = PreviousSchedulerKeyID[1, '*']
                        Events                  = Scheduling_Services('GetScheduleEvents', Date, '', ReactorNo, PreviousWorkOrder, False$)
                        If Events NE '' then
                            LastEvent           = Events[-1, 'B' : @FM]
                            LastSchedulerKeyID  = LastEvent<0, 2>
                            LastDate            = Field(LastSchedulerKeyID, '*', 4, 1)
                            If LastDate GT LatestDate then Transfer LastDate to LatestDate
                        end
                    Next PreviousEvent
                    If LatestDate GT Date then Transfer LatestDate to Date
                end
            end
        end

        If NumberOfEvents LT 0 then
            // The number of events is being reduced. Remove these from the end for this work order. Then find all other
            // work orders in the sfame reactor and shift them back by the number of events indicated.

            NumberOfEvents      = Neg(NumberOfEvents)
            // Reverse sort the keys and events. Always keep the keys and events in sync.
            WorkOrderKeys       = SRP_Array('SortRows', WorkOrderKeys, 'DR2', 'LIST', @FM, '*')
            WorkOrderEvents     = SRP_Array('SortRows', WorkOrderEvents, 'DR3', 'LIST', @FM, @VM)

            // Remove the extra work order events.
            Loop
                WorkOrderKey    = WorkOrderKeys<1>
                WorkOrderEvent  = WorkOrderEvents<1>
                WorkOrderKeys   = Delete(WorkOrderKeys, 1, 0, 0)
                WorkOrderEvents = Delete(WorkOrderEvents, 1, 0, 0)
                EventCount     += 1
                // Delete Schedule Detail record.
                RemoveEventKeyIDs  := WorkOrderEvent<0, 2> : @FM
                Scheduling_Services('DeleteScheduleDetail', WorkOrderKey)
            Until (EventCount GE NumberOfEvents)
            Repeat
            
            // Now that the extra work order events have been removed, check to see if there are any more
            // work order keys. If so, then use the last schedule date for the work order to be the
            // starting schedule date. Otherwise, go back to the previous schedule date.
            If WorkOrderKeys NE '' then
                ScheduleDate    = Field(WorkOrderKeys[1, @FM], '*', 2, 1)
            end else
                // All of the work order events were removed so use the date of the first event and then
                // subtract 1 to be safe in case the tip was not stacked with the tail of a previous
                // list of work orders.
                ScheduleDate    = Field(WorkOrderKey, '*', 2, 1) - 1
                // However, if the previous list of work orders is extended into the future already, then
                // use the latest date from that list instead since this is definitely the correct starting date.
                If ScheduleDate LT Date then ScheduleDate = Date
            end

        end else

            // The number of events is being increased (or added if this is a new work order). Increased work order events will be added to
            // the end of the existing eents. New work order events will be added to the indicated date unless a series of work orders
            // was already on the schedule. In this case the end of this work order series will be used as the start date. All other work order
            // events in the same reactor will be shifted forward.
            If WorkOrderKeys NE '' then
                // An already scheduled work order is being adjusted. Get the last scheduled event and read the schedule detail record to use
                // as a template for new events that will be added.
                ScheduleKeyID                   = WorkOrderKeys[-1, 'B' : @FM]
                ScheduleEvent                   = WorkOrderEvents[-1, 'B' : @FM]
                ScheduleDate                    = Field(ScheduleKeyID, '*', 2, 1)
*                ScheduleDetRow                  = Scheduling_Services('GetScheduleDetail', ScheduleKeyID)
                ScheduleDetRow                  = Database_Services('ReadDataRow', 'SCHED_DET', ScheduleKeyID)
                ScheduleDetRow<SCHED_DET_DESC$> = ''    ; // New schedule detail rows will not have any description.
            end else
                // There are no events for the indicated work order. This implies that a new work order is being added to the scheduler.

                // In order to work with the adjust logic, a pseudo-event needs to be created. This will also provide a template for the
                // new work order events being added to the schedule. Note, a sequence of 99 for the ScheduleKeyID is used to avoid any
                // possible conflicts with real schedule Key IDs.
                ScheduleKeyID   = ReactorNo : '*' : Date : '*' : 99
                WOLogRow        = Database_Services('ReadDataRow', 'WO_LOG', WorkOrderNo)
                NoteFlag        = Description NE ''
                CustNo          = WOLogRow<WO_LOG_CUST_NO$>
                EpiPartNo       = WOLogRow<WO_LOG_EPI_PART_NO$>
                WOClosedFlag    = False$
                HotLotFlag      = WOLogRow<WO_LOG_WO_MAT_KEY_HOTLOT$> NE ''

                If CustNo NE '' then
                    CompanyRow      = Database_Services('ReadDataRow', 'COMPANY', CustNo)
                    CustName        = CompanyRow<COMPANY_ABBREV$>
                end else
                    CustName        = ''
                end
                Begin Case
                    Case CustName _EQC 'International Rectifier'    ; CustName = 'IR'
                    Case CustName _EQC 'IRNewport'                  ; CustName = 'Newport'
                    Case CustName _EQC 'IFX (Kulim)'                ; CustName = 'Kulim'
                    Case CustName _EQC 'IFX Austria AG'             ; CustName = 'Austria'
                    Case CustName _EQC 'Tower Semiconductor'        ; CustName = 'Tower'
                End Case

                StartDTM        = (Date - 1) : '.0'
                EndDTM          = Date : '.0'
                CurrentFlag     = True$

                Begin Case
                    Case HotLotFlag
                        BackColor   = 'LightCoral'
                    Case Otherwise$
                        BackColor   = 'LightSteelBlue'
                End Case
                ForeColor           = 'Black'
                Title               = WorkOrderNo : ' (' : CustName : ' ' : EpiPartNo : ')'                        
                ScheduleEvent       = ReactorNo : @VM : WorkOrderNo : '*' : EpiPartNo : '*' : ScheduleKeyID : @VM : StartDTM : @VM : EndDTM : @VM : BackColor : @VM : ForeColor : @VM : Title
                ScheduleEvent      := @VM : Description : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : HotLotFlag : @SVM : NoteFlag : @SVM : WOClosedFlag : @SVM : CurrentFlag

                // Set the schedule date to be one day before the intended start date. This will allow the adjust logic to use the actual schedule start date.
                ScheduleDate        = Date - 1

                // Create the schedule detail database row.
                ScheduleDetRow                      = ''
                ScheduleDetRow<SCHED_DET_WO_NO$>    = WorkOrderNo
                ScheduleDetRow<SCHED_DET_DESC$>     = Description
            end

            // Create the new work order events.
            ScheduleDate   += 1
            DayRemain       = NumberOfDays
            Loop
                Locate ScheduleDate in BlockOutDates using @VM setting vPos then
                    // Do not schedule on the same date as a block out event.
                end else
                    Sequence    = 1
                    KeyFound    = False$    ; // Assume key not found for now.
                    Loop
                        NewScheduleKeyID    = ReactorNo : '*' : ScheduleDate : '*' : Sequence
                        Locate NewScheduleKeyID in OtherKeys using @FM setting fPos then
                            Sequence   += 1    
                        end else
                            // Increase the event count to determine if enough events have been created.
                            EventCount         += 1
                            KeyFound            = True$
                            If WorkOrderKeys EQ '' then
                                WorkOrderKeys   = NewScheduleKeyID
                            end else
                                WorkOrderKeys  := @FM : NewScheduleKeyID
                            end
                            SchedulerKey            = ScheduleEvent<0, 2>
                            SchedulerKey            = Field(SchedulerKey, '*', 1, 2) : '*' : NewScheduleKeyID
                            ScheduleEvent<0, 2>     = SchedulerKey
                            If @UserName EQ 'DANIEL_ST' then
                                If EventCount LT NumberOfEvents then
                                    // Assume this schedule detail will be a full day.
                                    DayLengthCode   = 4
                                end else
                                    // Calculate the portion of the day using the remaining time available.
                                    DayLengthCode   = DayRemain / .25
                                end
                                ScheduleDetRow<SCHED_DET_DAY_LENGTH_CODE$>  = DayLengthCode
                                DayLength   = DayLengthCode * .25
                                Swap '0.' with '.' in DayLength
                                Begin Case
                                    Case (EventCount EQ 1) OR (EventCount EQ NumberOfEvents)
                                        // This event must be the Head or Tail of the event series. Assume it will start
                                        // at the beginning of the day.
                                        StartDTM        = ScheduleDate : '.0'
                                        If DayLength EQ 1 then
                                            // This event is a full day.
                                            EndDTM          = ScheduleDate + 1  : '.0'
                                        end else
                                            EndDTM          = ScheduleDate : DayLength
                                        end
                                    Case Otherwise$
                                        // This event must be a standalone event or the middle of the event series.
                                        StartDTM        = ScheduleDate : '.0'
                                        EndDTM          = ScheduleDate + 1  : '.0'
                                End Case
                            end else
                                StartDTM    = ScheduleDate : '.0'
                                EndDTM      = ScheduleDate + 1 : '.0'
                            end
                            ScheduleEvent<0, 3>     = StartDTM
                            ScheduleEvent<0, 4>     = EndDTM
                            If WorkOrderEvents EQ '' then
                                WorkOrderEvents     = ScheduleEvent
                            end else
                                WorkOrderEvents    := @FM : ScheduleEvent
                            end
                            // Deduct from the number of days needed to create events for.
                            DayRemain  -= 1
                            // Create a new Schedule Event
                            AddEvents  := ScheduleEvent : @FM
                            Scheduling_Services('SetScheduleDetail', NewScheduleKeyID, ScheduleDetRow)
                        end
                    Until KeyFound
                    Repeat
                end
                ScheduleDate   += 1
            Until (EventCount GE NumberOfEvents)
            Repeat
            
            // This starts the other schedule events on the same date as the last new work order event.
            ScheduleDate    = Field(NewScheduleKeyID, '*', 2, 1)

        end

        // Now that the current work order list has been adjusted whether it is being reduced or increased, call the logic
        // to adjust other work order events.
        GoSub AdjustOtherWorkOrderEvents

    end else
        Error_Services('Add', 'ReactorNo, WorkOrderNo, or NumberOfEvents argument was missing from the ' : Service : ' service.')
    end

    // This service does not return a response to complete it's purpose, but for analysis and reporting purposes the adjusted
    // events will be returned to the caller if requested.
    RemoveEventKeyIDs[-1, 1]    = ''
    AddEvents[-1, 1]            = ''
    Response                    = RemoveEventKeyIDs : @RM : AddEvents

end service


//----------------------------------------------------------------------------------------------------------------------
// AddBlockOutEvents
//
// Adds one or more block out events to the schedule.
//----------------------------------------------------------------------------------------------------------------------
Service AddBlockOutEvents(ReactorNo, StartDate, EndDate, Description)

    // Always have an end date. No end date means this block out event will only be for one day.
    If EndDate EQ '' then EndDate = StartDate
    AllWorkOrderEvents  = ''
    PreviousEvents      = ''    ; // Used to track events that already exist on the schedule for this date that
                                ; // should be left alone.
    WorkOrderKeys       = ''
    WorkOrderEvents     = ''
    RemoveEventKeyIDs   = ''
    AddEvents           = ''

    If (ReactorNo NE '') AND (StartDate NE '') then
        // Get the block out events for this reactor.
        BlockOutEvents  = Scheduling_Services('GetScheduleEvents', StartDate - 1, '', ReactorNo, '', True$)

        // Get the other work order events for this reactor.
        OtherEvents     = Scheduling_Services('GetScheduleEvents', StartDate - 1, '', ReactorNo, '', False$)
        OtherEvents     = SRP_Array('Join', OtherEvents, BlockOutEvents, 'NOT', @FM)
        OtherKeys       = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        OtherKeys       = OtherKeys<2>
        Convert @VM to @FM in OtherKeys
        Convert '*' to @VM in OtherKeys
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the work orders
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the EPI Part
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        Convert @VM to '*' in OtherKeys

        // In order to work with the adjust logic, a pseudo-event needs to be created. This will also provide a template for the
        // new work order events being added to the schedule. Note, a sequence of 99 for the ScheduleKeyID is used to avoid any
        // possible conflicts with real schedule Key IDs.
        ScheduleKeyID   = ReactorNo : '*' : StartDate : '*' : 99
        NoteFlag        = Description NE ''
        WorkOrderNo     = ''
        CustNo          = ''
        EpiPartNo       = ''
        WOClosedFlag    = False$
        HotLotFlag      = False$
        CustName        = ''
        StartDTM        = StartDate : '.0'
        EndDTM          = (StartDate + 1) : '.0'
        CurrentFlag     = False$
        BackColor       = 'Plum'
        ForeColor       = 'Black'
        BlockOutType    = ''    ; // Future enhancement.
        Title   = 'Block Out' : ' (' : BlockOutType : ')'
        ScheduleEvent   = ReactorNo : @VM : WorkOrderNo : '*' : EpiPartNo : '*' : ScheduleKeyID : @VM : StartDTM : @VM : EndDTM : @VM : BackColor : @VM : ForeColor : @VM : Title
        ScheduleEvent  := @VM : Description : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : @VM : HotLotFlag : @SVM : NoteFlag : @SVM : WOClosedFlag : @SVM : CurrentFlag

        // Set the schedule date to the intended start date.
        ScheduleDate    = StartDate

        // Create the schedule detail database row.
        ScheduleDetRow                              = ''
        ScheduleDetRow<SCHED_DET_DESC$>             = Description
        ScheduleDetRow<SCHED_DET_BLOCK_OUT$>        = True$
        ScheduleDetRow<SCHED_DET_BLOCK_OUT_TYPE$>   = ''    ; // Future enhancement.

        // Create the new block out events.
        For ScheduleDate = StartDate to EndDate
            Sequence    = 1
            KeyFound    = False$    ; // Assume key not found for now.
            Loop
                NewScheduleKeyID    = ReactorNo : '*' : ScheduleDate : '*' : Sequence
                Locate NewScheduleKeyID in OtherKeys using @FM setting fPos then
                    Sequence   += 1    
                end else
                    KeyFound            = True$
                    SchedulerKey        = ScheduleEvent<0, 2>
                    SchedulerKey        = Field(SchedulerKey, '*', 1, 2) : '*' : NewScheduleKeyID
                    ScheduleEvent<0, 2> = SchedulerKey
                    ScheduleEvent<0, 3> = ScheduleDate : '.0'
                    ScheduleEvent<0, 4> = ScheduleDate + 1 : '.0'
                    // Create a new Schedule Event
                    AddEvents  := ScheduleEvent : @FM
                    Scheduling_Services('SetScheduleDetail', NewScheduleKeyID, ScheduleDetRow)
                end
            Until KeyFound
            Repeat
        Next ScheduleDate

        // Get an updated list of the block out events for this reactor. This will include the block outs
        // we just added.
        BlockOutEvents  = Scheduling_Services('GetScheduleEvents', StartDate, '', ReactorNo, '', True$)
        BlockOutKeys    = SRP_Array('Rotate', BlockOutEvents, @FM, @VM)
        BlockOutKeys    = BlockOutKeys<2>
        BlockOutDates   = BlockOutKeys
        Convert @VM to @FM in BlockOutDates
        Convert '*' to @VM in BlockOutDates
        BlockOutDates   = SRP_Array('Rotate', BlockOutDates, @FM, @VM)
        BlockOutDates   = BlockOutDates<4>

        // Get the other work order events for this reactor.
        OtherEvents     = Scheduling_Services('GetScheduleEvents', StartDate, '', ReactorNo, '', False$)
        OtherKeys       = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        OtherKeys       = OtherKeys<2>
        Convert @VM to @FM in OtherKeys
        Convert '*' to @VM in OtherKeys
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the work orders
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the EPI Part
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        Convert @VM to '*' in OtherKeys

        // Now that the block outs have been added, see if any work order events need to be adjusted.
        GoSub AdjustOtherWorkOrderEvents

    end else
        Error_Services('Add', 'ReactorNo or StartDate argument was missing from the ' : Service : ' service.')
    end

    // Return to the caller any events that were removed and added so the visual display can be updated if needed.
    RemoveEventKeyIDs[-1, 1]    = ''
    AddEvents[-1, 1]            = ''
    Response                    = RemoveEventKeyIDs : @RM : AddEvents

end service


//----------------------------------------------------------------------------------------------------------------------
// CancelBlockOutEvent
//
// Removes a block out event from the schedule.
//----------------------------------------------------------------------------------------------------------------------
Service CancelBlockOutEvent(BlockOutEventKeyID)

    AllWorkOrderEvents  = ''
    PreviousEvents      = ''    ; // Used to track events that already exist on the schedule for this date that
                                ; // should be left alone.
    WorkOrderKeys       = ''
    WorkOrderEvents     = ''
    RemoveEventKeyIDs   = ''
    AddEvents           = ''

    If BlockOutEventKeyID NE '' then

        ScheduleKeyID       = Field(BlockOutEventKeyID, '*', 3, 3)
        ReactorNo           = ScheduleKeyID[1, '*']
        Date                = ScheduleKeyID[Col2() + 1, '*']
        Sequence            = ScheduleKeyID[Col2() + 1, '*']
        RemoveEventKeyIDs  := BlockOutEventKeyID : @FM
        Scheduling_Services('DeleteScheduleDetail', ScheduleKeyID)

        // Set the schedule date to the block out date.
        ScheduleDate    = Date

        // Get an updated list of the block out events for this reactor. This will include the block outs
        // we just added.
        BlockOutEvents  = Scheduling_Services('GetScheduleEvents', Date, '', ReactorNo, '', True$)
        BlockOutKeys    = SRP_Array('Rotate', BlockOutEvents, @FM, @VM)
        BlockOutKeys    = BlockOutKeys<2>
        BlockOutDates   = BlockOutKeys
        Convert @VM to @FM in BlockOutDates
        Convert '*' to @VM in BlockOutDates
        BlockOutDates   = SRP_Array('Rotate', BlockOutDates, @FM, @VM)
        BlockOutDates   = BlockOutDates<4>

        // Get the other work order events for this reactor.
        OtherEvents     = Scheduling_Services('GetScheduleEvents', Date, '', ReactorNo, '', False$)
        OtherKeys       = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        OtherKeys       = OtherKeys<2>
        Convert @VM to @FM in OtherKeys
        Convert '*' to @VM in OtherKeys
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the work orders
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the EPI Part
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        Convert @VM to '*' in OtherKeys

        // Now that the block outs have been added, see if any work order events need to be adjusted.
        GoSub AdjustOtherWorkOrderEvents

    end else
        Error_Services('Add', 'BlockOutEventKeyID argument was missing from the ' : Service : ' service.')
    end

    // Return to the caller any events that were removed and added so the visual display can be updated if needed.
    RemoveEventKeyIDs[-1, 1]    = ''
    AddEvents[-1, 1]            = ''
    Response                    = RemoveEventKeyIDs : @RM : AddEvents

end service


//----------------------------------------------------------------------------------------------------------------------
// GetScheduleEvent
//
// Returns a JSON formatted object of information for a specific schedule event.
//----------------------------------------------------------------------------------------------------------------------
Service GetScheduleEvent(ReactorNo, StartDate, EventID)
SRP_Stopwatch('Start', Service)
    If Not(Num(StartDate)) then StartDate = Iconv(StartDate, 'D')

    ServiceKeyID   := '*' : ReactorNo : '*' : StartDate : '*' : EventID
    ScheduleEvent   = ''
    If (ReactorNo NE '') AND (StartDate NE '') AND (EventID NE '') then
        If SRP_JSON(objScheduleEvent, 'NEW', 'OBJECT') then
            SchedulerKeyID  = ReactorNo : '*' : StartDate : '*' : EventID
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'EventID', EventID, 'STRING')
            SchedulerRow    = Database_Services('ReadDataRow', 'SCHEDULER', SchedulerKeyID, True$, 15)
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'StartDate', Oconv(StartDate, 'D4/'), 'STRING')
            EndDate         = SchedulerRow<SCHEDULER.END_DATE$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'EndDate', Oconv(EndDate, 'D4/'), 'STRING')
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'ModifiedDTM', Oconv(SchedulerRow<SCHEDULER.MODIFIED$>, 'DT/4^HS'), 'STRING')
            Note            = SchedulerRow<SCHEDULER.DESCRIPTION$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'Note', Note, 'STRING')
            BlockOut        = SchedulerRow<SCHEDULER.BLOCK_OUT$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'BlockOut', Oconv(BlockOut, 'BYes,No'), 'STRING')
            BlockOutType    = SchedulerRow<SCHEDULER.BLOCK_OUT_TYPE$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'BlockOutType', BlockOutType, 'STRING')
            BackColor       = SchedulerRow<SCHEDULER.BACKCOLOR$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'BackColor', BackColor, 'STRING')
            ForeColor       = SchedulerRow<SCHEDULER.FORECOLOR$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'ForeColor', ForeColor, 'STRING')
            StartTime       = SchedulerRow<SCHEDULER.START_TIME$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'StartTime', Oconv(StartTime, 'MTH'), 'STRING')
            EndTime         = SchedulerRow<SCHEDULER.END_TIME$>
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'EndTime', Oconv(EndTime, 'MTH'), 'STRING')
            Reactor         = Reactor_Services('GetReactor', ReactorNo)
            If SRP_JSON(objReactor, 'PARSE', Reactor) EQ '' then
                SRP_JSON(objScheduleEvent, 'SET', 'Reactor', objReactor)
                SRP_JSON(objReactor, 'RELEASE')
            end
            WorkOrderNo     = EventID
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'WorkOrderStep', SchedulerRow<SCHEDULER.WO_STEP$>, 'STRING')
            WorkOrder       = Work_Order_Services('GetWorkOrder', WorkOrderNo, False$)
            If SRP_JSON(objWorkOrder, 'PARSE', WorkOrder) EQ '' then
                SRP_JSON(objScheduleEvent, 'SET', 'WorkOrder', objWorkOrder)
                WorkOrderNo     = SRP_JSON(objWorkOrder, 'GETVALUE', 'WorkOrderNumber')
                EpiPartNo       = SRP_JSON(objWorkOrder, 'GETVALUE', 'EpiPartNumber')
                CustNameShort   = SRP_JSON(objWorkOrder, 'GETVALUE', 'Company.NameShort')
                SRP_JSON(objWorkOrder, 'RELEASE')
            end else
                WorkOrderNo     = ''
                EpiPartNo       = ''
                CustNameShort   = ''
            end
            StartDateTime       = StartDate + (StartTime / SECONDS_PER_DAY$)
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'StartDateTime', Oconv(StartDateTime, 'DT2/^H'), 'STRING')
            EndDateTime         = EndDate + (EndTime / SECONDS_PER_DAY$)
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'EndDateTime', Oconv(EndDateTime, 'DT2/^H'), 'STRING')
            TotalScheduledDays  = EndDateTime - StartDateTime
            TotalScheduledDays  = SRP_Math('CEILING', TotalScheduledDays, '', 2)
            SRP_JSON(objScheduleEvent, 'SETVALUE', 'TotalScheduledDays', TotalScheduledDays, 'STRING')
            ScheduleEvent   = SRP_JSON(objScheduleEvent, 'STRINGIFY', 'FAST')
            SRP_JSON(objScheduleEvent, 'RELEASE')
        end else
            Error_Services('Add', 'Error creating objScheduleEvent in the ' : Service : ' service.')
        end
    end else
        Error_Services('Add', 'ReactorNo, StartDate, or EventID argument was missing from the ' : Service : ' service.')
    end

    Response    = ScheduleEvent
SRP_Stopwatch('Start', Service)
end service


//----------------------------------------------------------------------------------------------------------------------
// SetScheduleDetail
//
// Creates or updates the SCHED_DET row for the indicated Key ID.
//----------------------------------------------------------------------------------------------------------------------
Service SetScheduleDetail(ScheduleKeyID, ScheduleDetail)

    If (ScheduleKeyID NE '') AND (ScheduleDetail NE '') then
        TableName       = 'SCHED_DET'
        Database_Services('WriteDataRow', Tablename, ScheduleKeyID, ScheduleDetail, True$)
    end else
        Error_Services('Add', 'ScheduleKeyID or ScheduleDetail argument was missing from the ' : Service : ' service.') 
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// AddScheduleEvents
//
// Adds schedule events for the indicated work order on the indicated reactor starting on the indicated start date for
// the indicated number of days. All future schedule events will automatically be adjusted future into the future
// accordingly. The exception to this are schedule events which begin on or before the indicated start date. In this
// case, the new schedule events will be placed after the current schedule event.
//----------------------------------------------------------------------------------------------------------------------
Service AddScheduleEvents(WorkOrderNo, ReactorNo, RequestedStartDate, NumberOfDays, Description)

    If Not(Num(RequestedStartDate)) then RequestedStartDate = Iconv(RequestedStartDate, 'D')
    If NumberOfDays EQ '' then
        NumberOfDays    = Scheduling_Services('GetAdjustedDays', WorkOrderNo)
    end

    CancelledSchedDetKeyIDs = ''
    AddedSchedDetKeyIDs     = ''
Debug
    If (WorkOrderNo NE '') AND (ReactorNo NE '') AND (RequestedStartDate NE '') AND (NumberOfDays GT 0) then
        ActualStartDate     = ''                                ; // This will be used to store the actual start date the system is able to find.
        RemainingSeconds    = NumberOfDays * SECONDS_PER_DAY$   ; // Convert the total days of the new schedule events into seconds.
        Loop
            // Get the SCHED_DET Key IDs already scheduled for this reactor and schedule date. The basic rule is that no
            // schedule event already engaged (i.e., started on or before the requested start date) will be moved. The new
            // schedule event must begin in the first available time slot on or after the requested start date. All
            // schedule events that start after the requested start date will be moved into the future to make room for
            // new schedule events.
*            SchedDetKeyIDsRow   = Database_Services('ReadDataRow', 'SCHED_DET_KEY_IDS', ReactorNo : '*' : RequestedStartDate)
            WOSchedDetKeyIDs    = SchedDetKeyIDsRow<SCHED_DET_KEY_IDS_WORK_ORDER_SCHED_DET_KEY_IDS$>
            BOSchedDetKeyIDs    = SchedDetKeyIDsRow<SCHED_DET_KEY_IDS_BLOCK_OUT_SCHED_DET_KEY_IDS$>
            SchedDetKeyIDs      = SRP_Array('Clean', WOSchedDetKeyIDs : @VM : BOSchedDetKeyIDs, 'TrimAndMakeUnique', @VM)
            If SchedDetKeyIDs EQ '' then
                // Nothing is already scheduled. Use this date and set the start time (midnight). Calculate the end time
                // based on whether there are more seconds in the schedule than are in a full day.
                ActualStartDate     = RequestedStartDate
                ActualStartTime     = MIDNIGHT_AM$
                AvailableSeconds    = SECONDS_PER_DAY$
                If RemainingSeconds GE AvailableSeconds then
                    ActualStopTime  = MIDNIGHT_PM$
                end else
                    ActualStopTime  = RemainingSeconds
                end
            end else
                // There are existing schedule events. Check to see if there is any remaining portion of the day that
                // can be used to start the new schedule event.
                GoSub GetLastEndTime    ; // Assumes SchedDetKeyIDs is populated. Returns LastEndTime variable.
                If LastEndTime LT MIDNIGHT_PM$ then
                    // The last schedule event on this date ends before midnight. Use this date and set the
                    // start time to be the end time of the existing schedule event.
                    ActualStartDate     = RequestedStartDate
                    ActualStartTime     = LastEndTime
                    AvailableSeconds    = SECONDS_PER_DAY$ - ActualStartTime
                    If RemainingSeconds GE AvailableSeconds then
                        ActualStopTime  = MIDNIGHT_PM$
                    end else
                        ActualStopTime  = RemainingSeconds
                    end
                end
            end
        Until ActualStartDate NE ''
            RequestedStartDate += 1
        Repeat
        
        // To simplify the process of adding schedule events, remove all future schedule events related to work orders.
        // Save all future SCHED_DET rows into cache so they can be added back later.
        If ActualStopTime EQ MIDNIGHT_PM$ then
            // Start the search on the next date since the new schedule event will end at midnight.
            SearchDate  = ActualStartDate + 1
        end else
            // Start the search on the actual start date since the new schedule event will end before
            // midnight.
            SearchDate  = ActualStartDate
        end
        SchedDetKeyIDs  = Scheduling_Services('SearchSchedDet', SearchDate, '', ReactorNo, '', False$)
        If SchedDetKeyIDs NE '' then
            For Each SchedDetKeyID in SchedDetKeyIDs using @FM
                MoveScheduleEvent   = False$    ; // Assume false for now.
                FutureSchedDetRow   = Database_Services('ReadDataRow', 'SCHED_DET', SchedDetKeyID)
                FutureReactorNo     = SchedDetKeyID[1, '*']
                FutureScheduleDate  = SchedDetKeyID[Col2() + 1, '*']
                If ActualStartDate EQ FutureScheduleDate then
                    // Make sure this schedule event occurs after the new schedule event before nominating it to be
                    // moved.
                    FutureStartTime     = FutureSchedDetRow<SCHED_DET_START_TIME$>
                    If FutureStartTime GT ActualStartTime then MoveScheduleEvent = True$
                end else
                    MoveScheduleEvent   = True$
                end
                If MoveScheduleEvent EQ True$ then
                    Memory_Services('SetValue', Service : '*' : SchedDetKeyID, FutureSchedDetRow)
*                  Database_Services('DeleteDataRow', 'SCHED_DET', SchedDetKeyID, True$)
                end
            Next SchedDetKeyID
        end

        // Create the SCHED_DET row base.
        SchedDetRow                         = ''
        SchedDetRow<SCHED_DET_WO_NO$>       = WorkOrderNo
        SchedDetRow<SCHED_DET_DESC$>        = Description
        SchedDetRow<SCHED_DET_MODIFIED$>    = Iconv(Oconv(Date(), 'D4/') : ' ' : Oconv(Time(), 'MTH'), 'DTM')

        // Now add the new schedule events. Stop when the number of seconds remaining to be scheduled reaches 0.
        // Schedule around block out events if they exist.
        Transfer ActualStartDate to ScheduleDate    ; // Begin with the actual start date.
        Transfer ActualStartTime to StartTime       ; // Begin with the actual start time.
        Loop
        Until RemainingSeconds LE 0
            SchedDetKeyIDsRow   = Database_Services('ReadDataRow', 'SCHED_DET_KEY_IDS', ReactorNo : '*' : ScheduleDate)
            SchedDetKeyIDs      = SchedDetKeyIDsRow<SCHED_DET_KEY_IDS_BLOCK_OUT_SCHED_DET_KEY_IDS$>
            If SchedDetKeyIDs NE '' then
                // There are existing block out schedule events. Check to see if there is any remaining portion of the
                // day that can be used to start the new schedule event.
                GoSub GetLastEndTime    ; // Assumes SchedDetKeyIDs is populated. Returns LastEndTime variable.
                If LastEndTime LT MIDNIGHT_PM$ then
                    // The last schedule event on this date ends before midnight. Use this date and set the
                    // start time to be the end time of the existing schedule event.
                    StartTime   = LastEndTime
                end
            end

            SchedDetRow<SCHED_DET_START_TIME$>  = StartTime
            // Calculate the portion of the day remaining in the current day. Most of the time this will be the whole
            // day (i.e., DayPortion = 1), but if this is the first day of the schedule event, it might be starting
            // late in the day.
            AvailableSeconds    = SECONDS_PER_DAY$ - StartTime
            If RemainingSeconds GE AvailableSeconds then
                StopTime    = MIDNIGHT_PM$
            end else
                StopTime    = RemainingSeconds
            end
            RemainingSeconds                   -= AvailableSeconds
            SchedDetRow<SCHED_DET_END_TIME$>    = StopTime
            // Find the next available SchedDetKeyID for the curent reactor and schedule date and create the
            // SCHED_DET row.
            SchedDetKeyIDsRow   = Database_Services('ReadDataRow', 'SCHED_DET_KEY_IDS', ReactorNo : '*' : ScheduleDate)
            SchedDetKeyIDs      = SchedDetKeyIDsRow<SCHED_DET_KEY_IDS_WORK_ORDER_SCHED_DET_KEY_IDS$>
            SchedDetKeyIDs      = SRP_Array('SortRows', SchedDetKeyIDs, 'DR3', 'LIST', @VM, '*')
            // Get the last sequence number used and increase by one to create a unique SchedDetKeyID.
            Sequence            = Field(SchedDetKeyIDs<0, 1>, '*', 3, 1) + 1
            SchedDetKeyID       = ReactorNo : '*' : ScheduleDate : '*' : Sequence
            Database_Services('WriteDataRow', 'SCHED_DET', SchedDetKeyID, SchedDetRow, True$)
            If Error_Services('NoError') then
                // SCHED_DET row was successfully created. Add the Key ID to the list.
                AddedSchedDetKeyIDs    := SchedDetKeyID : @VM
            end

            // Increment the schedule date and reset the start time.
            ScheduleDate   += 1
            StartTime       = MIDNIGHT_AM$
        Repeat
        AddedSchedDetKeyIDs[-1, 1]  = ''
    end else
        Error_Services('Add', 'WorkOrderNo, ReactorNo, RequestedStartDate, or NumberOfDays argument was missing from the ' : Service : ' service.')
    end

    Response    = CancelledSchedDetKeyIDs

end service


//----------------------------------------------------------------------------------------------------------------------
// CancelScheduleEvents
//
// Cancels all schedule events for the indicated work order. If a reactor number is specified, then only events on that
// reactor are cancelled. The adjust future events flag indicates whether events in the future should be automatically
// adjusted, meaning that an attempt will be made to relocate these events to fill in the gap left behind by the
// cancelled events.
//----------------------------------------------------------------------------------------------------------------------
Service CancelScheduleEvents(WorkOrderNo, ReactorNo, AdjustFutureEvents)

    CancelledSchedDetKeyIDs = ''

    If WorkOrderNo NE '' then
        WOScheduleRow   = Database_Services('ReadDataRow', 'WO_SCHEDULE', WorkOrderNo)
        SchedDetKeyIDs  = WOScheduleRow<WO_SCHEDULE_SCHED_DET_KEY_IDS$>
        If SchedDetKeyIDs NE '' then
            For Each SchedDetKeyID in SchedDetKeyIDs using @VM
                ScheduleReactorNo   = SchedDetKeyID[1, '*']
                If (ReactorNo EQ '') OR (ReactorNo EQ ScheduleReactorNo) then
                   Database_Services('DeleteDataRow', 'SCHED_DET', SchedDetKeyID, True$)
                    If Error_Services('NoError') then
                        CancelledSchedDetKeyIDs    := SchedDetKeyID : @FM
                    end
                end
            Next SchedDetKeyID
        end
        CancelledSchedDetKeyIDs[-1, 1]  = ''
    end else
        Error_Services('Add', 'WorkOrderNo argument was missing from the ' : Service : ' service.')
    end

    Response    = CancelledSchedDetKeyIDs

end service


//----------------------------------------------------------------------------------------------------------------------
// DeleteScheduleDetail
//
// Deletes the SCHED_DET row for the indicated Key IDs.
//----------------------------------------------------------------------------------------------------------------------
Service DeleteScheduleDetail(ScheduleKeyIDs)

    If ScheduleKeyIDs NE '' then
        TableName       = 'SCHED_DET'
        For Each ScheduleKeyID in ScheduleKeyIDs using @FM
            Database_Services('DeleteDataRow', Tablename, ScheduleKeyID, True$)
        Next ScheduleKeyID
    end else
        Error_Services('Add', 'ScheduleKeyID argument was missing from the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// GetAdjustedDays
//
// Returns the calculated adjusted number of days needed to process the remaining wafers for the indicated work order.
// Note: this can return a negative value if there are more scheduled events needed to complete the remaining wafers.
//----------------------------------------------------------------------------------------------------------------------
Service GetAdjustedDays(WorkOrderNo)

    ServiceKeyID   := '*' : WorkOrderNo
    AdjustedDays    = ''

    If Memory_Services('IsValueCurrent', ServiceKeyID, 5, True$) then
        AdjustedDays    = Memory_Services('GetValue', ServiceKeyID)
    end else
        If WorkOrderNo NE '' then
            // Get the list of existing SCHED_DET Key IDs sorted by Schedule Date.
            WOScheduleRow   = Database_Services('ReadDataRow', 'WO_SCHEDULE', WorkOrderNo, True$, 15)
            SchedDetKeyIDs  = WOScheduleRow<WO_SCHEDULE_SCHED_DET_KEY_IDS$>
            SchedDetKeyIDs  = SRP_Array('SortRows', SchedDetKeyIDs, 'AR2', 'LIST', @VM, '*')
            // Get the first SCHED_DET Key ID. Use this to get a schedule object, which will also
            // contain the total scheduled days.
            TotalScheduledDays  = 0
            Date                = Date()
            If SchedDetKeyIDs NE '' then
                FirstSchedDetKeyID  = SchedDetKeyIDs<0, 1>
                For Each SchedDetKeyID in SchedDetKeyIDs using @VM
                    ReactorNo           = SchedDetKeyID[1, '*']
                    ScheduleDate        = SchedDetKeyID[Col2() + 1, '*']
                    Sequence            = SchedDetKeyID[Col2() + 1, '*']
                    // If the schedule date for the event is on the same date being referenced or later,
                    // consider this to be a future event and add its scheduled time to the total scheduled
                    // days. This will be used to determine if any adjustment is necessary.
                    If ScheduleDate GE Date then
                        ScheduleEvent       = Scheduling_Services('GetScheduleEvent', ReactorNo, ScheduleDate, Sequence, True$)
                        GoSub ParseScheduleEvent
                        If DayLengthCode EQ '' then DayLengthCode = 4
                        TotalScheduledDays += DayLengthCode * .25
                    end
                Next SchedDetKeyID
            end else
                // This work order has not yet ben scheduled. The adjusted days should indicate the actual number
                // of days needed to process this order from beginning to end.
                WorkOrder           = Work_Order_Services('GetWorkOrder', WorkOrderNo, True$)
                GoSub ParseWorkOrder
                FirstScheduleDate   = ''
            end
            // Make sure the Reactor Type assigned to this work order is formatted to the newer type.
            Begin Case
                Case WOReactorType _EQC 'ASM+'  ; WOReactorType = 'ASM+'
                Case WOReactorType _EQC 'EPP'   ; WOReactorType = 'EpiPro'
                Case WOReactorType _EQC 'EPS'   ; WOReactorType = 'ASM'
                Case WOReactorType _EQC 'HTR'   ; WOReactorType = 'HTR'
                Case WOReactorType _EQC 'GAN'   ; WOReactorType = 'GaN'
                Case WOReactorType _EQC ''	    ; WOReactorType = '***'
            End Case
            // Get the wafers per day that can be processed. This takes into consideration the current reactor
            // utilization value. Thus, this is a realistic number rather than an ideal number.
            WafersPerDay    = Epi_Part_Services('GetAdjustedWafersPerDayScheduler', EpiPartNo, WOReactorType)
            If Error_Services('NoError') then
                DaysNeeded      = SRP_Math('ROUND', (WafersRemaining / WafersPerDay) * 4, '', 0) / 4
                AdjustedDays    = DaysNeeded - TotalScheduledDays
                If (WafersRemaining EQ 0) OR ((AdjustedDays LT 0) AND (FirstScheduleDate EQ Date)) then
                    // This implies that the work order being analyzed is already finished. However, this is
                    // due to the current work order event finishing the job so this specific event needs to
                    // be kept in the list. The adjusted days counter needs to be increased by 1 to allow
                    // this to happen.
                    AdjustedDays += 1
                end
                Memory_Services('SetValue', ServiceKeyID, AdjustedDays)
            end
        end else
            Error_Services('Add', 'WorkOrderNo argument was missing from the ' : Service : ' service.')
        end
    end

    Response    = AdjustedDays

end service


//----------------------------------------------------------------------------------------------------------------------
// AutoScheduler
//
// Analyzes all work orders events for the current date for all reactors. Determines the number of days required for
// each work order to complete the remaining number of wafers. Each work order is adjusted automatically based on the
// adjuted number of days calculated.
//----------------------------------------------------------------------------------------------------------------------
Service AutoScheduler()
SRP_Stopwatch('Reset')
SRP_Stopwatch('Start', Service)
    Log             = ''
    Reactors        = Reactor_Services('GetReactors')
    Reactors        = Reactors[1, @RM]  ; // Remove the meta data from the end.
    Reactors        = SRP_Array('Rotate', Reactors, @FM, @VM)
    ReactorNos      = Reactors<3>

    For Each ReactorNo in ReactorNos using @VM
        ScheduleEvents  = Scheduling_Services('GetScheduleEvents', Date(), '', ReactorNo, '', False$)
        WorkOrderNos    = SRP_Array('Rotate', ScheduleEvents, @FM, @VM)
        WorkOrderNos    = WorkOrderNos<2>
        Convert @VM to @FM in WorkOrderNos
        Convert '*' to @VM in WorkOrderNos
        WorkOrderNos    = SRP_Array('Rotate', WorkOrderNos, @FM, @VM)
        WorkOrderNos    = WorkOrderNos<1>
        WorkOrderNos    = SRP_Array('Clean', WorkOrderNos, 'TrimAndMakeUnique', @VM)
        If WorkOrderNos NE '' then
            For Each WorkOrderNo in WorkOrderNos using @VM
                // Determine how many days has this Work order has been scheduled for starting from today
                AdjustedDays    = Scheduling_Services('GetAdjustedDays', WorkOrderNo)
                Log    := 'Reactor: ' : ReactorNo : ' - WorkOrder: ' : WorkOrderNo : ' - Number of days: ' : AdjustedDays : @FM
                Log    := ReactorNo : ',' : WorkOrderNo : ',' : AdjustedDays : @FM
                If Error_Services('NoError') then
                    If AdjustedDays NE '' AND AdjustedDays NE 0 then
                        AdjustedEvents  = Scheduling_Services('AdjustScheduleEvents', ReactorNo, WorkOrderNo, AdjustedDays, Date())
                        If Error_Services('NoError') then
                            Log    := AdjustedEvents : @FM
                        end else
                            Log    := Error_Services('GetMessage') : @FM
                        end
                    end
                end else
                    Message     = Error_Services('GetMessage')
                    Message    := ' - Reactor No: ' : ReactorNo
                    Error_Services('Set', Message)
                    Log    := Error_Services('GetMessage') : @FM
                end
            Next WorkOrderNo
        end
    Next ReactorNo
    
SRP_Stopwatch('Stop', Service)
If @UserName EQ 'DANIEL_ST' then
    Open 'SYSLISTS' to hsyslists then
        Log = SRP_Array('SortRows', Log, 'AL1', 'LIST', @FM, @VM)
        Write Log to hSysLists, 'SCHEDULERENGINE' else Debug
    end
    SRP_Stopwatch('ShowAll')
end
end service


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//----------------------------------------------------------------------------------------------------------------------
// AdjustOtherWorkOrderEvents
//
// Logic dedicated to adjusting all other work orders for a specific reactor. This is called by the AdjustScheduleEvents
// service.
//----------------------------------------------------------------------------------------------------------------------
AdjustOtherWorkOrderEvents:

    PreviousKeys    = ''    ; // List of previous schedule detail keys that should be referenced later on.
    If PreviousEvents NE '' then
        // This means one or more work orders has been identified as already being scheduled in the past. All events
        // related to them shoud be removed from the OtherKeys and OtherEvents variables. This will prevent them from being
        // adjusted.
        For Each PreviousEvent in PreviousEvents using @FM
            PreviousSchedulerKeyID  = PreviousEvent<0, 2>
            PreviousWorkOrder       = PreviousSchedulerKeyID[1, '*']
            NumOtherEvents          = DCount(OtherEvents, @FM)
            For OtherEventCnt = NumOtherEvents to 1 Step -1
                OtherEvent          = OtherEvents<OtherEventCnt>
                OtherSchedulerKeyID = OtherEvent<0, 2>
                OtherWorkOrder      = OtherSchedulerKeyID[1, '*']
                If OtherWorkOrder EQ PreviousWorkOrder then
                    PreviousKeys   := OtherKeys<OtherEventCnt> : @FM
                    OtherEvents     = Delete(OtherEvents, OtherEventCnt, 0, 0)
                    OtherKeys       = Delete(OtherKeys, OtherEventCnt, 0, 0)
                end
            Next OtherEventCnt
        Next PreviousEvent
        PreviousKeys[-1, 1] = ''
    end

    // Move the work order events and keys into the "All" work order event and key lists. This will be the master list
    // that will be adjusted and eventually returned by this service.
    Transfer WorkOrderKeys to AllWorkOrderKeys
    Transfer WorkOrderEvents to AllWorkOrderEvents

    // If there were previous events that were removed from the Other events and keys, there might not be any
    // other events that need to be adjuted. Check to see if there are other events before proceeding.
    If OtherEvents NE '' then
        If AllWorkOrderKeys NE '' then
            AllWorkOrderKeys   := @FM : OtherKeys
            AllWorkOrderEvents := @FM : OtherEvents
        end else
            AllWorkOrderKeys    = OtherKeys
            AllWorkOrderEvents  = OtherEvents
        end
        AllWorkOrderKeys    = SRP_Array('SortRows', AllWorkOrderKeys, 'AR2', 'LIST', @FM, '*')
        AllWorkOrderKeys    = SRP_Array('Clean', AllWorkOrderKeys, 'TrimAndMakeUnique', @FM)
        AllWorkOrderEvents  = SRP_Array('SortRows', AllWorkOrderEvents, 'AR3', 'LIST', @FM, @VM)
        AllWorkOrderEvents  = SRP_Array('Clean', AllWorkOrderEvents, 'TrimAndMakeUnique', @FM)

        // The original OtherEvents and OtherKeys lists were sorted by reactor number and then by
        // work order in order to keep the like work orders together. However, these now need to be
        // resorted in strict chronological order in order to maintain priority of work orders that
        // are scheduled in the future. Otherwise, work orders intended to be scheduled further
        // out in the calendar might get adjusted to occur prior to a higher priority work
        // order.
        OtherEvents     = SRP_Array('SortRows', OtherEvents, 'AR1' : @FM : 'AR3', 'LIST', @FM, @VM)
        MatchKeys       = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        MatchKeys       = MatchKeys<2>
        Convert @VM to @FM in MatchKeys
        Convert '*' to @VM in MatchKeys
        // These keys are sorted by work order and then by date. This puts the oldest date first
        // so priorty can be established.
        MatchKeys       = SRP_Array('SortRows', MatchKeys, 'AR1' : @FM : 'AR4', 'LIST', @FM, @VM)
        MatchKeys       = SRP_Array('Rotate', MatchKeys, @FM, @VM)
        NewOtherEvents  = ''
        // Create a new list of other events but pre-pend the oldest date related to each work order.
        // This will create a temporary priority sorting column.
        If OtherEvents NE '' then
            For Each OtherEvent in OtherEvents using @FM
                SchedulerKeyID  = OtherEvent<0, 2>
                WorkOrderNo     = SchedulerKeyID[1, '*']
                Locate WorkOrderNo in MatchKeys<1> using @VM setting vPos then
                    FirstDate   = MatchKeys<4, vPos>
                    NewOtherEvents := FirstDate : @VM : OtherEvent : @FM
                end
            Next OtherEvent
            NewOtherEvents[-1, 1]   = ''
            Transfer NewOtherEvents to OtherEvents
        end
        // Now sort the other events by the temporary column.
        OtherEvents     = SRP_Array('SortRows', OtherEvents, 'AR1', 'LIST', @FM, @VM)
        OtherEvents     = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        // Remove the temporary column and restore other events to its normal format.
        OtherEvents     = Delete(OtherEvents, 1, 0, 0)
        OtherEvents     = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        // Since other events has been sorted, the other keys needs to be rebuilt.
        OtherKeys       = SRP_Array('Rotate', OtherEvents, @FM, @VM)
        OtherKeys       = OtherKeys<2>
        Convert @VM to @FM in OtherKeys
        Convert '*' to @VM in OtherKeys
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the work orders
        OtherKeys       = Delete(OtherKeys, 1, 0, 0)   ; // Remove the EPI Part
        OtherKeys       = SRP_Array('Rotate', OtherKeys, @FM, @VM)
        Convert @VM to '*' in OtherKeys

        // Relocate the pre-existing work order events.
        PreviousScheduleDate    = ''
        PreviousWorkOrder       = ''
        Loop
        Until (OtherKeys EQ '')
            Locate ScheduleDate in BlockOutDates using @VM setting vPos then
                // Do not schedule on the same date as a block out event.
            end else
                // Get the next schedule key that needs to be adjusted. It will always be the first
                // item in the list of keys.
                OtherKey        = OtherKeys<1>
                // Look for this schedule in the combined list of all work order keys (which should
                // always be found). Then use the index position to extract the schedule detail information.
                // Remove the schedule key and event from the master lists so they don't get processed again.
                Locate OtherKey in AllWorkOrderKeys using @FM setting fPos then
                    OtherEvent          = AllWorkOrderEvents<fPos>
                    AllWorkOrderKeys    = Delete(AllWorkOrderKeys, fPos, 0, 0)
                    AllWorkOrderEvents  = Delete(AllWorkOrderEvents, fPos, 0, 0)
                end else
                    // This condition should never occur.
                    OtherEvent          = ''
                end
                
                // Get the current work order for the event being adjusted. Compare it to the previous
                // work order already adjusted (if applicable). If the work order is different, then use
                // don't advance the schedule date. Just increase the sequence so it can be added to the
                // same date. Otherwise, advance the schedule date and reset the sequence to 1.
                CurrentWorkOrder    = OtherEvent<0, 2>[1, '*']
                OtherKeys           = Delete(OtherKeys, 1, 0, 0)
                OtherEvents         = Delete(OtherEvents, 1, 0, 0)
                Begin Case
                    Case PreviousWorkOrder EQ ''
                        // Use the current schedule date as already assigned above.
                        Sequence        = 1
                    Case CurrentWorkOrder EQ PreviousWorkOrder
                        // Use the newly advanced schedule date. Sequence to 1.
                        Sequence        = 1
                    Case CurrentWorkOrder NE PreviousWorkOrder
                        // Current work order is different from previous work order.
                        // Use the previous schedule date but increase the sequence.
                        ScheduleDate    = PreviousScheduleDate
                        Sequence       += 1
                End Case

                // Using the starting values from above, look for the next available schedule key that
                // the current event can use.
                KeyFound    = False$    ; // Assume key not found for now.
                Loop
                    // Get the next "new" schedule detail key that is available.
                    NewScheduleKeyID    = ReactorNo : '*' : ScheduleDate : '*' : Sequence
                    Locate NewScheduleKeyID in PreviousKeys : @FM : AllWorkOrderKeys using @FM setting fPos then
                        // The new schedule key already exists. Just increase the sequence counter to create
                        // another new schedule key to compare with.
                        Sequence   += 1
                    end else
                        // We now have a "new" schedule detail key. Update the database using the new
                        // schedule detail key and then delete the original key.
                        KeyFound            = True$
                        
                        // The event detail contains information from the original schedule date. This needs to be
                        // adjusted to reflect the new schedule date.
                        OrigOtherEvent      = OtherEvent    ; // Used to append to the RemoveEventKeyIDs list below.
                        SchedulerKey        = OtherEvent<0, 2>
                        SchedulerKey        = Field(SchedulerKey, '*', 1, 2) : '*' : NewScheduleKeyID
                        OtherEvent<0, 2>    = SchedulerKey
                        OtherEvent<0, 3>    = ScheduleDate : '.0'
                        OtherEvent<0, 4>    = ScheduleDate + 1 : '.0'

                        // For reporting purposes, add the updated event to the list of all work order events.
                        AllWorkOrderKeys   := @FM : NewScheduleKeyID
                        AllWorkOrderEvents := @FM : OtherEvent

                        // OtherKey is the original key being adjusted. Delete it first before adding the
                        // new scheduler key.
*                        ScheduleDetRow  = Scheduling_Services('GetScheduleDetail', OtherKey)
                        ScheduleDetRow  = Database_Services('ReadDataRow', 'SCHED_DET', OtherKey)
                        If Error_Services('NoError') then
                            RemoveEventKeyIDs  := OrigOtherEvent<0, 2> : @FM
                            Scheduling_Services('DeleteScheduleDetail', OtherKey)
                            If Error_Services('NoError') then
                                AddEvents  := OtherEvent : @FM
                                Scheduling_Services('SetScheduleDetail', NewScheduleKeyID, ScheduleDetRow)
                            end
                        end
                    end
                Until KeyFound
                Repeat
                // Store the current schedule date in the previous schedule date variable so it can be used to compare
                // with in the next loop.
                PreviousScheduleDate    = ScheduleDate
                // Store the current work order in the previous work order variable so it can be used to compare with
                // in the next loop.
                PreviousWorkOrder       = CurrentWorkOrder                        
            end
            // Advanced to the next schedule date whether a new schedule event was created or if a block out date was encountered.
            ScheduleDate   += 1
        Repeat
    end
       
return


ParseScheduleEvent:

    If Assigned(objScheduleEvent) else objScheduleEvent = 0
    If objScheduleEvent LE 0 then
        SRP_JSON(objScheduleEvent, 'PARSE', ScheduleEvent)
    end
    If objScheduleEvent GT 0 then
        ReactorNo           = SRP_JSON(objScheduleEvent, 'GETVALUE', 'Reactor.ReactorNumber')
        ReactorType         = SRP_JSON(objScheduleEvent, 'GETVALUE', 'Reactor.Type')
        SusceptorSize       = SRP_JSON(objScheduleEvent, 'GETVALUE', 'Reactor.SusceptorSize')
        ScheduleDate        = SRP_JSON(objScheduleEvent, 'GETVALUE', 'ScheduleDate')
        ScheduleDate        = Iconv(ScheduleDate, 'D')
        Sequence            = SRP_JSON(objScheduleEvent, 'GETVALUE', 'Sequence')
        BackColor           = SRP_JSON(objScheduleEvent, 'GETVALUE', 'BackColor')
        ForeColor           = SRP_JSON(objScheduleEvent, 'GETVALUE', 'ForeColor')
        ModifiedDTM         = Iconv(SRP_JSON(objScheduleEvent, 'GETVALUE', 'ModifiedDTM'), 'DT')
        BlockOut            = Iconv(SRP_JSON(objScheduleEvent, 'GETVALUE', 'BlockOut'), 'BYes,No')
        BlockOutType        = SRP_JSON(objScheduleEvent, 'GETVALUE', 'BlockOutType')
        Description         = SRP_JSON(objScheduleEvent, 'GETVALUE', 'Note')
        DayLengthCode       = SRP_JSON(objScheduleEvent, 'GETVALUE', 'DayLengthCode')
        StartTime           = Iconv(SRP_JSON(objScheduleEvent, 'GETVALUE', 'StartTime'), 'MT')
        EndTime             = Iconv(SRP_JSON(objScheduleEvent, 'GETVALUE', 'EndTime'), 'MT')
        If EndTime EQ MIDNIGHT_AM$ then EndTime = MIDNIGHT_PM$
        objWorkOrder        = SRP_JSON(objScheduleEvent, 'GET', 'WorkOrder')
        GoSub ParseWorkOrder
        If BackColor EQ '' then
            Begin Case
                Case BackColor NE ''
                    // Set in the schedule detail record. Use current color.
                Case HotLot
                    BackColor   = 'LightCoral'
                Case Closed
                    BackColor   = 'LightGray'
                Case BlockOut
                    BackColor   = 'Plum'
                Case Otherwise$
                    BackColor   = 'LightSteelBlue'
            End Case
        end
        Current = False$    ; // Assume false for now.
        If (ScheduleDate GE Date()) then
            If (ModifiedDTM NE '') then
                CurrentDTM  = Iconv(Oconv(Date(), 'D4/') : ' ' : Oconv(Time(), 'MTH'), 'DTM')
                ElapseTime  = (CurrentDTM - ModifiedDTM) * 24
                IF (ElapseTime LE 12) then
                    CurrentFlag = True$
                end
            end
        end
        If ForeColor EQ '' then ForeColor = 'Black'
        FirstSchedDetKeyID  = SRP_JSON(objScheduleEvent, 'GETVALUE', 'FirstSchedDetKeyID')
        FirstReactorNo      = FirstSchedDetKeyID[1, '*']
        FirstScheduleDate   = Oconv(FirstSchedDetKeyID[Col2() + 1, '*'], 'D4/')
        LastSchedDetKeyID   = SRP_JSON(objScheduleEvent, 'GETVALUE', 'LastSchedDetKeyID')
        LastReactorNo       = LastSchedDetKeyID[1, '*']
        LastScheduleDate    = Oconv(LastSchedDetKeyID[Col2() + 1, '*'], 'D4/')
        TotalScheduledDays  = SRP_JSON(objScheduleEvent, 'GETVALUE', 'TotalScheduledDays')
        ScheduleEvent       = SRP_JSON(objScheduleEvent, 'STRINGIFY', 'FAST')
        SRP_JSON(objScheduleEvent, 'RELEASE')
    end else
        
    end

return


ParseWorkOrder:

    If Assigned(objWorkOrder) else objWorkOrder = 0
    If objWorkOrder LE 0 then
        SRP_JSON(objWorkOrder, 'PARSE', WorkOrder)
    end
    If objWorkOrder GT 0 then
        WorkOrderNo         = SRP_JSON(objWorkOrder, 'GETVALUE', 'WorkOrderNumber')
        EpiPartNo           = SRP_JSON(objWorkOrder, 'GETVALUE', 'EpiPartNumber')
        WOReactorType       = SRP_JSON(objWorkOrder, 'GETVALUE', 'ReactorType')
        PSN                 = SRP_JSON(objWorkOrder, 'GETVALUE', 'PSN')
        Recipe              = SRP_JSON(objWorkOrder, 'GETVALUE', 'Recipe')
        HotLot              = Iconv(SRP_JSON(objWorkOrder, 'GETVALUE', 'HotLot'), 'BYes,No')
        Closed              = Iconv(SRP_JSON(objWorkOrder, 'GETVALUE', 'Closed'), 'BYes,No')
        TotalWafers         = SRP_JSON(objWorkOrder, 'GETVALUE', 'TotalWafers')
        WafersRemaining     = SRP_JSON(objWorkOrder, 'GETVALUE', 'WafersRemaining')
        PercentComplete     = SRP_JSON(objWorkOrder, 'GETVALUE', 'PercentComplete')
        CustNameShort       = SRP_JSON(objWorkOrder, 'GETVALUE', 'Company.NameShort')
        SRP_JSON(objWorkOrder, 'RELEASE')
    end else
        
    end

return

GetLastEndTime:

    EndTimes    = ''
    For Each SchedDetKeyID in SchedDetKeyIDs using @VM
        SchedDetRow     = Database_Services('ReadDataRow', 'SCHED_DET', SchedDetKeyID)
        EndTimes       := SchedDetRow<SCHED_DET_END_TIME$> : @FM
    Next SchedDetKeyID
    EndTimes[-1, 1] = ''
    // Sort the list of end times so the latest time is at the top.
    EndTimes        = SRP_Array('SortRows', EndTimes, 'DR1', 'ARRAY', @FM, @VM)
    LastEndTime     = EndTimes<1>

end

