refactor: migrate project structure by reorganizing realization code snippets into documentation and analysis categories.

This commit is contained in:
2026-05-27 10:48:45 +02:00
parent eb82e4e0b2
commit 24c0593f15
116 changed files with 3309 additions and 236 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -18,7 +18,7 @@ INFO: Formlose Notizen mit: Code-Stelle, Bedingungen, Prozess/Szenario genügen
## 03 Konzept erstellen
### Status: 🟩 Active
### Status: ⬛ Done
Idee dokumentieren: Wie können die Code Stellen die einen Auftrag starten aus dem HostBooking so umgebaut werden, dass der ConveyorDispo den Start übernimmt. Am besten ins Ablauf-Diagramm aus (01 ConveyorDispo Analysieren) ergänzen.
@@ -26,7 +26,7 @@ Idee dokumentieren: Wie können die Code Stellen die einen Auftrag starten aus d
## 04 Änderung Implementieren
### Status: ⬜ New
### Status: ⬛ Done
Ziel: HostBooking startet keine Aufträge selbst
@@ -36,7 +36,7 @@ HostBooking und ConveyorDispo anpassen.
## 05 Änderung Debuggen/Testen
### Status: ⬜ New
### Status: ⬛ Done
Änderugnen gegen die Emulation Testen Szenario:
- Normale Auslagerung, Kiste steht im Lager
@@ -48,7 +48,7 @@ HostBooking und ConveyorDispo anpassen.
## 06 Dokumentation
### Status: ⬜ New
### Status: 🟩 Active
Die Änderungen sollten mit einem "Warum" im Code per Kommentar dokumentiert sein.
Das Konzept sollte als Ablaufdiagramm dokumentiert sein.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

BIN
02_Analyse_Konzept/Datenbank/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
02_Analyse_Konzept/HostBooking/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

View File

@@ -110,14 +110,14 @@ namespace Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms.MessageImplementati
else
{
//Todo: Wouldn't it be better to do this in StartInitialOrders?
/*var otherPicOrder = db.OrdersHost.ByLeNo(message.LeNo)
var otherPicOrder = db.OrdersHost.ByLeNo(message.LeNo)
.ByStatus(TransportOrderStatus.Initial)
.ApplyWmsOrdering()
.FirstOrDefault();
if (otherPicOrder != null)
{
_transportOrderService.StartNextTransport(otherPicOrder.Id);
}*/
}
//Set source to TOPUP in case Departure has been sent from TopUP or IPT stations, to be able to accept the box on scale
if (message.Position == Constants.MfcAllDestinationsOldSystem.TOPUP || message.Position.StartsWith(Constants.MfcAllDestinationsOldSystem.IPT01.Substring(0, 3)))
{

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

View File

@@ -0,0 +1,493 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.DbAccess.Base;
using Gebhardt.Shared;
using Gebhardt.Shared.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Factories;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
public class OrdersHost : AutoIncrementEntity
{
private Le _le;
private ICollection<OrdersConveyor> _ordersConveyor;
private ICollection<OrdersMiniload> _ordersMiniload;
private ICollection<ResourceList> _resources;
internal OrdersHost(TransportOrderType type, string leNo, string source, string destination, int priority, int? idOrderWms, int? idOrderWmsHead, int? idOrderWmsPos, OrderTypeWms? typeWms)
{
Type = type;
LeNo = leNo;
Source = source;
Destination = destination;
Priority = priority;
IdOrderWms = idOrderWms;
IdOrderWmsHead = idOrderWmsHead;
IdOrderWmsPos = idOrderWmsPos;
TypeWms = typeWms;
OrdersConveyor = new List<OrdersConveyor>();
OrdersMiniload = new List<OrdersMiniload>();
Resources = new List<ResourceList>();
}
private OrdersHost()
{
}
private ILazyLoader LazyLoader { get; set; }
public string LeNo { get; private set; }
public Le Le
{
get => LazyLoader.Load(this, ref _le);
private set => _le = value;
}
public TransportOrderType Type { get; private set; }
public string Source { get; set; }
/// <summary>
/// the (possibly gross) destination for the Le: a storage area, a work place, etc.
/// </summary>
public string Destination { get; set; }
public string HostDestination { get; set; }
public TransportOrderStatus Status { get; private set; } = TransportOrderStatus.Initial;
public int? IdOrderWmsHead { get; private set; }
public int? IdOrderWms { get; private set; }
public int? IdOrderWmsPos { get; private set; }
// Currently not used at ETRA
public int Priority { get; set; }
public DateTime? PriorityDate { get; set; }
public DateTime? StartTime { get; private set; }
public string Info { get; set; }
public OrderTypeWms? TypeWms { get; }
public string Error { get; internal set; }
public bool IsEmptyLeRequest { get; set; }
// Used by sequencer to know what order to send out boxes in.
public int? SequenceWms { get; set; }
public bool? IsDirectPicking { get; set; }
//Used in deadlock situations when the same box is used in multiple Sequencers
public bool? IsStolen { get; set; }
public DateTime? SequencerRetrievalTime { get; set; }
public ICollection<OrdersMiniload> OrdersMiniload
{
get => LazyLoader.Load(this, ref _ordersMiniload);
set => _ordersMiniload = value;
}
public ICollection<OrdersConveyor> OrdersConveyor
{
get => LazyLoader.Load(this, ref _ordersConveyor);
set => _ordersConveyor = value;
}
public ICollection<ResourceList> Resources
{
get => LazyLoader.Load(this, ref _resources);
set => _resources = value;
}
//Todo-Job: I don't like that the order is directly transmitted and no Tord is sent.
public OrdersConveyor StartConveyorOrderToNextDestination(IDestinationProperties newDestination = null, string reasonforredirect = null)
{
if (Status == TransportOrderStatus.InDestinationZone && newDestination is { BuffersEmptyLe: true })
{
throw new InvalidOperationException($"Cannot start {nameof(OrdersConveyor)} to next destination ({newDestination.Name}) as the {nameof(OrdersHost)} has already arrived.");
}
OrdersConveyor openOrder = OrdersConveyor.SingleOrDefault(oc => TransportOrderStatusGroups.Open.Contains(oc.Status));
if (openOrder != null)
{
openOrder.HandleFinished(newDestination);
}
OrdersConveyor followingOrder = OrdersConveyorFactory.GetInstance(this).InitialForOrdersHost(openOrder?.Destination, newDestination?.Name);
followingOrder.Transmit(reasonforredirect);
return followingOrder;
}
public void Redirect(IDestinationProperties newDestination = null, string reasonForRedirect = null)
{
if (newDestination != null && newDestination.Name != Destination)
{
OrdersConveyor openOrder = OrdersConveyor.SingleOrDefault(oc => TransportOrderStatusGroups.Open.Contains(oc.Status));
if (openOrder != null)
{
openOrder.Cancel($"Redirected to {newDestination.Name}");
UpdateResources(TransportOrderStatus.Cancelled, null);
}
Destination = newDestination.Name;
Info = reasonForRedirect;
OrdersConveyor followingOrder = OrdersConveyorFactory.GetInstance(this).InitialForOrdersHost(openOrder?.Destination, newDestination.Name);
followingOrder.Transmit();
}
}
public OrdersHost Reinitialize(string info)
{
Status = TransportOrderStatus.Initial;
Info = info;
StartTime = null;
return this;
}
public OrdersHost Postpone(IDestinationProperties destination, string info)
{
if (!TransportOrderStatusGroups.Waiting.Contains(Status))
{
throw new InvalidOperationException("Only orders that have not started yet can be postponed.");
}
if (!destination.BuffersEmptyLe)
{
Reinitialize(info);
}
else
{
Cancel(string.Empty, info);
}
return this;
}
public OrdersConveyor TransportToNio(string error)
{
Error = error;
OrdersConveyor nioTransport = OrdersConveyor?.FirstOrDefault();
if (nioTransport == null)
{
nioTransport = OrdersConveyorFactory.GetInstance(this).PendingForOrdersHost(error: error);
}
nioTransport.Transmit(error);
return nioTransport;
}
public OrdersHost Schedule(string source = null, string info = null)
{
if (TransportOrderStatusGroups.Waiting.Contains(Status))
{
Status = TransportOrderStatus.Pending;
Info = info;
if (source != null)
{
Source = source;
}
}
else
{
Log.Write(LogLevel.Error, $"cannot schedule due to Status={Status}; {this} ");
}
return this;
}
/// <summary>
/// set the OrdersHost to initial - if it's Status is InProgress or Transmitted
/// </summary>
/// <param name="info">informal reason</param>
/// <returns>true if rescheduled</returns>
public bool Reschedule(string info = null, string source = null)
{
if (Status != TransportOrderStatus.InProgress && Status != TransportOrderStatus.Transmitted)
{
Log.Write(LogLevel.Error, $"cannot reschedule due to Status={Status}; {this} ");
return false;
}
Status = TransportOrderStatus.Initial;
Info = info;
if (source != null) { Source = source; }
Log.Write(LogLevel.Info, $"Rescheduled {this}");
return true;
}
public OrdersHost Start()
{
if (TransportOrderStatusGroups.Waiting.Contains(Status))
{
Status = TransportOrderStatus.InProgress;
StartTime = DBDateTime.Now;
}
return this;
}
public void ManageResources()
{
if (!(Resources ??= new List<ResourceList>()).All(r => r.Status == TransportOrderStatus.Cancelled || r.Status == TransportOrderStatus.Finished) &&
Resources.Count != 0)
{
return;
}
// Is there an old resource that we need to reuse? (Primary key).
ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == Destination);
if (oldResource != null)
{
oldResource.Status = Status;
}
// If not create new.
else
{
Resources.Add(new ResourceList
{
OrdersHostId = Id,
LeNo = LeNo,
Name = Destination,
Status = Status
});
}
}
public void AddResourceEntry(TransportOrderStatus status, string destination)
{
// Is there an old resource that we need to reuse? (Primary key).
ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == destination);
if (oldResource != null)
{
oldResource.Status = status;
}
// If not create new.
else
{
Resources.Add(new ResourceList
{
OrdersHostId = Id,
LeNo = LeNo,
Name = destination,
Status = status
});
}
}
public void FinishResources(string destination)
{
// Is there an old resource that we need to reuse? (Primary key)
List<ResourceList> oldResource = Resources.Where(r => r.Name == destination).ToList();
foreach (ResourceList res in oldResource)
{
res.Status = TransportOrderStatus.Finished;
}
}
public void CancelResources(string destination)
{
List<ResourceList> oldResource = Resources.Where(r => r.Name == destination).ToList();
foreach (ResourceList res in oldResource)
{
Log.Write(LogLevel.Info, $"Cancelling previously assigned resource for Le {LeNo} to destination {destination}");
res.Status = TransportOrderStatus.Cancelled;
}
}
public OrdersHost EnterZone(IDestinationProperties destination)
{
if (!(TransportOrderStatusGroups.Active.Contains(Status) || Status == TransportOrderStatus.InSequencer))
{
return this;
}
Status = TransportOrderStatus.InDestinationZone;
UpdateResources(TransportOrderStatus.InDestinationZone, null);
return this;
}
/// <summary>
/// Set the status to <code>InDestinationZone</code>, update the resources and register the Le at the destination (if
/// known)
/// </summary>
/// <param name="destinationService"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public OrdersHost Arrive(IDestinationService destinationService)
{
if (TransportOrderStatusGroups.Waiting.Contains(Status))
{
throw new InvalidOperationException("Order has to start before it can arrive.");
}
if (TransportOrderStatusGroups.Complete.Contains(Status))
{
throw new InvalidOperationException("Order can not arrive if it is already finished.");
}
if (TransportOrderStatusGroups.Active.Contains(Status))
{
Status = TransportOrderStatus.InDestinationZone;
destinationService?.SetArrived(LeNo, Destination);
UpdateResources(TransportOrderStatus.InDestinationZone, null);
}
return this;
}
/// <summary>
/// finish this order, related OrdersMiniload, Orders Conveyor and clear the resources
/// </summary>
public void Finish()
{
OrdersConveyor.ToList().ForEach(oc =>
{
if (TransportOrderStatusGroups.Open.Contains(oc.Status))
{
oc.HandleFinished();
using IWcsDbContext db = new WcsDbContextFactory().GetDbContext();
ConveyorTelegrams.SendTordDeleteEtra(db, oc.LeNo);
db.SaveChanges();
}
});
OrdersMiniload.ToList().ForEach(om =>
{
if (TransportOrderStatusGroups.Open.Contains(om.Status))
{
om.HandleFinished();
}
});
Status = TransportOrderStatus.Finished;
UpdateResources(TransportOrderStatus.Finished, null);
}
/// <summary>
/// Cancel this order, related OrdersMiniload, Orders Conveyor and clear the resources
/// </summary>
public void Cancel(string error, string info, bool cancelOrdersMiniload = true)
{
OrdersConveyor.ToList().ForEach(oc =>
{
if (TransportOrderStatusGroups.Open.Contains(oc.Status))
{
oc.Cancel(error);
}
});
if (cancelOrdersMiniload)
{
OrdersMiniload.ToList().ForEach(om =>
{
if (TransportOrderStatusGroups.Open.Contains(om.Status))
{
om.Cancel(error);
}
});
}
Status = TransportOrderStatus.Cancelled;
Error = error;
Info = info;
UpdateResources(TransportOrderStatus.Cancelled, null);
}
public OrdersMiniload GetPendingOrdersMiniload()
{
return OrdersMiniload?.SingleOrDefault(o => o.Status == TransportOrderStatus.Pending);
}
public OrdersMiniload GetOpenOrdersMiniload()
{
return OrdersMiniload?.FirstOrDefault(o => TransportOrderStatusGroups.Open.Contains(o.Status));
}
public OrdersMiniload GetActiveOrdersMiniload()
{
return OrdersMiniload?.SingleOrDefault(o => TransportOrderStatusGroups.Active.Contains(o.Status));
}
public OrdersConveyor GetActiveOrdersConveyor()
{
return OrdersConveyor?.SingleOrDefault(o => TransportOrderStatusGroups.Active.Contains(o.Status));
}
public void UpdateResources(TransportOrderStatus resourceListStatus, List<ResourceList> occupiedBySameLeButDifferentOrder)
{
//If a OrdersHost is Finished or Cancelled all ResourceList for this OH are affected, for other status only entries with same Destination
Resources
.Where(r => !TransportOrderStatusGroups.Complete.Contains(r.Status) && (r.Name == Destination || resourceListStatus == TransportOrderStatus.Finished || resourceListStatus == TransportOrderStatus.Cancelled))
.ToList()
.ForEach(r => r.Status = resourceListStatus);
occupiedBySameLeButDifferentOrder?.ForEach(r => r.Status = TransportOrderStatus.Cancelled);
}
public void UpdateSpecificResource(TransportOrderStatus resourceListStatus, string destination)
{
ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == destination);
if (oldResource == null)
{
return;
}
else
{
oldResource.Status = resourceListStatus;
}
}
public OrdersHost ReplaceNextEmptyLe(Le le)
{
if (!LeType.EmptyLeTypeNames.Contains(Le.Type))
{
throw new ArgumentException($"Reassigning an LE is only allowed to replace any of {nameof(LeType.EmptyLeTypeNames)}. Current LE: {le.LeNo}");
}
Le = le;
ResourceList resource = Resources.SingleOrDefault(r => r.Name == Destination);
if (resource != null)
{
resource.Le = le;
}
return this;
}
public override string ToString()
{
return $"{nameof(OrdersHost)}[{nameof(Id)}: {Id}, {nameof(LeNo)}: {LeNo}, {nameof(Type)}: {Type}, {nameof(Source)}: {Source}, {nameof(Destination)}: {Destination}, {nameof(Status)}: {Status}, {nameof(IdOrderWmsHead)}: {IdOrderWmsHead}, {nameof(IdOrderWms)}: {IdOrderWms}, {nameof(IdOrderWmsPos)}: {IdOrderWmsPos}]";
}
/// <summary>
/// Set the status to InDestinationZone regardless of the previous status. This is ONLY to be used for creating an orders host dummy when we get a no read on the outputs. Otherwise we cannot send an UnloadHuToAgv telegram to unload the board to an AGV.
/// </summary>
/// <returns></returns>
public OrdersHost ForceSetStatusInDestinationZone()
{
Status = TransportOrderStatus.InDestinationZone;
return this;
}
public OrdersHost ForceSetStatusInSequencer()
{
Status = TransportOrderStatus.InSequencer;
return this;
}
public OrdersHost ForceSetStatusInProgress()
{
Status = TransportOrderStatus.InProgress;
return this;
}
public OrdersHost ForceSetStatusTransmitted()
{
Status = TransportOrderStatus.Transmitted;
return this;
}
public bool IsSequencerCancelOrder()
{
return Source.Contains(WcsNames.SEQ) && Destination == Common.Constants.MfcAllDestinations.StorageLoop2;
}
}

