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 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; } } /// /// 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. /// /// the type of le to use /// List of buffers to refill. /// /// List of available aisles (aisle, output place, storage device in aisle (if any) are /// ready) /// private void SupplyBuffer(LeTypeName leType, string buffer, IReadOnlyList 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); } /// /// 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. /// private void RerouteLesOnConveyor(LeTypeName leType, string buffers) { using IWcsDbContext db = _dbContextFactory.GetDbContext(); List destinationsAllowingForRerouting = _destinationService.Where(d => d.IsStorageArea).Select(d => d.Name).ToList(); List dontAllowforReroutingLastWhere = new List() { 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)) .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)); } } } /// /// 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. /// private void RefillBuffersFromStorage(LeTypeName leType, string buffers, IReadOnlyList availableAisles, string preferredStorageArea) { using IWcsDbContext db = _dbContextFactory.GetDbContext(); List 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)); } } } } /// /// Refills buffers according to a request received from the WMS. /// An initial has already been created when handling the request. /// Here, select aisle, start and create . /// private void RefillBuffersWithRequestedLes(string buffers, IReadOnlyList 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)) .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 availableAisles, string preferredStorageArea) { using IWcsDbContext db = _dbContextFactory.GetDbContext(); List 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 aislesWithEmptyLes = emptyLesInStorageWithoutOpenOrders.GroupBy(xx => xx.Le.Aisle).ToDictionary(xx => xx.Key, xx => xx.Count()); Dictionary 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 aislesWithNumbers = new Dictionary(); 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; } /// /// Filter depending on OnlyEmptiesToOldStorage setting: /// If setting is true, get all empty LEs from old storage if available /// /// /// /// private List FilterForAllowedAisles(List 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; } } } }