Substitute Material Customization Sample

This sample customization will automatically replace material shortage with available substitute materials

Customization requirements:

  1. UDF on operations named the ExternalId of the primary material. The (string) value of the UDF should contain a comma delineated list of alternate material ExternalIds.

Behavior:

  1. After Optimize, the customization will determine any material shortages.
  2. For any shortage, it will look for substitute material UDFs on the operation. If there are any substitutes, ATP will be run for each substitute and only a substitute material that will not delay another operation will be considered.
  3. If any substitute materials qualify, one will be chosen to replace the primary requirement that has a shortage. The material requirement Item will be changed to the substitute.

How that can be expanded:

  • Additional logic could be added for behavior step 2 to determine if delaying another operation in the future is acceptable.
  • Additional logic could be added for behavior step 3 to decide which substitute to choose if multiple qualify.
  • If using constraining materials, a second optimize can be performed automatically by the customization to reschedule and remove material delays fixed by the substitutes.

Customization Points:

The EndOfSimulationCustomization triggers the SubstituteMaterials feature.

Code:

This is an example customization class that implements the behavior described above.

using System.Collections.Generic;

using PT.ERPTransmissions;
using PT.Scheduler;
using PT.SchedulerDefinitions;
using PT.Transmissions;

namespace PlanetTogether.PlanetTogether.SubsituteMaterials
{
public class PostSimStageCust : PT.Scheduler.Simulation.Customizations.EndOfSimulationCustomization
{
public override string Name
{
get { return "Substitute Materials"; }
}

public override string Description
{
get { return "This add-in will analyze material shortages and change the material requirement to use a substitute material."; }
}

protected override void SimulationInitialization(ScenarioDetail a_sd, ScenarioDetail.SimulationType a_simulationType, ScenarioBaseT a_transmission)
{
CleanTransmissionsProcess();
base.SimulationInitialization(a_sd, a_simulationType, a_transmission);
}

protected override void EndOfSimulationCleanup(ScenarioDetail.SimulationType a_simType, ScenarioBaseT a_t, ScenarioDetail a_sd)
{
if (a_simType != ScenarioDetail.SimulationType.Optimize) return;

//Determinme which operations have a constraint and swap their material requirement
JobDataSet dataSet = new JobDataSet();
JobDataSetFiller filler = new JobDataSetFiller();

foreach (Job job in a_sd.JobManager)
{
if (!job.Template)
{
Dictionary<string, string> materialSubstituteMappings = new Dictionary<string, string>();
foreach (ResourceOperation operation in job.GetOperations())
{
foreach (MaterialRequirement mr in operation.MaterialRequirements)
{
if (mr == null || mr.BuyDirect || mr.Warehouse == null) continue;
for (int i = 0; i < mr.SupplySourcesCount; i++)
{
if (mr.GetSupplySource(i).Shortage)
{
//Check for alternate materils
UserField userField = operation.UserFields?.Find(mr.Item.Name);
if (userField != null && userField.DataValue is string alternates)
{
string[] split = alternates.Split(':');
foreach (string substituteMaterial in split)
{
foreach (InventoryManager.ItemInventory itemInventory in mr.Warehouse.Inventories)
{
if (itemInventory.Inventory.Item.Name == substituteMaterial)
{
decimal qty = itemInventory.Inventory.GetAtpQty(a_sd.GetPlanningHorizonEnd());
if (qty >= mr.TotalRequiredQty)
{
//This material can be used a substitute
materialSubstituteMappings.Add(mr.ExternalId, itemInventory.Inventory.Item.ExternalId);
}

break;
}
}
}
}
}
}
}
}

if (materialSubstituteMappings.Count > 0)
{
//Add the job to the dataset
filler.FillDataSet(dataSet, job, a_sd.JobManager);
//Replace the material requirement
foreach (JobDataSet.ResourceOperationRow opRow in dataSet.ResourceOperation)
{
foreach (JobDataSet.MaterialRequirementRow mrRow in opRow.GetMaterialRequirementRows())
{
if (materialSubstituteMappings.TryGetValue(mrRow.ExternalId, out string replacementMaterialExternalId))
{
mrRow.ItemExternalId = replacementMaterialExternalId;
}
}
}
}
}
}

if (dataSet.Job.Rows.Count > 0)
{
//There was at least one job update
ApplicationExceptionList errs = new ApplicationExceptionList();
JobT t = new JobT();
t.Fill(ref errs, dataSet, false);
AddTransmissionsToProcess(0, t);

//Trigger another optimize
//ScenarioDetailOptimizeT optimizeT = new ScenarioDetailOptimizeT(a_sd.Scenario.Id, true, false);
//AddTransmissionsToProcess(1, optimizeT);
}
}
}
}

Customization File:

Compatible with versions 11.36.1 and later: File download