View File

@@ -0,0 +1,48 @@
using Gebhardt.DbAccess.Base.Configuration;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using static Gebhardt.StoreWare.WcsWms.Constants.ModelConstants;
namespace Gebhardt.StoreWare.Wcs.Common.DbAccess.Configuration;
public class OrdersHostEntityConfiguration : BaseEntityConfiguration<OrdersHost>
{
public override void Configure(EntityTypeBuilder<OrdersHost> builder)
{
base.Configure(builder);
builder.HasIndex(e => new { e.LeNo, e.Status }, "I1_OrdersHost");
builder.HasIndex(e => new { e.Destination, e.Status }, "I2_OrdersHost");
builder.HasIndex(e => new { e.Type, e.Status }, "I3_OrdersHost");
builder.Property(e => e.Destination).IsRequired().HasMaxLength(OrderSourceDestLength);
builder.Property(e => e.Error).HasMaxLength(ErrorTextLength);
builder.Property(e => e.IdOrderWms);
builder.Property(e => e.IdOrderWmsHead);
builder.Property(e => e.IdOrderWmsPos);
builder.Property(e => e.Info).HasMaxLength(InfoTextLength);
builder.Property(e => e.Source).IsRequired().HasMaxLength(OrderSourceDestLength);
builder.Property(e => e.Status)
.IsRequired();
builder.Property(e => e.Type)
.IsRequired();
builder.Property(e => e.LeNo).IsRequired().HasMaxLength(LeNoLength);
builder.HasOne(e => e.Le).WithMany().HasForeignKey(e => e.LeNo).OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.Resources).WithOne(e => e.OrdersHost).HasForeignKey(e => e.OrdersHostId).IsRequired();
}
}

Binary file not shown.

View File

@@ -1,25 +0,0 @@
/// <summary>
/// finish this order, related OrdersMiniload, Orders Conveyor and clear the resources
/// </summary>
public void Finish()
{
OrdersConveyor.ToList().ForEach(oc =>
{
if (TransportOrderStatusGroups.Open.Contains(oc.Status))
{
oc.HandleFinished();
using IWcsDbContext db = new WcsDbContextFactory().GetDbContext();
ConveyorTelegrams.SendTordDeleteEtra(db, oc.LeNo);
db.SaveChanges();
}
});
OrdersMiniload.ToList().ForEach(om =>
{
if (TransportOrderStatusGroups.Open.Contains(om.Status))
{
om.HandleFinished();
}
});
Status = TransportOrderStatus.Finished;
UpdateResources(TransportOrderStatus.Finished, null);
}

View File

@@ -1,5 +0,0 @@
public OrdersHost ForceSetStatusInProgress()
{
Status = TransportOrderStatus.InProgress;
return this;
}

View File

@@ -1,9 +0,0 @@
public void UpdateResources(TransportOrderStatus resourceListStatus, List<ResourceList> occupiedBySameLeButDifferentOrder)
{
//If a OrdersHost is Finished or Cancelled all ResourceList for this OH are affected, for other status only entries with same Destination
Resources
.Where(r => !TransportOrderStatusGroups.Complete.Contains(r.Status) && (r.Name == Destination || resourceListStatus == TransportOrderStatus.Finished || resourceListStatus == TransportOrderStatus.Cancelled))
.ToList()
.ForEach(r => r.Status = resourceListStatus);
occupiedBySameLeButDifferentOrder?.ForEach(r => r.Status = TransportOrderStatus.Cancelled);
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
{
internal record OrderListItem(int OrdersHostId, TransportOrderStatus Status, Le Le, string Destination, int Priority, int? IdOrderWmsHead,
DateTime Created, string HostDestination, bool? DepartureFlag, string? DepartureLocation);
internal class OrderList : List<OrderListItem>
{
public OrderList(List<OrderListItem> items) : base(items)
{
}
/// <summary>
/// Removes all order list items with the same LeNo and a higher list index.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualLeNo(OrderListItem item)
{
if (item is {Le: { }})
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Le.LeNo == item.Le.LeNo);
}
}
/// <summary>
/// Removes all order list items with the same destination and a higher list index.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualDestination(OrderListItem item)
{
if (item != null)
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Destination == item.Destination);
}
}
/// <summary>
/// Removes all order list items with the same aisle name / storage area and a higher list index.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualAisle(OrderListItem item)
{
if (item?.Le?.StorageArea != null && item?.Le?.AisleName != null)
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Le.StorageArea == item.Le.StorageArea && i.Le.AisleName == item.Le.AisleName);
}
}
/// <summary>
/// Removes all order list items with a higher list index that have the same LeNo but lower priority.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualLeNoButLowerPriority(OrderListItem item)
{
if (item is {Le: { }})
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Le.LeNo == item.Le.LeNo && i.Priority < item.Priority);
}
}
}
}

