330 lines
18 KiB
C#
330 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Gebhardt.Shared;
|
|
using Gebhardt.Shared.Process;
|
|
using Gebhardt.StoreWare.Wcs.Common;
|
|
using Gebhardt.StoreWare.Wcs.Common.Application.LeHandling.Interfaces;
|
|
using Gebhardt.StoreWare.Wcs.Common.Application.StorageHandling.Interfaces;
|
|
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
|
|
using Gebhardt.StoreWare.Wcs.Common.Dao;
|
|
using Gebhardt.StoreWare.Wcs.Common.DbAccess;
|
|
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
|
|
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
|
|
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Queries;
|
|
using static Gebhardt.StoreWare.Wcs.Common.Constants;
|
|
|
|
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
|
|
{
|
|
public class ToEmptyLeBuffer : ProcessWorker
|
|
{
|
|
private record AisleUtilization(AisleForLe Aisle, int Utilization);
|
|
|
|
private readonly IWcsDbContextFactory _dbContextFactory;
|
|
|
|
private readonly IDestinationService _destinationService;
|
|
private readonly ILeService _leService;
|
|
private readonly ITransportOrderService _transportOrderService;
|
|
|
|
public ToEmptyLeBuffer(int workInterval,
|
|
IDestinationService destinationService,
|
|
ILeService leService,
|
|
ITransportOrderService transportOrderService,
|
|
IAisleService aisleService,
|
|
IWcsDbContextFactory dbContextFactory)
|
|
: base(nameof(ToEmptyLeBuffer), workInterval, true)
|
|
{
|
|
_destinationService = destinationService;
|
|
_leService = leService;
|
|
_transportOrderService = transportOrderService;
|
|
_dbContextFactory = dbContextFactory;
|
|
}
|
|
|
|
public override bool DoWork()
|
|
{
|
|
try
|
|
{
|
|
using IWcsDbContext db = _dbContextFactory.GetDbContext();
|
|
//Retrieve aisles suited for storage (i.e. lifter/handover aisles are excluded)
|
|
List<AisleForLe> availableAisles = db.Aisle.GetAislesWithStorageCompartmentsReadyForRetrieval();
|
|
availableAisles = FilterForAllowedAisles(availableAisles);
|
|
|
|
SupplyBuffer(LeTypeName.MiniloadSmall, Constants.MfcAllDestinations.EMB_S, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL01);
|
|
SupplyBuffer(LeTypeName.MiniloadBig, Constants.MfcAllDestinations.EMB_M, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL01);
|
|
SupplyBuffer(LeTypeName.MiniloadSmall, Constants.MfcAllDestinationsOldSystem.ETXBU_S, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL02);
|
|
SupplyBuffer(LeTypeName.MiniloadBig, Constants.MfcAllDestinationsOldSystem.ETXBU_M, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL02);
|
|
|
|
return true;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.WriteException(exception);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refills each buffer with empty LEs depending on its demand.
|
|
/// First, it is checked if empty LEs on conveyer can be rerouted.
|
|
/// Second, empty LE demand is satisfied by retrieval from storage.
|
|
/// Last, empty LEs that have been explicitly requested by the WMS are retrieved.
|
|
/// </summary>
|
|
/// <param name="leType">the type of le to use</param>
|
|
/// <param name="buffers">List of buffers to refill.</param>
|
|
/// <param name="availableAisles">
|
|
/// List of available aisles (aisle, output place, storage device in aisle (if any) are
|
|
/// ready)
|
|
/// </param>
|
|
private void SupplyBuffer(LeTypeName leType, string buffer, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
|
|
{
|
|
if (string.IsNullOrEmpty(buffer))
|
|
{
|
|
Log.Write(LogLevel.Error, $"No buffers provided for {nameof(SupplyBuffer)}");
|
|
return;
|
|
}
|
|
using IWcsDbContext db = _dbContextFactory.GetDbContext();
|
|
|
|
var destinationBuffer = db.ResourceSetting.GetResourceByName(buffer);
|
|
double destinationBufferDemand = destinationBuffer.Demand;
|
|
double destinationBufferCapacity = destinationBuffer.Capacity + destinationBuffer.Overload;
|
|
double bufferFreePercentage = destinationBufferDemand / destinationBufferCapacity * 100.0;
|
|
|
|
//Setting to minimize crane usage
|
|
SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.EmptyBoxesDemandThreshold, out int demandThreshold, 40);
|
|
|
|
if (destinationBufferDemand > 0)
|
|
{
|
|
RerouteLesOnConveyor(leType, buffer);
|
|
}
|
|
|
|
//If the buffers free space percentage is greater than the threshold settings value we also take boxes from storage
|
|
if (bufferFreePercentage > demandThreshold)
|
|
{
|
|
RefillBuffersFromStorage(leType, buffer, availableAisles, preferredStorageArea);
|
|
}
|
|
RefillBuffersWithRequestedLes(buffer, availableAisles, preferredStorageArea);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refills buffers with demand by rerouting LEs on conveyor that have a storage area as destination
|
|
/// (i.e. aisle selection has not been performed yet.
|
|
/// </summary>
|
|
private void RerouteLesOnConveyor(LeTypeName leType, string buffers)
|
|
{
|
|
using IWcsDbContext db = _dbContextFactory.GetDbContext();
|
|
List<string> destinationsAllowingForRerouting = _destinationService.Where(d => d.IsStorageArea).Select(d => d.Name).ToList();
|
|
List<string> dontAllowforReroutingLastWhere = new List<string>() { MfcAllDestinationsOldSystem.IPT01, MfcAllDestinationsOldSystem.IPT02, MfcAllDestinationsOldSystem.IPT03, MfcAllDestinationsOldSystem.TOPUP, MfcAllDestinationsOldSystem.REP01, MfcAllDestinationsOldSystem.REP02, MfcAllDestinationsOldSystem.REP03, MfcAllDestinationsOldSystem.ETXBU_M, MfcAllDestinationsOldSystem.ETXBU_S };
|
|
|
|
//Transport orders with a storage area as destination
|
|
OrderList orders = new(db.OrdersHost
|
|
.ByDestination(destinationsAllowingForRerouting)
|
|
.ByType(TransportOrderType.Transport)
|
|
.ByStatus(TransportOrderStatus.Pending, TransportOrderStatus.InProgress)
|
|
.Where(o => o.Le.IsEmpty
|
|
&& (o.Le.Status == LeStatus.OnConveyor || o.Le.Status == LeStatus.InDestinationZone)
|
|
&& o.Le.Type == leType)
|
|
.AsEnumerable()
|
|
.Where(o => !o.Le.HasError() && !dontAllowforReroutingLastWhere.Contains(o.Le.LastWhere))
|
|
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag))
|
|
.ToList());
|
|
|
|
foreach (OrderListItem order in orders)
|
|
{
|
|
Resource destination = db.ResourceSetting.ByName(buffers).GetResourceWithHighestDemand();
|
|
if (destination is { Demand: > 0 })
|
|
{
|
|
if (db.OrdersMiniload.ByLeNo(order.Le.LeNo).Open().Any())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//Reroute LE
|
|
Log.Write(LogLevel.Debug, $"LE {order.Le.LeNo} will be rerouted to {destination.Name}.");
|
|
_leService.CancelOpenTransports(order.Le.LeNo, $"Rerouted to {destination.Name}");
|
|
Log.Write(LogLevel.Debug, $"LE {order.Le.LeNo} will be rerouted to {destination.Name}.");
|
|
|
|
// This is to mitigate filling a very long string into the source field. Having AKL01 for the old system is not a problem.
|
|
string source;
|
|
if (order.Le.LastWhere == null || order.Le.LastWhere.StartsWith("["))
|
|
{
|
|
source = Constants.MfcAllDestinations.AKL01;
|
|
}
|
|
else
|
|
{
|
|
source = order.Le.LastWhere;
|
|
}
|
|
int id = _leService.CreateTransport(order.Le.LeNo, source, destination.Name);
|
|
_transportOrderService.StartNextTransport(id, nameof(RerouteLesOnConveyor));
|
|
}
|
|
else if (order.Status == TransportOrderStatus.Pending)
|
|
{
|
|
_transportOrderService.StartNextTransport(order.OrdersHostId, info: nameof(RerouteLesOnConveyor));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refills one or more buffers with empty LEs from the storage.
|
|
/// First, Overload for each buffer is checked and updated via the SettingsManager
|
|
/// Then, for each buffer with demand, an aisle is selected and a retrieval order is created.
|
|
/// </summary>
|
|
private void RefillBuffersFromStorage(LeTypeName leType, string buffers, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
|
|
{
|
|
using IWcsDbContext db = _dbContextFactory.GetDbContext();
|
|
List<Resource> buffersWithDemand = db.ResourceSetting.ByName(buffers).GetResources().WithDemand().OrderByDescending(r => r.Demand).ToList();
|
|
foreach (Resource buffer in buffersWithDemand)
|
|
{
|
|
if (buffer.Demand > 0)
|
|
{
|
|
AisleForLe aisle = GetAisleForEmptyLeRetrieval(leType, availableAisles, preferredStorageArea);
|
|
if (aisle != null)
|
|
{
|
|
//Refill buffer
|
|
//OrdersHost has no storage area, i.e. aisle name must be unique by itself when used as source.
|
|
Log.Write(LogLevel.Debug, $"Empty LE of type {leType} will be retrieved from {aisle.AisleName} and sent towards {buffer.Name}.");
|
|
int id = _leService.CreateTransport(LeType.GetNextEmptyLeTypeForActual(leType).ToString(), aisle.AisleName, buffer.Name);
|
|
_transportOrderService.StartNextTransport(id, nameof(RefillBuffersFromStorage));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refills buffers according to a request received from the WMS.
|
|
/// An initial <see cref="OrdersHost" /> has already been created when handling the request.
|
|
/// Here, select aisle, start <see cref="OrdersHost" /> and create <see cref="OrdersMiniload" />.
|
|
/// </summary>
|
|
private void RefillBuffersWithRequestedLes(string buffers, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
|
|
{
|
|
using IWcsDbContext db = _dbContextFactory.GetDbContext();
|
|
//Initial transport orders for empty LEs (requested by the WMS)
|
|
OrderList orders = new(db.OrdersHost
|
|
.ByDestination(buffers)
|
|
.ByStatus(TransportOrderStatus.Initial)
|
|
.ByType(TransportOrderType.Transport)
|
|
.OnlyNextEmpty()
|
|
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag))
|
|
.ToList());
|
|
|
|
foreach (OrderListItem order in orders)
|
|
{
|
|
AisleForLe aisle = GetAisleForEmptyLeRetrieval(LeType.GetActualLeTypeForNextEmpty(order.Le.Type), availableAisles, preferredStorageArea);
|
|
if (aisle != null)
|
|
{
|
|
//OrdersHost has no storage area, i.e. aisle name must be unique by itself when used as source.
|
|
Log.Write(LogLevel.Debug, $"Requested empty LE of type {order.Le.Type} will be retrieved from {aisle.AisleName} and sent towards {order.Destination}.");
|
|
_transportOrderService.ScheduleOrdersHost(order.OrdersHostId, aisle.AisleName, nameof(RefillBuffersWithRequestedLes));
|
|
_transportOrderService.StartNextTransport(order.OrdersHostId, nameof(RefillBuffersWithRequestedLes));
|
|
}
|
|
}
|
|
}
|
|
|
|
private AisleForLe GetAisleForEmptyLeRetrieval(LeTypeName leTypeName, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
|
|
{
|
|
using IWcsDbContext db = _dbContextFactory.GetDbContext();
|
|
|
|
List<AisleForLe> availableForLeRetrieval = availableAisles.ToList();
|
|
|
|
//Left outer join empty LEs with open orders miniload, then exclude LEs with open orders.
|
|
var emptyLesInStorageWithoutOpenOrders = db.Le
|
|
.ByStatus(LeStatus.InStorage)
|
|
.Where(l => !l.Location.IsLocked)
|
|
.ByType(leTypeName)
|
|
.Empty()
|
|
.GroupJoin(db.OrdersHost.Open(),
|
|
le => le.LeNo,
|
|
o => o.LeNo,
|
|
(le, ordersHost) => new { Le = le, OrdersHost = ordersHost })
|
|
.SelectMany(j => j.OrdersHost.DefaultIfEmpty(),
|
|
(l, o) => new { l.Le, OrdersHost = o })
|
|
.Where(j => j.OrdersHost == null)
|
|
.ToList();
|
|
|
|
if (emptyLesInStorageWithoutOpenOrders.Count > 0)
|
|
{
|
|
Log.Write(LogLevel.Info, $"empty Les found: {emptyLesInStorageWithoutOpenOrders.Count}");
|
|
}
|
|
|
|
Dictionary<AisleForLe, int> aislesWithEmptyLes = emptyLesInStorageWithoutOpenOrders.GroupBy(xx => xx.Le.Aisle).ToDictionary(xx => xx.Key, xx => xx.Count());
|
|
Dictionary<AisleForLe, int> aislesWithActiveNextEmptyMiniload = db.OrdersMiniload.Open().ByLeNo(LeType.GetNextEmptyLeTypeForActual(leTypeName).ToString()).ToList()
|
|
.GroupBy(xx => xx.Aisle).ToDictionary(xx => xx.Key, xx => xx.Count());
|
|
|
|
//Leave only aisles containing LEs that can be retrieved
|
|
availableForLeRetrieval.RemoveAll(a => !aislesWithEmptyLes.ContainsKey(a) || (aislesWithEmptyLes.ContainsKey(a) && aislesWithActiveNextEmptyMiniload.ContainsKey(a) && aislesWithEmptyLes[a] - aislesWithActiveNextEmptyMiniload[a] <= 0));
|
|
|
|
// See if we can remove all other storage areas. If not, allow other.
|
|
if (availableForLeRetrieval.Any(xx => xx.StorageArea != preferredStorageArea)
|
|
&& availableAisles.Any(x => x.StorageArea == preferredStorageArea))
|
|
{
|
|
availableForLeRetrieval.RemoveAll(xx => xx.StorageArea != preferredStorageArea);
|
|
}
|
|
|
|
//Build one structre that has all information
|
|
Dictionary<AisleForLe, (int EmptyBoxes, int OrderedEmpties)> aislesWithNumbers = new Dictionary<AisleForLe, (int emptyBoxes, int orders)>();
|
|
foreach (var aisle in availableForLeRetrieval.Distinct())
|
|
{
|
|
int emptyBoxes = 0;
|
|
int orderedEmpties = 0;
|
|
if (aislesWithEmptyLes.ContainsKey(aisle))
|
|
{
|
|
emptyBoxes = aislesWithEmptyLes[aisle];
|
|
}
|
|
if (aislesWithActiveNextEmptyMiniload.ContainsKey(aisle))
|
|
{
|
|
orderedEmpties = aislesWithEmptyLes[aisle];
|
|
}
|
|
|
|
aislesWithNumbers.Add(aisle, (emptyBoxes, orderedEmpties));
|
|
}
|
|
|
|
AisleUtilization currentMinUtilization = null;
|
|
//Check MLS before Crane (as longterm MLS should be used for filled boxes), and we want more space in MLS
|
|
foreach (AisleForLe aisle in aislesWithNumbers.OrderBy(ar => ar.Key.AisleName.StartsWith("C")).ThenByDescending(ar => ar.Value.EmptyBoxes - ar.Value.OrderedEmpties).Select(ar => ar.Key))
|
|
{
|
|
AisleForLe inputAisle = db.AisleForLe.GetInputAisle(aisle);
|
|
int lesTowardsAisle = db.OrdersConveyor.Active().ByDestination(inputAisle.AisleName).Count();
|
|
//TODO include input/output le lifter orders?
|
|
//TODO include OrdersHost for orders that have no ordersMiniload yet?
|
|
int openOrdersForAisle = db.OrdersMiniload.Open().ByAisle(aisle.AisleName, aisle.StorageArea).Count();
|
|
|
|
int totalUtilization = openOrdersForAisle + lesTowardsAisle;
|
|
//We allow a utilization of 4 orders to be neglected, so we put more relevance on the number of totes
|
|
if (currentMinUtilization == null || currentMinUtilization.Utilization > totalUtilization + 4)
|
|
{
|
|
currentMinUtilization = new AisleUtilization(aisle, totalUtilization);
|
|
}
|
|
}
|
|
//Select aisle with lowest utilization
|
|
return currentMinUtilization?.Aisle;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Filter depending on OnlyEmptiesToOldStorage setting:
|
|
/// If setting is true, get all empty LEs from old storage if available
|
|
/// </summary>
|
|
/// <param name="availableAisles"></param>
|
|
/// <param name="le"></param>
|
|
/// <returns></returns>
|
|
private List<AisleForLe> FilterForAllowedAisles(List<AisleForLe> availableAisles)
|
|
{
|
|
SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.OnlyEmptiesToOldStorage, out bool onlyEmptiesToOldStorage, false, true);
|
|
if (!onlyEmptiesToOldStorage)
|
|
{
|
|
return availableAisles;
|
|
}
|
|
//get all empties from AKL02 if possible
|
|
if (availableAisles.Any(a => a.StorageArea == Constants.MfcAllDestinations.AKL02))
|
|
{
|
|
return availableAisles.Where(a => a.StorageArea == Constants.MfcAllDestinations.AKL02).ToList();
|
|
}
|
|
//Fallback get from available aisles
|
|
else
|
|
{
|
|
return availableAisles;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
} |