using Comal.Classes; using InABox.Core; using InABox.Database; using System.Linq; using System; using MathNet.Numerics; namespace Comal.Stores; using HoldingDictionary = Dictionary<(Guid product, Guid style, Guid location, Guid job, StockDimensions dimensions), StockHolding>; public class StockHoldingStore : BaseStore { public enum Action { Increase, Decrease } public static StockMovement[] LoadMovementData(IStore store, Guid[] ids) { return store.Provider.Query( new Filter(x => x.ID).InList(ids), Columns.None().Add(x => x.ID) .Add(x => x.Location.ID) .Add(x => x.Product.ID) .Add(x => x.Style.ID) .Add(x => x.Job.ID) .Add(x => x.Dimensions.Unit.ID) .Add(x => x.Dimensions.Quantity) .Add(x => x.Dimensions.Height) .Add(x => x.Dimensions.Width) .Add(x => x.Dimensions.Length) .Add(x => x.Dimensions.Weight) .Add(x => x.Dimensions.UnitSize) .Add(x => x.Dimensions.Value) .Add(x => x.JobRequisitionItem.ID) .Add(x => x.Units) .Add(x => x.Cost) ).ToArray(); } public static HoldingDictionary LoadStockHoldings(IStore store, StockMovement[] mvts, HoldingDictionary? holdings = null) { return StockHoldingExtensions.LoadStockHoldings( mvts, Columns.None() .Add(x => x.Units) .Add(x => x.Qty) .Add(x => x.Value) .Add(x => x.Available) .Add(x => x.Weight) .Add(x => x.AverageValue), holdings, store.QueryProvider()); } public static void ModifyHoldings(StockMovement[] mvts, HoldingDictionary holdings, Action action) { foreach(var mvt in mvts) { var key = (mvt.Product.ID, mvt.Style.ID, mvt.Location.ID, mvt.Job.ID, mvt.Dimensions); var holding = holdings.GetValueOrDefault(key); if(holding is null) { holding = new(); holding.Location.ID = mvt.Location.ID; holding.Product.ID = mvt.Product.ID; holding.Style.ID = mvt.Style.ID; holding.Job.ID = mvt.Job.ID; holding.Dimensions.CopyFrom(mvt.Dimensions); holdings[key] = holding; } double multiplier = action == Action.Increase ? 1F : -1F; holding.Units += (multiplier * mvt.Units); holding.Qty += (multiplier * mvt.Units * mvt.Dimensions.Value); holding.Value += (multiplier * mvt.Units * mvt.Cost); holding.Available += (multiplier * (mvt.JobRequisitionItem.ID == Guid.Empty ? mvt.Units : 0.0)); holding.Weight = holding.Qty * holding.Dimensions.Weight; holding.AverageValue = holding.Units != 0 ? holding.Value / holding.Units : 0.0F; } } public static void SaveHoldings(IStore store, HoldingDictionary holdings) { var holdingStore = store.FindSubStore(); holdingStore.Delete( holdings.Values.Where(x => x.ID != Guid.Empty && x.Units.IsEffectivelyEqual(0.0) && x.Available.IsEffectivelyEqual(0.0)), ""); holdingStore.Save( holdings.Values.Where(x => x.IsChanged() && (!x.Units.IsEffectivelyEqual(0.0) || !x.Available.IsEffectivelyEqual(0.0))), ""); } public static void UpdateStockHoldings(IStore store, Guid[] ids, Action action) { var movements = LoadMovementData(store, ids); var holdings = LoadStockHoldings(store, movements); ModifyHoldings(movements, holdings, action); SaveHoldings(store, holdings); } /// /// Maintains the Stock Holding Table when manipulating Stock Movements /// We only accept an ID, because the rest of the movement is pulled from the database /// (slower, but more reliable) /// /// The id of the Stock Movement to query /// The action to perform (increase / decrease) public static void UpdateStockHolding(IStore store, Guid id, Action action) { var movement = store.Provider.Query( new Filter(x => x.ID).IsEqualTo(id), Columns.None().Add(x => x.ID) .Add(x => x.Location.ID) .Add(x => x.Product.ID) .Add(x => x.Style.ID) .Add(x => x.Job.ID) .Add(x => x.Dimensions.Unit.ID) .Add(x => x.Dimensions.Quantity) .Add(x => x.Dimensions.Height) .Add(x => x.Dimensions.Width) .Add(x => x.Dimensions.Length) .Add(x => x.Dimensions.Weight) .Add(x => x.Dimensions.UnitSize) .Add(x => x.Dimensions.Value) .Add(x => x.JobRequisitionItem.ID) .Add(x => x.Units) .Add(x => x.Cost) ).Rows .FirstOrDefault()? .ToObject(); if (movement == null) return; var holding = store.Provider.Query(new Filter(x => x.Product.ID).IsEqualTo(movement.Product.ID) .And(x => x.Location.ID).IsEqualTo(movement.Location.ID) .And(x => x.Style.ID).IsEqualTo(movement.Style.ID) .And(x => x.Job.ID).IsEqualTo(movement.Job.ID) .And(x => x.Dimensions).DimensionEquals(movement.Dimensions), Columns.None().Add(x => x.ID) .Add(x => x.Units) .Add(x => x.Qty) .Add(x => x.Value) .Add(x => x.Available) ).Rows .FirstOrDefault()? .ToObject(); if (holding == null) { holding = new(); holding.Location.ID = movement.Location.ID; holding.Product.ID = movement.Product.ID; holding.Style.ID = movement.Style.ID; holding.Job.ID = movement.Job.ID; holding.Dimensions.CopyFrom(movement.Dimensions); } double multiplier = action == Action.Increase ? 1F : -1F; holding.Units += (multiplier * movement.Units); holding.Qty += (multiplier * movement.Units * movement.Dimensions.Value); holding.Value += (multiplier * movement.Units * movement.Cost); holding.Available += (multiplier * (movement.JobRequisitionItem.ID == Guid.Empty ? movement.Units : 0.0)); holding.Weight = holding.Qty * holding.Dimensions.Weight; holding.AverageValue = holding.Units != 0 ? holding.Value / holding.Units : 0.0F; // Automagically clean up empty holdings if (holding.Units.IsEffectivelyEqual(0.0) && holding.Available.IsEffectivelyEqual(0.0)) { if (holding.ID != Guid.Empty) DbFactory.NewProvider(Logger.Main).Delete(holding, ""); } else DbFactory.NewProvider(Logger.Main).Save(holding); } }