View File

@@ -0,0 +1,276 @@
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 Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using static Gebhardt.StoreWare.Wcs.Common.Constants;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo;
public class OrderManager : ProcessWorker
{
private readonly IAisleService _aisleService;
private readonly IWcsDbContextFactory _dbContextFactory;
private readonly IDestinationService _destinationService;
private readonly ILeService _leService;
private readonly ITransportOrderService _transportOrderService;
public OrderManager(IDestinationService destinationService, ITransportOrderService transportOrderService, ILeService leService, IAisleService aisleService, IWcsDbContextFactory dbContextFactory, int workInterval)
: base(nameof(OrderManager), workInterval, true)
{
_destinationService = destinationService;
_transportOrderService = transportOrderService;
_leService = leService;
_aisleService = aisleService;
_dbContextFactory = dbContextFactory;
}
public override bool DoWork()
{
bool workDone = false;
try
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
IQueryable<OrdersHost> departureNotificationOrders = db.OrdersHost
.ByStatus(TransportOrderStatus.InDestinationZone, TransportOrderStatus.Pending)
.IsDepartureReady();
foreach (OrdersHost order in departureNotificationOrders)
{
// Restart order: Case when order has status InDestinationZone and has been flagged for departure by HostBooking (DepartureNotificationHandler)
if (order.Status == TransportOrderStatus.InDestinationZone)
{
order.UpdateResources(TransportOrderStatus.InProgress, null);
order.ForceSetStatusInProgress();
order.Le.SetStatus(LeStatus.OnConveyor);
_destinationService.SetLeRequestDeparture(order.Le.LeNo, order.DepartureLocation);
// TODO Check if telegram can be sent from here
ConveyorTelegrams.SendDepartureEtra(db, order.LeNo, order.DepartureLocation);
order.DepartureFlag = false;
order.DepartureLocation = null;
db.SaveChanges();
}
}
// Starting orders that are initial and were marked for departure by HostBooking (DepartureNotificationHandler)
OrderList departureOrders = new(departureNotificationOrders
.ByStatus(TransportOrderStatus.Pending)
.OrderBy(o => o.StartTime)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority,
o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag, o.DepartureLocation))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(departureOrders);
IQueryable<OrdersHost> pendingOrders = db.OrdersHost.ByStatus(TransportOrderStatus.Pending);
OrderList onConveyor = new(pendingOrders
.Where(o => o.Le.LocationId == null
&& (!o.Le.IsEmpty || o.Type == TransportOrderType.TransportHost)
&& (o.Le.Status == LeStatus.OnConveyor || o.Le.Status == LeStatus.InDestinationZone))
.OrderBy(o => o.StartTime)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag, o.DepartureLocation))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(onConveyor);
var pendingOrdersWithDemand = pendingOrders
.Where(x => x.Le.Status != LeStatus.Created)
//Do not start pending orders for LEs inside a sequencer
.ExcludeOrdersInSequencer()
//Consider all destinations that are commissioning workstations or a storage area
.ByDestination(_destinationService.GetCommissioningWorkstations()
.Union(_destinationService.Where(d => d.IsStorageArea).Select(d => d.Name))
.Union(_destinationService.Where(d => d.IsSequencer).Select(d => d.Name)));
//// Always prioritize cancelled sequencer orders
//var cancelledSeqOrders = pendingOrdersWithDemand.ByCancelledSequencerOrder();
//OrderList forResourcesWithDemand = new(cancelledSeqOrders
// .Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
// .AsNoTracking()
// .ToList());
//workDone |= StartNextOrders(forResourcesWithDemand);
//Then do the normal orders with demand.
pendingOrdersWithDemand = pendingOrdersWithDemand.ApplyWmsOrderingSequencerRetrievalTime(db);
OrderList forResourcesWithDemand = new(pendingOrdersWithDemand
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag, o.DepartureLocation))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(forResourcesWithDemand);
OrderList forOtherDestinations = new(pendingOrders
.Where(x => x.Le.Status != LeStatus.Created)
.ExcludeDestination(db.ResourceSetting.Select(r => r.Name).ToList())
.ApplyWmsOrderingSequencerRetrievalTime(db)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination , o.DepartureFlag, o.DepartureLocation))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(forOtherDestinations);
}
catch (Exception ex)
{
Log.WriteException(ex);
}
return workDone;
}
private bool StartNextOrders(OrderList orders)
{
bool workDone = false;
using IWcsDbContext db = _dbContextFactory.GetDbContext();
List<Le> les = db.Le.Where(l => orders.Select(o => o.Le.LeNo).Contains(l.LeNo)).Distinct().ToList();
for (int i = 0; i < orders.Count; i++)
{
OrderListItem order = orders[i];
try
{
Le le = les.Single(l => l.LeNo == order.Le.LeNo);
if (LeIsExcludedAsAisleNotReady(orders, le, order)
|| LeIsExcludedAsOrderMiniloadIsActive(orders, db, le, order)
|| LeIsExcludedAsLeIsOnItsWayToNOK(orders, db, order))
{
workDone = true;
continue;
}
// Special case for sequencer orders: reserve half the space for each workstation
if (order.Destination.StartsWith("SEQ"))
{
Resource sequencerResource = db.ResourceSetting.GetResourceByName(order.Destination);
if (sequencerResource != null)
{
SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.MaximumUsableCapacityPercentPerWorkstation, out int maximumUsableCapacityPercentPerWorkstation, 50, true);
maximumUsableCapacityPercentPerWorkstation = maximumUsableCapacityPercentPerWorkstation > 100 ? 100 : maximumUsableCapacityPercentPerWorkstation;
maximumUsableCapacityPercentPerWorkstation = maximumUsableCapacityPercentPerWorkstation < 50 ? 50 : maximumUsableCapacityPercentPerWorkstation;
int maximumUsableCapacityPerWorkstation = (sequencerResource.Capacity + sequencerResource.Overload) * maximumUsableCapacityPercentPerWorkstation / 100;
// Count active orders for this specific HostDestination going to the same sequencer
int activeOrdersForHostDestination = db.OrdersHost
.Count(o => o.HostDestination == order.HostDestination
&& o.Destination == order.Destination
&& (o.Status == TransportOrderStatus.InProgress
|| o.Status == TransportOrderStatus.InDestinationZone
|| o.Status == TransportOrderStatus.InSequencer));
if (activeOrdersForHostDestination >= maximumUsableCapacityPerWorkstation)
{
Log.Write(LogLevel.Info, 30, $"Sequencer capacity limit reached for {order.HostDestination} at {order.Destination}. Active: {activeOrdersForHostDestination}, Reserved: {maximumUsableCapacityPerWorkstation}");
continue;
}
}
}
Resource resource = db.ResourceSetting.GetResourceByName(order.Destination);
if (resource is null or { Demand: > 0 })
{
// Only cancel active transports not transportHost. Only WMS is allowed to finish this orders
var activeOrdersNotOnTheWayToWorkstation = db.OrdersHost
.ByLeNo(order.Le.LeNo)
.Active()
.Where(o => o.Type != TransportOrderType.TransportHost);
if (activeOrdersNotOnTheWayToWorkstation.Any())
{
_leService.CancelActiveTransports(order.Le.LeNo, $"Another active order to a commissioning area exists");
} else if (!db.OrdersHost.ByLeNo(order.Le.LeNo).Active().Any())
{
_transportOrderService.StartNextTransport(order.OrdersHostId);
// Should not change through Transportstart (flag is set to false after OrdersHost start) because boolean is saved inside OrderListItem
if (order.DepartureFlag == true)
{
// Centralized from HostBooking (DepartureNotificationHandler) for otherPicOrder
_destinationService.SetLeRequestDeparture(order.Le.LeNo, order.DepartureLocation);
ConveyorTelegrams.SendDepartureEtra(db, order.Le.LeNo, order.DepartureLocation);
db.Le.FirstOrDefault(l => l.LeNo == order.Le.LeNo)?.SetStatus(LeStatus.OnConveyor);
db.SaveChanges();
}
}
orders.RemoveSubsequentWithEqualLeNo(order);
workDone = true;
}
else if (resource is { Demand: <= 0 })
{
if (!le.IsInStorage())
{
List<string> destinationAisles = _destinationService.Where(d => d.IsStorage).Select(d => d.Name).ToList();
if (!db.OrdersHost.OpenByLeNo(order.Le.LeNo).ByDestination(destinationAisles).Any())
{
_transportOrderService.PostponeOrdersHost(order.OrdersHostId, $"Destination {order.Destination} has no demand.");
orders.RemoveSubsequentWithEqualLeNo(order);
}
continue;
}
orders.RemoveSubsequentWithEqualDestination(order);
}
}
catch (Exception ex)
{
Log.Write(LogLevel.Error, $"Can not start OrdersHost: {order.OrdersHostId} for LE: {order.Le.LeNo}");
Log.WriteException(ex);
}
}
return workDone;
}
private static bool LeIsExcludedAsOrderMiniloadIsActive(OrderList orders, IWcsDbContext db, Le le, OrderListItem order)
{
if (db.OrdersMiniload.Underway().ByLeNo(le.LeNo).Any())
{
orders.RemoveSubsequentWithEqualLeNo(order);
Log.Write(LogLevel.Info, $"{nameof(OrdersHost)} with Id '{order.OrdersHostId}' for LE {le.LeNo} cannot be started due to an open OrdersMiniload.");
return true;
}
return false;
}
private bool LeIsExcludedAsAisleNotReady(OrderList orders, Le le, OrderListItem order)
{
if (le.IsInStorage())
{
if (!_aisleService.IsAisleReadyForRetrievalOrder(order.Le.StorageArea, order.Le.AisleName))
{
orders.RemoveSubsequentWithEqualAisle(order);
Log.Write(LogLevel.Info, $"{nameof(OrdersHost)} with Id '{order.OrdersHostId}' for LE {le.LeNo} cannot be started as not all participating aisles / devices are available for {le.AisleName}/{le.StorageArea}.");
return true;
}
}
return false;
}
private bool LeIsExcludedAsLeIsOnItsWayToNOK(OrderList orders, IWcsDbContext db, OrderListItem order)
{
OrdersHost orderForSameLe = db.OrdersHost
.ByLeNo(order.Le.LeNo)
.ByStatus(TransportOrderStatus.Pending, TransportOrderStatus.InProgress, TransportOrderStatus.InDestinationZone)
.OrderBy(o => o.Status != TransportOrderStatus.Pending ? 0 : 1) // get active order (if any), pending otherwise
.FirstOrDefault();
if (orderForSameLe == null)
{
return false;
}
if (orderForSameLe.Le.HasError() && orderForSameLe.Destination.IsInList(MfcAllDestinations.ERR12, MfcAllDestinations.ERR11, MfcAllDestinationsOldSystem.ERR01)
|| order.Le.HasError() && order.Destination.IsInList(MfcAllDestinations.ERR12, MfcAllDestinations.ERR11, MfcAllDestinationsOldSystem.ERR01))
{
orders.RemoveSubsequentWithEqualLeNo(order);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.Shared;
using Gebhardt.Shared.DbAccess;
using Gebhardt.Shared.Process;
using Gebhardt.StoreWare.Wcs.Common;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Dao;
using Gebhardt.StoreWare.Wcs.Common.Dao.Interfaces;
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 Microsoft.EntityFrameworkCore;
using static Gebhardt.StoreWare.Wcs.Common.Constants;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
{
public class StartInitialOrdersHost : ProcessWorker
{
private readonly IWcsDbContextFactory _dbContextFactory;
private readonly ITransportOrderService _transportOrderService;
public StartInitialOrdersHost(int workInterval, ITransportOrderService transportOrderService, IWcsDbContextFactory dbContextFactory) : base(nameof(StartInitialOrdersHost), workInterval, true)
{
_transportOrderService = transportOrderService;
_dbContextFactory = dbContextFactory;
}
public override bool DoWork()
{
try
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
OrderList initialOrders = new(db.OrdersHost
.ByStatus(TransportOrderStatus.Initial)
.ExcludeNextEmpty()
.Where(o => o.Le.Status != LeStatus.Created)
.ApplyWmsOrderingSequencerRetrievalTime(db)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag, o.DepartureLocation))
.ToList());
//This step is needed to only start orders with all LEs available
initialOrders = OnlyFirstOrderPerLeNo(db, initialOrders);
return StartOrders(initialOrders);
}
catch (Exception ex)
{
Log.WriteException(ex);
return false;
}
}
/// <summary>
/// Returns only the first order per LE
/// </summary>
/// <param name="initialOrders"></param>
/// <returns>Filtered List</returns>
private OrderList OnlyFirstOrderPerLeNo(IWcsDbContext db, OrderList initialOrders)
{
List<OrderListItem> distinctOrderList = new ();
List<OrderListItem> lockedOrders = new();
foreach (OrderListItem order in initialOrders)
{
//Only take first order per LE
if (!distinctOrderList.Any(d=>d.Le.LeNo == order.Le.LeNo))
{
distinctOrderList.Add(order);
}
//Others are marked as locked (for info message)
else
{
lockedOrders.Add(order);
Log.Write(LogLevel.Info, 60, $"OrdersHost ID {order.OrdersHostId} for LeNo {order.Le.LeNo} waits for OrdersHost: ID {distinctOrderList.Where(d => d.Le.LeNo == order.Le.LeNo).First().OrdersHostId} which is started first.");
}
}
//Make visible in DB (and to user)
string infoMessage = "Le has transports that are started first.";
var markOrders = db.OrdersHost.Where(o => lockedOrders.Select(l => l.OrdersHostId).Contains(o.Id) && o.Info != infoMessage).ToList();
foreach (var order in markOrders)
{
order.Info = infoMessage;
}
//Rest Info if order can be started
var unMarkOrders = db.OrdersHost.Where(o => distinctOrderList.Select(l => l.OrdersHostId).Contains(o.Id) && o.Info == infoMessage).ToList();
foreach (var order in unMarkOrders)
{
order.Info = null;
}
db.SaveChanges();
return new OrderList(distinctOrderList);
}
private bool StartOrders(OrderList orderList)
{
bool workDone = false;
using IWcsDbContext db = _dbContextFactory.GetDbContext();
for (int i = 0; i < orderList.Count; i++)
{
OrderListItem orderToBeScheduled = orderList[i];
try
{
// If there exists a cancelled sequencer orders for this LE this should always be started first
var cancelledSequencerOrder = db.OrdersHost
.ByLeNo(orderToBeScheduled.Le.LeNo)
.ByStatus(TransportOrderStatus.Initial)
.ByCancelledSequencerOrder()
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag, o.DepartureLocation))
.ToList();
if (cancelledSequencerOrder.FirstOrDefault() != null)
{
var cancelledOrderToBeScheduled = cancelledSequencerOrder.FirstOrDefault();
Log.Write(LogLevel.Debug, $"{nameof(OrdersHost)} with Id {cancelledOrderToBeScheduled.OrdersHostId} will be scheduled since this is a cancelled sequencer order.");
_transportOrderService.ScheduleOrdersHost(cancelledOrderToBeScheduled.OrdersHostId);
//Order has been scheduled. Do not consider orders for the same LE.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
workDone = true;
continue;
}
// If there exists an order that has been flagged by HostBooking for departure
var departureFlaggedOrder = db.OrdersHost
.ByLeNo(orderToBeScheduled.Le.LeNo)
.ByStatus(TransportOrderStatus.Initial)
.IsDepartureReady()
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority,
o.IdOrderWmsHead, o.Created, o.HostDestination, o.DepartureFlag, o.DepartureLocation))
.ToList();
if (departureFlaggedOrder.FirstOrDefault() != null)
{
var departureOrderToBeScheduled = departureFlaggedOrder.FirstOrDefault();
Log.Write(LogLevel.Debug, $"{nameof(OrdersHost)} with Id {departureOrderToBeScheduled.OrdersHostId} will be scheduled since it has been flagged for departure");
_transportOrderService.ScheduleOrdersHost(departureOrderToBeScheduled.OrdersHostId);
//Order has been scheduled. Do not consider orders for the same LE.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
workDone = true;
continue;
}
OrdersHost orderForSameLe = db.OrdersHost
.ByLeNo(orderToBeScheduled.Le.LeNo)
.ByStatus(TransportOrderStatus.Pending, TransportOrderStatus.InProgress, TransportOrderStatus.InDestinationZone, TransportOrderStatus.InSequencer)
.OrderBy(o => o.Status != TransportOrderStatus.Pending ? 0 : 1) // get active order (if any), pending otherwise
.FirstOrDefault();
if (orderForSameLe != null && orderForSameLe.Id != orderToBeScheduled.OrdersHostId)
{
workDone = TryScheduleWithExistingOrder(orderForSameLe, db, orderToBeScheduled);
//Check equal LEs with open order only once
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
continue;
}
if (TryScheduleWithoutExistingOrder(orderToBeScheduled, db))
{
//Order has been scheduled. Do not consider orders for the same LE.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
workDone = true;
continue;
}
else
{
Log.Write(LogLevel.Debug, $"Destination: {orderToBeScheduled.Destination} has no ressources, skip all orders to this destination");
orderList.RemoveSubsequentWithEqualDestination(orderToBeScheduled);
}
}
catch(Exception ex)
{
Log.Write(LogLevel.Error, $"Can not set ordersHost {orderToBeScheduled.OrdersHostId} for LE {orderToBeScheduled.Le.LeNo} to pending. Due to Exception:");
Log.WriteException(ex);
}
//We can remove all subsequent orders with same LE, because the list is already ordered in a way that priorities are taken into account.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
}
return workDone;
}
private bool TryScheduleWithoutExistingOrder(OrderListItem orderToBeScheduled, IWcsDbContext db)
{
if (CanScheduleOrder(orderToBeScheduled, db))
{
Log.Write(LogLevel.Debug, $"{nameof(OrdersHost)} with Id {orderToBeScheduled.OrdersHostId} will be scheduled, as there are no other {nameof(OrdersHost)} scheduled or active orders for the same LE.");
_transportOrderService.ScheduleOrdersHost(orderToBeScheduled.OrdersHostId);
return true;
}
return false;
}
private bool CanScheduleOrder(OrderListItem orderToBeScheduled, IWcsDbContext db)
{
// Orders with their associated LE not being in storage can always be scheduled...
//TODO Check for OnConveyor/InDestinationZone? IsInStorage() does not cover OnInputLeLifter/OnLhd/OnOutputLeLifter...
if (!orderToBeScheduled.Le.IsInStorage())
{
return true;
}
//...if LE is in storage, whether the order can be scheduled is determined by the amount of pending orders that may exist towards the destination and the destination's demand.
if (!SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.MaxPendingOrdersPerDestination, out int maxPendingOrdersPerDestination))
{
maxPendingOrdersPerDestination = 10;
}
Resource resourceDestination = db.ResourceSetting.GetResourceByName(orderToBeScheduled.Destination);
// for unmanaged resources, a demand of 1 is used.
int destinationDemand = resourceDestination?.Demand ?? 1;
int countPendingOrdersToDestination = db.OrdersHost.ByStatus(TransportOrderStatus.Pending).ByDestination(orderToBeScheduled.Destination).Count();
return destinationDemand - countPendingOrdersToDestination > 0 || countPendingOrdersToDestination - destinationDemand < maxPendingOrdersPerDestination;
}
private bool TryScheduleWithExistingOrder(OrdersHost orderForSameLe, IWcsDbContext db, OrderListItem orderToBeScheduled)
{
// We not want to overwrite existing orders. We wait until we are back in Storage. (This happened only about 40 times a day.
return false;
}
}
}

View File

@@ -0,0 +1,330 @@
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;
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,6 @@
## Aufbau Tabelle OrdersHost in Relationenschreibweise
### Schema: TabellenName:(AttributName DatenTyp Restriktion)
### Schlüssel: PK: Primärschlüssel, FK: Fremdschlüssel
OrdersHost(Id [PK] int not_null, LeNo [FK] string not_null, Type string not_null, Source string not_null, Destination string not_null, HostDestination string not_null, Status string not_null, IdOrderWmsHead int, IdOrderWms int, IdOrderWmsPos int, Priority int not_null, PriorityDate datetime, StartTime datetime, Info string, Error string, IsEmptyLeRequest bool not_null, SequenceWms int, IsDirectPicking bool, IsStolen bool, SequencerRetrievalTime datetime, Creator string not_null, Created datetime not_null, CcuVersion int not_null, Process string not_null, Timestamp datetime not_null, DepartureFlag bool, DepartureLocation string, )

BIN
03_Realisierung/HostBooking/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,160 @@
using System.Linq;
using Gebhardt.Shared;
using Gebhardt.StoreWare.Wcs.Common;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Factories;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Queries;
using Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms.Interfaces;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.Interfaces;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.Models;
namespace Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms.MessageImplementation
{
public class DepartureNotificationHandler : IHandleRecord<IDepartureNotification>
{
private readonly IDestinationService _destinationService;
private readonly ITransportOrderService _transportOrderService;
private readonly IWcsDbContextFactory _wcsDbContextFactory;
public DepartureNotificationHandler(IDestinationService destinationService, ITransportOrderService transportOrderService, IWcsDbContextFactory wcsDbContextFactory)
{
_destinationService = destinationService;
_transportOrderService = transportOrderService;
_wcsDbContextFactory = wcsDbContextFactory;
}
public bool Handle(IDepartureNotification message)
{
using IWcsDbContext db = _wcsDbContextFactory.GetDbContext();
// If we don't have the LE, we need to depart. Otherwise we can't get rid of it.
if (db.Le.ByLeNo(message.LeNo) == null || message.LeNo.StartsWith("B") || message.StorageArea.ToLower() == "dummy")
{
ConveyorTelegrams.SendDepartureEtra(db, message.LeNo, message.Position);
//For EtraBoxes we can Finish the OrdersHost here As there is no feedback anymore for this boxes:
if (message.LeNo.StartsWith("B") && message.Position.Contains("PTL"))
{
//Finish all TransportOrders from this PTL Place as the place is empty now.
var etraBoxOrder = db.OrdersHost.ByStatus(TransportOrderStatus.Transmitted).Where(o => o.Source == message.Position);
if (etraBoxOrder.Any())
{
foreach (var oh in etraBoxOrder)
{
oh.Finish();
if (oh.LeNo == message.LeNo)
{
Log.Write(LogLevel.Info, $"EtraBox {oh.LeNo} is pushed off, finish orders host {oh.Id}.");
}
else
{
oh.Info = $"CleanedUp by Departure for {message.LeNo}";
Log.Write(LogLevel.Info, $"EtraBox {oh.LeNo} orders host {oh.Id} is finished because EtraBox {message.LeNo} was pushed off from the same PTL place.");
}
}
}
}
db.SaveChanges();
return true;
}
var openOH = db.OrdersHost.OpenByLeNo(message.LeNo).ToList();
if (!openOH.Any())
{
//Receive and Replenishment places don't need to create a WCS Transport.
if(message.Position.StartsWith(Constants.MfcAllDestinationsOldSystem.IPT01.Substring(0,3)) || message.Position.StartsWith(Constants.MfcAllDestinations.RCV01.Substring(0, 3)) || message.Position== Constants.MfcAllDestinationsOldSystem.TOPUP)
{
//Ignore and go on
return true;
}
OrdersHost ordersHost = OrdersHostFactory.GetInstance()
.InitialForLe(message.LeNo, TransportOrderType.Transport, message.Position, _destinationService.GetDefaultStorage(message.Position));
db.Add(ordersHost);
// Always depart a box on the error stations to prevent blocks
IDestinationProperties destination = _destinationService.Get(message.Position);
if (destination != null && destination.IsNio)
{
ConveyorTelegrams.SendDepartureEtra(db, message.LeNo, message.Position);
ordersHost.Le.SetStatus(LeStatus.OnConveyor);
}
Log.Write(LogLevel.Info, $"Added OrdersHost for {message.LeNo} from {message.Position} to {ordersHost.Destination}.");
}
else
{
// Is there an order to this place InProgress or InDestinationZone? => Start again by setting this order to pending
var currentOrderToPlace = db.OrdersHost
.ByLeNo(message.LeNo)
.ByDestination(message.Position)
.ByStatus(TransportOrderStatus.InProgress, TransportOrderStatus.InDestinationZone)
.Where (o => o.DepartureFlag != true)
.FirstOrDefault();
if (currentOrderToPlace != null)
{
if (currentOrderToPlace.Status == TransportOrderStatus.InDestinationZone)
{
IDestinationProperties destination = _destinationService.Get(currentOrderToPlace.Destination);
if (currentOrderToPlace.HostDestination != null)
{
currentOrderToPlace.Destination = destination.ConnectedSequencer;
}
//ConveyorTelegrams.SendDepartureEtra(db, message.LeNo, message.Position);
//currentOrderToPlace.UpdateResources(TransportOrderStatus.InProgress, null);
//currentOrderToPlace.ForceSetStatusInProgress();
//currentOrderToPlace.Le.SetStatus(LeStatus.OnConveyor);
// Flags order for departure so that ConveyorDispo can restart it.
currentOrderToPlace.DepartureFlag = true;
currentOrderToPlace.DepartureLocation = message.Position;
db.SaveChanges();
Log.Write(LogLevel.Info, $"OrdersHost for {message.LeNo} to {message.Position} flagged for departure by DepartureNotification.");
return true;
}
}
else
{
var test = db.OrdersHost.ByLeNo(message.LeNo).FirstOrDefault();
var otherPicOrder = db.OrdersHost.ByLeNo(message.LeNo)
.ByStatus(TransportOrderStatus.Initial)
.Where(o => o.DepartureFlag != true)
.ApplyWmsOrdering()
.FirstOrDefault();
if (otherPicOrder != null)
{
// _transportOrderService.StartNextTransport(otherPicOrder.Id);
// DepatureFlag set to true to signal processing through ConveyorDispo instead of HostBooking
otherPicOrder.DepartureFlag = true;
otherPicOrder.DepartureLocation = message.Position;
db.SaveChanges();
return true;
}
//Set source to TOPUP in case Departure has been sent from TopUP or IPT stations, to be able to accept the box on scale
if (message.Position == Constants.MfcAllDestinationsOldSystem.TOPUP || message.Position.StartsWith(Constants.MfcAllDestinationsOldSystem.IPT01.Substring(0, 3)))
{
var activeOH = openOH.Where(o=>o.Status == TransportOrderStatus.InProgress).FirstOrDefault();
if (activeOH != null)
{
activeOH.Source = Constants.MfcAllDestinationsOldSystem.TOPUP;
}
}
else
{
_destinationService.SetLeRequestDeparture(message.LeNo, message.Position);
ConveyorTelegrams.SendDepartureEtra(db, message.LeNo, message.Position);
db.Le.FirstOrDefault(l => l.LeNo == message.LeNo)?.SetStatus(LeStatus.OnConveyor);
}
db.SaveChanges();
return true;
}
}
// update destination status!
_destinationService.SetLeRequestDeparture(message.LeNo, message.Position);
db.SaveChanges(true);
return true;
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,501 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.DbAccess.Base;
using Gebhardt.Shared;
using Gebhardt.Shared.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Factories;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
public class OrdersHost : AutoIncrementEntity
{
private Le _le;
private ICollection<OrdersConveyor> _ordersConveyor;
private ICollection<OrdersMiniload> _ordersMiniload;
private ICollection<ResourceList> _resources;
internal OrdersHost(TransportOrderType type, string leNo, string source, string destination, int priority, int? idOrderWms, int? idOrderWmsHead, int? idOrderWmsPos, OrderTypeWms? typeWms)
{
Type = type;
LeNo = leNo;
Source = source;
Destination = destination;
Priority = priority;
IdOrderWms = idOrderWms;
IdOrderWmsHead = idOrderWmsHead;
IdOrderWmsPos = idOrderWmsPos;
TypeWms = typeWms;
OrdersConveyor = new List<OrdersConveyor>();
OrdersMiniload = new List<OrdersMiniload>();
Resources = new List<ResourceList>();
}
private OrdersHost()
{
}
private ILazyLoader LazyLoader { get; set; }
public string LeNo { get; private set; }
public Le Le
{
get => LazyLoader.Load(this, ref _le);
private set => _le = value;
}
public TransportOrderType Type { get; private set; }
public string Source { get; set; }
/// <summary>
/// the (possibly gross) destination for the Le: a storage area, a work place, etc.
/// </summary>
public string Destination { get; set; }
public string HostDestination { get; set; }
public TransportOrderStatus Status { get; private set; } = TransportOrderStatus.Initial;
public int? IdOrderWmsHead { get; private set; }
public int? IdOrderWms { get; private set; }
public int? IdOrderWmsPos { get; private set; }
// Currently not used at ETRA
public int Priority { get; set; }
public DateTime? PriorityDate { get; set; }
public DateTime? StartTime { get; private set; }
public string Info { get; set; }
public OrderTypeWms? TypeWms { get; }
public string Error { get; internal set; }
public bool IsEmptyLeRequest { get; set; }
// Used by sequencer to know what order to send out boxes in.
public int? SequenceWms { get; set; }
public bool? IsDirectPicking { get; set; }
//Used in deadlock situations when the same box is used in multiple Sequencers
public bool? IsStolen { get; set; }
public DateTime? SequencerRetrievalTime { get; set; }
// Used to signal ConveyorDispo that a depature request has been made by HostBooking (DepatureNotificationHandler)
public bool? DepartureFlag { get; set; }
// Used to store the location of a DepartureNotification handled by HostBooking (DepartureNotificationHandler)
public string? DepartureLocation { get; set; }
public ICollection<OrdersMiniload> OrdersMiniload
{
get => LazyLoader.Load(this, ref _ordersMiniload);
set => _ordersMiniload = value;
}
public ICollection<OrdersConveyor> OrdersConveyor
{
get => LazyLoader.Load(this, ref _ordersConveyor);
set => _ordersConveyor = value;
}
public ICollection<ResourceList> Resources
{
get => LazyLoader.Load(this, ref _resources);
set => _resources = value;
}
//Todo-Job: I don't like that the order is directly transmitted and no Tord is sent.
public OrdersConveyor StartConveyorOrderToNextDestination(IDestinationProperties newDestination = null, string reasonforredirect = null)
{
if (Status == TransportOrderStatus.InDestinationZone && newDestination is { BuffersEmptyLe: true })
{
throw new InvalidOperationException($"Cannot start {nameof(OrdersConveyor)} to next destination ({newDestination.Name}) as the {nameof(OrdersHost)} has already arrived.");
}
OrdersConveyor openOrder = OrdersConveyor.SingleOrDefault(oc => TransportOrderStatusGroups.Open.Contains(oc.Status));
if (openOrder != null)
{
openOrder.HandleFinished(newDestination);
}
OrdersConveyor followingOrder = OrdersConveyorFactory.GetInstance(this).InitialForOrdersHost(openOrder?.Destination, newDestination?.Name);
followingOrder.Transmit(reasonforredirect);
return followingOrder;
}
public void Redirect(IDestinationProperties newDestination = null, string reasonForRedirect = null)
{
if (newDestination != null && newDestination.Name != Destination)
{
OrdersConveyor openOrder = OrdersConveyor.SingleOrDefault(oc => TransportOrderStatusGroups.Open.Contains(oc.Status));
if (openOrder != null)
{
openOrder.Cancel($"Redirected to {newDestination.Name}");
UpdateResources(TransportOrderStatus.Cancelled, null);
}
Destination = newDestination.Name;
Info = reasonForRedirect;
OrdersConveyor followingOrder = OrdersConveyorFactory.GetInstance(this).InitialForOrdersHost(openOrder?.Destination, newDestination.Name);
followingOrder.Transmit();
}
}
public OrdersHost Reinitialize(string info)
{
Status = TransportOrderStatus.Initial;
Info = info;
StartTime = null;
return this;
}
public OrdersHost Postpone(IDestinationProperties destination, string info)
{
if (!TransportOrderStatusGroups.Waiting.Contains(Status))
{
throw new InvalidOperationException("Only orders that have not started yet can be postponed.");
}
if (!destination.BuffersEmptyLe)
{
Reinitialize(info);
}
else
{
Cancel(string.Empty, info);
}
return this;
}
public OrdersConveyor TransportToNio(string error)
{
Error = error;
OrdersConveyor nioTransport = OrdersConveyor?.FirstOrDefault();
if (nioTransport == null)
{
nioTransport = OrdersConveyorFactory.GetInstance(this).PendingForOrdersHost(error: error);
}
nioTransport.Transmit(error);
return nioTransport;
}
public OrdersHost Schedule(string source = null, string info = null)
{
if (TransportOrderStatusGroups.Waiting.Contains(Status))
{
Status = TransportOrderStatus.Pending;
Info = info;
if (source != null)
{
Source = source;
}
}
else
{
Log.Write(LogLevel.Error, $"cannot schedule due to Status={Status}; {this} ");
}
return this;
}
/// <summary>
/// set the OrdersHost to initial - if it's Status is InProgress or Transmitted
/// </summary>
/// <param name="info">informal reason</param>
/// <returns>true if rescheduled</returns>
public bool Reschedule(string info = null, string source = null)
{
if (Status != TransportOrderStatus.InProgress && Status != TransportOrderStatus.Transmitted)
{
Log.Write(LogLevel.Error, $"cannot reschedule due to Status={Status}; {this} ");
return false;
}
Status = TransportOrderStatus.Initial;
Info = info;
if (source != null) { Source = source; }
Log.Write(LogLevel.Info, $"Rescheduled {this}");
return true;
}
public OrdersHost Start()
{
if (TransportOrderStatusGroups.Waiting.Contains(Status))
{
Status = TransportOrderStatus.InProgress;
StartTime = DBDateTime.Now;
DepartureFlag = false;
DepartureLocation = null;
}
return this;
}
public void ManageResources()
{
if (!(Resources ??= new List<ResourceList>()).All(r => r.Status == TransportOrderStatus.Cancelled || r.Status == TransportOrderStatus.Finished) &&
Resources.Count != 0)
{
return;
}
// Is there an old resource that we need to reuse? (Primary key).
ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == Destination);
if (oldResource != null)
{
oldResource.Status = Status;
}
// If not create new.
else
{
Resources.Add(new ResourceList
{
OrdersHostId = Id,
LeNo = LeNo,
Name = Destination,
Status = Status
});
}
}
public void AddResourceEntry(TransportOrderStatus status, string destination)
{
// Is there an old resource that we need to reuse? (Primary key).
ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == destination);
if (oldResource != null)
{
oldResource.Status = status;
}
// If not create new.
else
{
Resources.Add(new ResourceList
{
OrdersHostId = Id,
LeNo = LeNo,
Name = destination,
Status = status
});
}
}
public void FinishResources(string destination)
{
// Is there an old resource that we need to reuse? (Primary key)
List<ResourceList> oldResource = Resources.Where(r => r.Name == destination).ToList();
foreach (ResourceList res in oldResource)
{
res.Status = TransportOrderStatus.Finished;
}
}
public void CancelResources(string destination)
{
List<ResourceList> oldResource = Resources.Where(r => r.Name == destination).ToList();
foreach (ResourceList res in oldResource)
{
Log.Write(LogLevel.Info, $"Cancelling previously assigned resource for Le {LeNo} to destination {destination}");
res.Status = TransportOrderStatus.Cancelled;
}
}
public OrdersHost EnterZone(IDestinationProperties destination)
{
if (!(TransportOrderStatusGroups.Active.Contains(Status) || Status == TransportOrderStatus.InSequencer))
{
return this;
}
Status = TransportOrderStatus.InDestinationZone;
UpdateResources(TransportOrderStatus.InDestinationZone, null);
return this;
}
/// <summary>
/// Set the status to <code>InDestinationZone</code>, update the resources and register the Le at the destination (if
/// known)
/// </summary>
/// <param name="destinationService"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public OrdersHost Arrive(IDestinationService destinationService)
{
if (TransportOrderStatusGroups.Waiting.Contains(Status))
{
throw new InvalidOperationException("Order has to start before it can arrive.");
}
if (TransportOrderStatusGroups.Complete.Contains(Status))
{
throw new InvalidOperationException("Order can not arrive if it is already finished.");
}
if (TransportOrderStatusGroups.Active.Contains(Status))
{
Status = TransportOrderStatus.InDestinationZone;
destinationService?.SetArrived(LeNo, Destination);
UpdateResources(TransportOrderStatus.InDestinationZone, null);
}
return this;
}
/// <summary>
/// finish this order, related OrdersMiniload, Orders Conveyor and clear the resources
/// </summary>
public void Finish()
{
OrdersConveyor.ToList().ForEach(oc =>
{
if (TransportOrderStatusGroups.Open.Contains(oc.Status))
{
oc.HandleFinished();
using IWcsDbContext db = new WcsDbContextFactory().GetDbContext();
ConveyorTelegrams.SendTordDeleteEtra(db, oc.LeNo);
db.SaveChanges();
}
});
OrdersMiniload.ToList().ForEach(om =>
{
if (TransportOrderStatusGroups.Open.Contains(om.Status))
{
om.HandleFinished();
}
});
Status = TransportOrderStatus.Finished;
UpdateResources(TransportOrderStatus.Finished, null);
}
/// <summary>
/// Cancel this order, related OrdersMiniload, Orders Conveyor and clear the resources
/// </summary>
public void Cancel(string error, string info, bool cancelOrdersMiniload = true)
{
OrdersConveyor.ToList().ForEach(oc =>
{
if (TransportOrderStatusGroups.Open.Contains(oc.Status))
{
oc.Cancel(error);
}
});
if (cancelOrdersMiniload)
{
OrdersMiniload.ToList().ForEach(om =>
{
if (TransportOrderStatusGroups.Open.Contains(om.Status))
{
om.Cancel(error);
}
});
}
Status = TransportOrderStatus.Cancelled;
Error = error;
Info = info;
UpdateResources(TransportOrderStatus.Cancelled, null);
}
public OrdersMiniload GetPendingOrdersMiniload()
{
return OrdersMiniload?.SingleOrDefault(o => o.Status == TransportOrderStatus.Pending);
}
public OrdersMiniload GetOpenOrdersMiniload()
{
return OrdersMiniload?.FirstOrDefault(o => TransportOrderStatusGroups.Open.Contains(o.Status));
}
public OrdersMiniload GetActiveOrdersMiniload()
{
return OrdersMiniload?.SingleOrDefault(o => TransportOrderStatusGroups.Active.Contains(o.Status));
}
public OrdersConveyor GetActiveOrdersConveyor()
{
return OrdersConveyor?.SingleOrDefault(o => TransportOrderStatusGroups.Active.Contains(o.Status));
}
public void UpdateResources(TransportOrderStatus resourceListStatus, List<ResourceList> occupiedBySameLeButDifferentOrder)
{
//If a OrdersHost is Finished or Cancelled all ResourceList for this OH are affected, for other status only entries with same Destination
Resources
.Where(r => !TransportOrderStatusGroups.Complete.Contains(r.Status) && (r.Name == Destination || resourceListStatus == TransportOrderStatus.Finished || resourceListStatus == TransportOrderStatus.Cancelled))
.ToList()
.ForEach(r => r.Status = resourceListStatus);
occupiedBySameLeButDifferentOrder?.ForEach(r => r.Status = TransportOrderStatus.Cancelled);
}
public void UpdateSpecificResource(TransportOrderStatus resourceListStatus, string destination)
{
ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == destination);
if (oldResource == null)
{
return;
}
else
{
oldResource.Status = resourceListStatus;
}
}
public OrdersHost ReplaceNextEmptyLe(Le le)
{
if (!LeType.EmptyLeTypeNames.Contains(Le.Type))
{
throw new ArgumentException($"Reassigning an LE is only allowed to replace any of {nameof(LeType.EmptyLeTypeNames)}. Current LE: {le.LeNo}");
}
Le = le;
ResourceList resource = Resources.SingleOrDefault(r => r.Name == Destination);
if (resource != null)
{
resource.Le = le;
}
return this;
}
public override string ToString()
{
return $"{nameof(OrdersHost)}[{nameof(Id)}: {Id}, {nameof(LeNo)}: {LeNo}, {nameof(Type)}: {Type}, {nameof(Source)}: {Source}, {nameof(Destination)}: {Destination}, {nameof(Status)}: {Status}, {nameof(IdOrderWmsHead)}: {IdOrderWmsHead}, {nameof(IdOrderWms)}: {IdOrderWms}, {nameof(IdOrderWmsPos)}: {IdOrderWmsPos}]";
}
/// <summary>
/// Set the status to InDestinationZone regardless of the previous status. This is ONLY to be used for creating an orders host dummy when we get a no read on the outputs. Otherwise we cannot send an UnloadHuToAgv telegram to unload the board to an AGV.
/// </summary>
/// <returns></returns>
public OrdersHost ForceSetStatusInDestinationZone()
{
Status = TransportOrderStatus.InDestinationZone;
return this;
}
public OrdersHost ForceSetStatusInSequencer()
{
Status = TransportOrderStatus.InSequencer;
return this;
}
public OrdersHost ForceSetStatusInProgress()
{
Status = TransportOrderStatus.InProgress;
return this;
}
public OrdersHost ForceSetStatusTransmitted()
{
Status = TransportOrderStatus.Transmitted;
return this;
}
public bool IsSequencerCancelOrder()
{
return Source.Contains(WcsNames.SEQ) && Destination == Common.Constants.MfcAllDestinations.StorageLoop2;
}
}

View File

@@ -0,0 +1,52 @@
using Gebhardt.DbAccess.Base.Configuration;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using static Gebhardt.StoreWare.WcsWms.Constants.ModelConstants;
namespace Gebhardt.StoreWare.Wcs.Common.DbAccess.Configuration;
public class OrdersHostEntityConfiguration : BaseEntityConfiguration<OrdersHost>
{
public override void Configure(EntityTypeBuilder<OrdersHost> builder)
{
base.Configure(builder);
builder.HasIndex(e => new { e.LeNo, e.Status }, "I1_OrdersHost");
builder.HasIndex(e => new { e.Destination, e.Status }, "I2_OrdersHost");
builder.HasIndex(e => new { e.Type, e.Status }, "I3_OrdersHost");
builder.Property(e => e.Destination).IsRequired().HasMaxLength(OrderSourceDestLength);
builder.Property(e => e.Error).HasMaxLength(ErrorTextLength);
builder.Property(e => e.IdOrderWms);
builder.Property(e => e.IdOrderWmsHead);
builder.Property(e => e.IdOrderWmsPos);
builder.Property(e => e.DepartureFlag);
builder.Property(e => e.DepartureLocation);
builder.Property(e => e.Info).HasMaxLength(InfoTextLength);
builder.Property(e => e.Source).IsRequired().HasMaxLength(OrderSourceDestLength);
builder.Property(e => e.Status)
.IsRequired();
builder.Property(e => e.Type)
.IsRequired();
builder.Property(e => e.LeNo).IsRequired().HasMaxLength(LeNoLength);
builder.HasOne(e => e.Le).WithMany().HasForeignKey(e => e.LeNo).OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.Resources).WithOne(e => e.OrdersHost).HasForeignKey(e => e.OrdersHostId).IsRequired();
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.StoreWare.Wcs.Common.Dao;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using static Gebhardt.StoreWare.Wcs.Common.Constants;
namespace Gebhardt.StoreWare.Wcs.Common.DbAccess.Queries
{
public static class OrdersHostQueries
{
public static IQueryable<OrdersHost> ByStatus(this IQueryable<OrdersHost> entity, params TransportOrderStatus[] states)
{
return entity.Where(o => states.Contains(o.Status));
}
public static IQueryable<OrdersHost> ByType(this IQueryable<OrdersHost> entity, params TransportOrderType[] types)
{
return entity.Where(o => types.Contains(o.Type));
}
public static IQueryable<OrdersHost> ByLeNo(this IQueryable<OrdersHost> entity, string leNo)
{
return entity.Where(o => o.LeNo == leNo);
}
/// <summary>
/// select entities with one of the given destinations
/// </summary>
/// <returns></returns>
public static IQueryable<OrdersHost> ByDestination(this IQueryable<OrdersHost> entity, IEnumerable<string> destinations)
{
return entity.Where(o => destinations.Contains(o.Destination));
}
/// <summary>
/// select entities which hs not one of the given destinations
/// </summary>
/// <returns></returns>
public static IQueryable<OrdersHost> ExcludeDestination(this IQueryable<OrdersHost> entity, IEnumerable<string> destinations)
{
return entity.Where(o => !destinations.Contains(o.Destination));
}
/// <summary>
/// select entities with the given destination
/// </summary>
/// <returns></returns>
public static IQueryable<OrdersHost> ByDestination(this IQueryable<OrdersHost> entity, string destination)
{
return entity.Where(o => destination == o.Destination);
}
/// <summary>
/// searches for OH
/// </summary>
/// <param name="entity"></param>
/// <param name="leNo"></param>
/// <returns></returns>
public static OrdersHost ActiveByLeNo(this IQueryable<OrdersHost> entity, string leNo)
{
return entity
.ByLeNo(leNo)
.SingleOrDefault(o => o.Status == TransportOrderStatus.InProgress || o.Status == TransportOrderStatus.InDestinationZone || o.Status == TransportOrderStatus.InSequencer);
}
public static IQueryable<OrdersHost> AllActiveByLeNo(this IQueryable<OrdersHost> entity, string leNo)
{
return entity
.ByLeNo(leNo)
.Where(o => o.Status == TransportOrderStatus.InProgress || o.Status == TransportOrderStatus.InDestinationZone || o.Status == TransportOrderStatus.InSequencer);
}
/// <summary>
/// find - if exists open orders host entries (Status: Initial, InProgress, InDestinationZone, Pending)
/// </summary>
/// <param name="entity"></param>
/// <param name="leNo">the le number</param>
/// <returns>null or an open orders host</returns>
public static IQueryable<OrdersHost> OpenByLeNo(this IQueryable<OrdersHost> entity, string leNo)
{
return entity
.Where(o => o.LeNo == leNo)
.Open()
//Better include this here in case one of the initial orders is started by that (done in many places)
.ApplyWmsOrdering();
}
/// <summary>
/// find - open orders host entries (Status: Initial, InProgress, InDestinationZone, Pending)
/// </summary>
/// <param name="entity"></param>
/// <returns>null or an open orders host</returns>
public static IQueryable<OrdersHost> Open(this IQueryable<OrdersHost> entity)
{
return entity.Where(o => TransportOrderStatusGroups.Open.Contains(o.Status));
}
public static IQueryable<OrdersHost> Active(this IQueryable<OrdersHost> entity)
{
return entity.Where(o => TransportOrderStatusGroups.Active.Contains(o.Status));
}
public static IQueryable<OrdersHost> ActivePendingOrInSequecer(this IQueryable<OrdersHost> entity)
{
return entity.Where(o => TransportOrderStatusGroups.Active.Contains(o.Status) || o.Status == TransportOrderStatus.InSequencer || o.Status == TransportOrderStatus.Pending);
}
/// <summary>
/// Get all destinations for which there are orders host existing in any state
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static List<string> GetAllDestinations(this IQueryable<OrdersHost> entity)
{
return entity.Select(oh => oh.Destination).Distinct().ToList();
}
/// <summary>
/// Get all destinations for which there are orders host existing in the given states
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static List<string> GetAllDestinations(this IQueryable<OrdersHost> entity, params TransportOrderStatus[] states)
{
return entity.Where(oh => states.Contains(oh.Status)).Select(oh => oh.Destination).Distinct().ToList();
}
/// <summary>
/// Get all destinations for the orders host (maybe precede by an "Open()"!)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static List<string> GetAllDestinations(this IQueryable<OrdersHost> entity, IEnumerable<string> destinations)
{
return entity.Where(oh => destinations.Contains(oh.Destination)).Select(oh => oh.Destination).Distinct().ToList();
}
public static IQueryable<OrderCountByDay> GetCountByDate(this IQueryable<OrdersHost> entity, TransportOrderType orderType, IEnumerable<DateTime> dates)
{
return entity.Where(oh => oh.Status == TransportOrderStatus.Finished && oh.Created.Date > dates.First().Date && oh.Type == orderType).GroupBy(oh => oh.Created.Date).OrderBy(g => g.Key).Select(r => new OrderCountByDay(r.Count(), r.Key));
}
public static IQueryable<OrderCountByDestination> GetCountByDestination(this IQueryable<OrdersHost> entity)
{
// This SQL is translated into a Lambda expression
DateTime today = DateTime.Now.Date;
DateTime yesterday = today.AddDays(-1);
return entity.Where(oh => oh.Status == TransportOrderStatus.Finished && oh.Created.Date >= yesterday && oh.Type == TransportOrderType.TransportHost).GroupBy(oh => oh.Destination).OrderBy(g => g.Key).Select(r => new OrderCountByDestination(
r.Key,
r.Sum(d => d.Created.Date == today ? 1 : 0),
r.Sum(d => d.Created.Date == yesterday ? 1 : 0)));
}
public static IQueryable<OrdersHost> ExcludeNextEmpty(this IQueryable<OrdersHost> entity)
{
return entity.Where(o => o.LeNo != LeTypeName.NextEmptyMiniloadSmall.ToString() && o.LeNo != LeTypeName.NextEmptyMiniloadBig.ToString());
}
public static IQueryable<OrdersHost> OnlyNextEmpty(this IQueryable<OrdersHost> entity)
{
return entity.Where(o => o.LeNo == LeTypeName.NextEmptyMiniloadSmall.ToString() || o.LeNo == LeTypeName.NextEmptyMiniloadBig.ToString());
}
/// <summary>
/// Special WMS ordering that takes the common Sequencer Retrieval Time into account
/// </summary>
/// <param name="entity"></param>
/// <param name="db"></param>
/// <returns></returns>
public static IOrderedQueryable<OrdersHost> ApplyWmsOrderingSequencerRetrievalTime(this IQueryable<OrdersHost> entity,
IWcsDbContext db)
{
var query = entity
.GroupJoin(
db.OrdersHost
.GroupBy(xx => xx.IdOrderWmsHead)
.Select(g => new
{
IdOrderWmsHead = g.Key,
SequencerRetrievalTime = g.Min(y => y.SequencerRetrievalTime)
}),
o => o.IdOrderWmsHead,
m => m.IdOrderWmsHead,
(o, m) => new { o, m }
)
.SelectMany(x => x.m.DefaultIfEmpty(), (x, m) => new { x.o, m })
.OrderByDescending(x => x.o.IsStolen)
.ThenByDescending(x => x.o.IsDirectPicking)
.ThenByDescending(x => x.m.SequencerRetrievalTime != null)
.ThenBy(x => x.m.SequencerRetrievalTime)
.ThenBy(x => x.o.Created)
.Select(x => x.o);
return (IOrderedQueryable<OrdersHost>) query;
}
public static IOrderedQueryable<OrdersHost> ApplyWmsOrdering(this IQueryable<OrdersHost> entity)
{
// Only order by Created, as WMS anyways only releases a limited number of orders. This saves us from running into dealocks.
//Sequencer RetrievalTime is set when box is retrieved from sequencer.
return entity
.OrderByDescending(o => o.IsStolen)
.ThenByDescending(o => o.IsDirectPicking)
.ThenByDescending(o => o.SequencerRetrievalTime != null)
.ThenBy(o => o.SequencerRetrievalTime)
.ThenBy(o => o.Created);
}
/// <summary>
/// Exclude all OHs where the LE is located in the sequencer, unless this order is a cancel order
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static IQueryable<OrdersHost> ExcludeOrdersInSequencer(this IQueryable<OrdersHost> entity)
{
//The comented orders are started in Seq_Dispo
return entity.Where(o => !(o.Le.Status == LeStatus.InStorage && o.Le.Aisle.Type == AisleType.Sequencer) /*|| (o.Source.Contains(WcsNames.SEQ) && o.Destination == MfcAllDestinations.StorageLoop2)*/);
}
/// <summary>
/// Filter by sequenceorders that has been cancelled
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static IQueryable<OrdersHost> ByCancelledSequencerOrder(this IQueryable<OrdersHost> entity)
{
return entity.Where(o => o.Source.Contains(WcsNames.SEQ) && o.Destination == MfcAllDestinations.StorageLoop2);
}
/// <summary>
/// Filters by all orders that have been marked for departure by HostBooking (DepartureNotificationHandler)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static IQueryable<OrdersHost> IsDepartureReady(this IQueryable<OrdersHost> entity) {
return entity.Where(o => o.DepartureLocation != null && o.DepartureFlag == true);
}
}
}

View File

@@ -1,108 +0,0 @@
# Projektdokumentation: Optimierung der Auftragsverarbeitung eines WCS
**Prüfling:** Kai Kröger
**Beruf:** Fachinformatiker für Anwendungsentwicklung
**Betrieb:** Gebhardt Fördertechnik GmbH
**Zeitraum:** 04.05.2026 - 25.05.2026 (80 Stunden)
---
## 1. Einleitung
### 1.1 Das Unternehmen
Die Gebhardt Fördertechnik GmbH ist ein international agierendes Familienunternehmen mit Sitz in Sinsheim, das sich auf die Entwicklung und Herstellung modularer Intralogistiksysteme spezialisiert hat. Mit über 70 Jahren Erfahrung bietet Gebhardt ein breites Spektrum an Lösungen von der Förder- und Lagertechnik bis hin zu komplexen Warehouse Control Systemen (WCS). Ein zentraler Baustein des Portfolios ist die Softwareplattform GEBHARDT StoreWare, die Materialfluss- (MFS), Lagerverwaltungs- (LVS) und Visualisierungssysteme integriert, um eine effiziente Steuerung und Kontrolle der Warenströme in automatisierten Lagern zu ermöglichen.
### 1.2 Projektziel
Das Ziel des Projekts ist die Optimierung und Stabilisierung der Auftragsverarbeitung innerhalb des WCS. Aktuell führt die dezentrale Logik beim Starten von Transportaufträgen (OrdersHost) zu Race Conditions, da mehrere parallel laufende Prozesse simultan versuchen, denselben Auftrag zu initiieren. Dies resultiert in inkonsistenten Datenbeständen und nicht abschließbaren Auftragsleichen. Im Rahmen dieser Arbeit wird die Start-Logik im Prozess ConveyorDispo zentralisiert. Durch gezieltes Refactoring und die Implementierung thread-sicherer Mechanismen wird sichergestellt, dass Aufträge exklusiv und prozesssicher verarbeitet werden, was die Fehleranfälligkeit der Anlage signifikant reduziert.
### 1.3 Projektumfeld & Schnittstellen
*Einordnung des WCS in die Systemlandschaft (SAP, Host-Systeme). Spezifikation der Schnittstellen (TCP/IP, IDocs).*
---
## 2. Projektplanung (Bewertung: Ressourcen & Ablauf)
### 2.1 Projektphasen & Zeitplanung
*Vergleich von Soll-Planung (Antrag) und grober Zeitübersicht.*
### 2.2 Ressourcenplanung
*Hardware, Software-Stack (C# .NET), Personal.*
### 2.3 Kostenplanung
*Kalkulation der Personalkosten für 80 Stunden und potenzielle Sachkosten.*
---
## 3. Analyse & Konzept (Bewertung: Ausgangssituation)
### 3.1 Ist-Analyse
Die technische Ist-Analyse der Quellcode-Basis von GEBHARDT StoreWare identifizierte zwei Haupttypen von Race Conditions, die durch die dezentrale Logik-Hoheit verursacht werden:
#### A. Technische Race Condition (Status-Konflikt bei Neuanlage)
Trifft eine `DepartureNotification` für eine unbekannte Ladeeinheit (LE) ein, erzeugt der `DepartureNotificationHandler` im Prozess `HostBooking` einen neuen `OrdersHost`-Eintrag im Status `Initial`.
* **Fehlerablauf:** Noch während der Handler-Durchlauf aktiv ist, sendet er ein Start-Telegramm an die SPS. Parallel dazu identifiziert der zyklische Worker `StartInitialOrdersHost` (Teil des Prozesses `ConveyorDispo`) den neuen `Initial`-Eintrag und initiiert seinerseits einen Startvorgang.
* **Folge:**
- **Telegramm-Kollision:** Die SPS erhält zeitnah zwei Start-Befehle für dieselbe LE, was zu Fehlsteuerungen führt.
- **Datenbank-Inkonsistenz:** Beide Prozesse versuchen simultan den Datensatz zu aktualisieren, was zu `DbUpdateConcurrencyExceptions` führt. Der Auftrag verbleibt oft in einem inkonsistenten Status ("Auftragsleiche").
#### B. Architektonische Race Condition (Umgehung der Ressourcenprüfung)
Befindet sich ein Behälter im Status `InDestinationZone` (kurz vor dem Ziel), greift eine Sonderlogik für Sequencer-Anbindungen.
* **Fehlerablauf:** Der Handler im `HostBooking` ändert eigenmächtig das Ziel auf den `ConnectedSequencer` und erzwingt mittels `ForceSetStatusInProgress` den physischen Start.
* **Folge:**
- **Kapazitäts-Blindheit:** Da dieser Start am zentralen `ConveyorDispo` vorbeigeschleust wird, findet keine Prüfung der Sequencer-Auslastung (Throttling) statt. Die physische Kapazität des Sequencers wird überschritten, was zu Anlagenstaus und Deadlocks im Materialfluss führt.
### 3.2 Soll-Konzept
Das Ziel des Soll-Konzepts ist die Überführung der dezentrale Start-Logik in eine monolithische Architektur innerhalb des `ConveyorDispo`.
**Funktionale Anforderungen:**
- **Exklusivität:** Nur der Prozess `ConveyorDispo` darf den Status eines `OrdersHost`-Auftrags von `Initial` (oder einem neuen Zwischenstatus wie `ReadyToStart`) auf `InProgress` ändern und das entsprechende Start-Telegramm senden.
- **Entkopplung:** Der `DepartureNotificationHandler` wird zum reinen "Event-Melder" refactored. Er aktualisiert den physischen Ort der LE und setzt den Auftragsstatus lediglich auf `Initial`, um den Startwunsch für den `ConveyorDispo` zu signalisieren.
- **Zentralisierung:** Alle Start-Entscheidungen (inkl. Sequencer-Anbindung und Ressourcenprüfung) werden im `OrderManager` des `ConveyorDispo` konsolidiert.
**Nicht-funktionale Anforderungen:**
- **Datenintegrität:** Eliminierung von `ConcurrencyExceptions` durch klare Zuständigkeitstrennung.
- **Wartbarkeit:** Reduzierung von redundantem Code (DRY) und Beseitigung von "Sonderlocken" in den Handlern.
### 3.3 Wirtschaftlichkeitsbetrachtung
*ROI-Analyse: Wie viel kostet der Fehler aktuell? Wie schnell amortisiert sich die Entwicklung?*
---
## 4. Design & Architektur (Bewertung: Durchführung/Entscheidungen)
### 4.1 Technische Entscheidungen
*Begründung der Wahl von C# und spezifischen Entwurfsmustern (z.B. Singleton, Lock-Mechanismen).*
### 4.2 Systemarchitektur
*Darstellung der neuen Prozesssteuerung. Wie wird die Zentralisierung technisch umgesetzt?*
### 4.3 Schnittstellen-Design
*Detaillierte Beschreibung der Kommunikationsprotokolle (Auflagen-Erfüllung).*
---
## 5. Realisierung (Bewertung: Durchführung/Auftragsbearbeitung)
### 5.1 Implementierung
*Wichtige Code-Ausschnitte (Fokus auf Thread-Safety und Logik-Zentralisierung).*
### 5.2 Qualitätssicherung & Testen
*Testplan, Unit-Tests, Integrationstests im WCS-Emulator.*
### 5.3 Abweichungen vom Projektplan
*Dokumentation und Begründung von Änderungen während der Durchführung.*
---
## 6. Projektabschluss (Bewertung: Auftragsergebnisse)
### 6.1 Soll-Ist-Vergleich
*Überprüfung, ob alle Ziele des Soll-Konzepts erreicht wurden.*
### 6.2 Abnahme & Übergabe
*Protokoll der Übergabe an die Fachabteilung/QS.*
### 6.3 Fazit & Ausblick
*Persönliches Resümee und mögliche Erweiterungen.*
---
## Anhang (Zählt nicht zu den 15 Seiten)
- Kundendokumentation (Bedienungsanleitung/API-Doku)
- Quellcode (Relevante Ausschnitte)
- Abnahmeprotokoll
- Glossar & Quellenverzeichnis

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

Some files were not shown because too many files have changed in this diff Show More