Sfoglia il codice sorgente

Converted StockLocation.LastStockTake to a manual field and added StockTakeStatus enum
Added StockHolding.LastStockTake Aggregate

frogsoftware 1 anno fa
parent
commit
46b9599941

+ 0 - 388
prs.classes/Entities/Stock/StockHolding.cs

@@ -1,388 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using InABox.Core;
-using PRSClasses;
-
-namespace Comal.Classes
-{
-    
-    // public class StockHoldingUnitAggregate : CoreAggregate<StockHolding, StockMovement, double>
-    // {
-    //     public override Expression<Func<StockMovement, double>> Aggregate => x => x.Units;
-    //
-    //     public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
-    //         new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
-    //         {
-    //             { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
-    //             { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
-    //             { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
-    //             { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
-    //             { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
-    //             { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
-    //             { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
-    //             { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
-    //             { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
-    //             { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
-    //         };
-    //
-    //     public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    // }
-    //
-    // public class StockHoldingValueAggregate : CoreAggregate<StockHolding, StockMovement, double>
-    // {
-    //     public override Expression<Func<StockMovement, double>> Aggregate => x => x.Value;
-    //
-    //     public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
-    //         new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
-    //         {
-    //             { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
-    //             { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
-    //             { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
-    //             { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
-    //             { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
-    //             { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
-    //             { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
-    //             { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
-    //             { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
-    //             { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
-    //         };
-    //
-    //     public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    // }
-    //
-    // public class StockHoldingAverageValueFormula : IFormula<StockHolding, double>
-    // {
-    //     public Expression<Func<StockHolding, double>> Value => x => x.Value;
-    //
-    //     public Expression<Func<StockHolding, double>>[] Modifiers => new Expression<Func<StockHolding, double>>[] { x => x.Units };
-    //
-    //     public FormulaOperator Operator => FormulaOperator.Divide;
-    //     
-    //     public FormulaType Type => FormulaType.Virtual;
-    // }
-    //
-    // public class StockHoldingQuantityAggregate : CoreAggregate<StockHolding, StockMovement, double>
-    // {
-    //     public override Expression<Func<StockMovement, double>> Aggregate => x => x.Qty;
-    //
-    //     public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
-    //         new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
-    //         {
-    //             { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
-    //             { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
-    //             { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
-    //             { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
-    //             { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
-    //             { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
-    //             { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
-    //             { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
-    //             { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
-    //             { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
-    //         };
-    //
-    //     public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    // }
-
-
-    // public class StockHoldingAvailableAggregate : CoreAggregate<StockHolding, StockMovement, double>
-    // {
-    //     public override Expression<Func<StockMovement, double>> Aggregate => x => x.Units;
-    //
-    //     public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
-    //         new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
-    //         {
-    //             { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
-    //             { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
-    //             { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
-    //             { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
-    //             { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
-    //             { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
-    //             { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
-    //             { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
-    //             { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
-    //             { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
-    //         };
-    //
-    //     public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    //
-    //     public override Filter<StockMovement>? Filter => new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(Guid.Empty);
-    // }
-    
-    // public class StockHoldingWeightFormula : IFormula<StockHolding, double>
-    // {
-    //     public Expression<Func<StockHolding, double>> Value => x => x.Qty;
-    //
-    //     public Expression<Func<StockHolding, double>>[] Modifiers => new Expression<Func<StockHolding, double>>[] { x => x.Dimensions.Weight };
-    //
-    //     public FormulaOperator Operator => FormulaOperator.Multiply;
-    //     
-    //     public FormulaType Type => FormulaType.Virtual;
-    // }
-    //
-    // public class StockHoldingIsRemnantCondition : ICondition<StockHolding, double, object>
-    // {
-    //     public Expression<Func<StockHolding, double>> Left => x => x.Dimensions.Value;
-    //
-    //     public Condition Condition => Condition.LessThan;
-    //
-    //     public Expression<Func<StockHolding, double>> Right => x => x.Product.DefaultInstance.Dimensions.Value;
-    //
-    //     public Expression<Func<StockHolding, object>> True => x => true;
-    //
-    //     public Expression<Func<StockHolding, object>> False => x => null;
-    //     
-    //     public ConditionType Type => ConditionType.Virtual;
-    // }
-    
-    // public class StockHoldingUnionGenerator : AutoEntityUnionGenerator<IStockHolding>
-    // {
-    //     protected override void Configure()
-    //     {
-    //         AddTable<StockMovement>();
-    //     }
-    //
-    //     public override bool Distinct => true;
-    //
-    //     public override Column<IStockHolding>[] IDColumns => Columns;
-    //
-    //     public static Column<IStockHolding>[] Columns => new Column<IStockHolding>[]
-    //     {
-    //         new Column<IStockHolding>(x => x.Job.ID),
-    //         new Column<IStockHolding>(x => x.Location.ID),
-    //         new Column<IStockHolding>(x => x.Product.ID),
-    //         new Column<IStockHolding>(x => x.Style.ID),
-    //         new Column<IStockHolding>(x => x.Dimensions.Unit.ID),
-    //         new Column<IStockHolding>(x => x.Dimensions.Quantity),
-    //         new Column<IStockHolding>(x => x.Dimensions.Length),
-    //         new Column<IStockHolding>(x => x.Dimensions.Width),
-    //         new Column<IStockHolding>(x => x.Dimensions.Height),
-    //         new Column<IStockHolding>(x => x.Dimensions.Weight),
-    //     };
-    //
-    //     public static Filter<StockMovement>? GetFilter(IStockHolding holding)
-    //     {
-    //         var filter = new Filters<StockMovement>();
-    //
-    //         foreach(var column in Columns)
-    //         {
-    //             filter.Add(new Filter<StockMovement>(column.Cast<StockMovement>()).IsEqualTo(CoreUtils.GetPropertyValue(holding, column.Property)));
-    //         }
-    //
-    //         return filter.Combine();
-    //     }
-    // }
-    //
-    // [UserTracking(typeof(StockMovement))]
-    // [AutoEntity(typeof(StockHoldingUnionGenerator))]
-    // public class StockHoldingView : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>, 
-    //     IStockHolding, ILicense<WarehouseLicense>
-    // {
-    //     
-    //     public override ProductLink Product { get; set; }
-    //
-    //     public ProductStyleLink Style { get; set; }
-    //
-    //     public StockLocationLink Location { get; set; }
-    //
-    //     public JobLink Job { get; set; }
-    //
-    //     [RequiredColumn]
-    //     [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
-    //     public override StockDimensions Dimensions { get; set; }
-    //
-    //     [Condition(typeof(StockHoldingIsRemnantCondition))]
-    //     [NullEditor]
-    //     public bool IsRemnant { get; set; }
-    //     
-    //     [Aggregate(typeof(StockHoldingUnitAggregate))]
-    //     [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-    //     public double Units { get; set; }
-    //     
-    //     [Aggregate(typeof(StockHoldingQuantityAggregate))]
-    //     [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-    //     public double Qty { get; set; }
-    //     
-    //     [Formula(typeof(StockHoldingWeightFormula))]
-    //     [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-    //     public double Weight { get; set; }
-    //     
-    //     [Aggregate(typeof(StockHoldingValueAggregate))]
-    //     [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-    //     public double Value { get; set; }
-    //     
-    //     [Formula(typeof(StockHoldingAverageValueFormula))]
-    //     [DoubleEditor(Editable = Editable.Disabled)]
-    //     public double AverageValue { get; set; }
-    // }
-    
-    // FV 6/2/24: This used to be a view (see above), but we were running into performance issues trying to
-    // get aggregates fof stock locations (ie show me how many holdings in a given location was taking upwrds of
-    // 20 seconds.  Have moved update logic to StockHoldingStore / StockMovementStore so updates are a bit slower,
-    // but reads are now much faster.
-    [UserTracking(typeof(StockMovement))]
-    [Unrecoverable]
-    public class StockHolding : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>, 
-        IStockHolding, ILicense<WarehouseLicense>
-    {
-        
-        [Editable(Editable.Disabled)]
-        [EditorSequence(1)]
-        public StockLocationLink Location { get; set; }
-        
-        private class ProductLookupGenerator : LookupDefinitionGenerator<Product, StockHolding>
-        {
-            public override Filter<Product>? DefineFilter(StockHolding[] items)
-                => LookupFactory.DefineFilter<Product>().And(x => x.NonStock).IsEqualTo(false);
-        }
-        [Editable(Editable.Disabled)]
-        [EditorSequence(2)]
-        [LookupDefinition(typeof(ProductLookupGenerator))]
-        public override ProductLink Product { get; set; }
-        
-        [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
-        [Editable(Editable.Disabled)]
-        [EditorSequence(3)]
-        public override StockDimensions Dimensions { get; set; }
-
-        [Editable(Editable.Disabled)]
-        [EditorSequence(4)]
-        public ProductStyleLink Style { get; set; }
-        
-        [Editable(Editable.Disabled)]
-        [EditorSequence(4)]
-        public JobLink Job { get; set; }
-        
-        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-        [EditorSequence(5)]
-        public double Units { get; set; }
-        
-        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-        [EditorSequence(6)]
-        public double Qty { get; set; }
-        
-        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-        [EditorSequence(7)]
-        public double Weight { get; set; }
-        
-        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-        [EditorSequence(8)]
-        public double Value { get; set; }
-        
-        [DoubleEditor(Editable = Editable.Disabled)]
-        [EditorSequence(9)]
-        public double AverageValue { get; set; }
-        
-        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
-        [EditorSequence(10)]
-        public double Available { get; set; }
-        
-        public static Column<IStockHolding>[] Columns => new Column<IStockHolding>[]
-        {
-            new Column<IStockHolding>(x => x.Job.ID),
-            new Column<IStockHolding>(x => x.Location.ID),
-            new Column<IStockHolding>(x => x.Product.ID),
-            new Column<IStockHolding>(x => x.Style.ID),
-            new Column<IStockHolding>(x => x.Dimensions.Unit.ID),
-            new Column<IStockHolding>(x => x.Dimensions.Quantity),
-            new Column<IStockHolding>(x => x.Dimensions.Length),
-            new Column<IStockHolding>(x => x.Dimensions.Width),
-            new Column<IStockHolding>(x => x.Dimensions.Height),
-            new Column<IStockHolding>(x => x.Dimensions.Weight),
-        };
-        
-        public static Filter<StockMovement>? GetFilter(IStockHolding holding)
-        {
-            var filter = new Filters<StockMovement>();
-        
-            foreach(var column in Columns)
-            {
-                filter.Add(new Filter<StockMovement>(column.Cast<StockMovement>()).IsEqualTo(CoreUtils.GetPropertyValue(holding, column.Property)));
-            }
-        
-            return filter.Combine();
-        }
-    }
-
-    public static class StockHoldingExtensions
-    {
-        /// <summary>
-        /// Create a new stock movement from an <see cref="IStockHolding"/>, copying across the "key" properties;
-        /// that is, the job, product, style, location and dimensions.
-        /// </summary>
-        /// <param name="holding"></param>
-        /// <returns></returns>
-        public static StockMovement CreateMovement(this IStockHolding holding)
-        {
-            var movement = new StockMovement();
-            movement.Job.ID = holding.Job.ID;
-            movement.Job.Synchronise(holding.Job);
-            movement.Product.ID = holding.Product.ID;
-            movement.Product.Synchronise(holding.Product);
-            movement.Style.ID = holding.Style.ID;
-            movement.Style.Synchronise(holding.Style);
-            movement.Location.ID = holding.Location.ID;
-            movement.Location.Synchronise(holding.Location);
-            movement.Dimensions.CopyFrom(holding.Dimensions);
-            return movement;
-        }
-
-        public static IEnumerable<StockHolding> GroupMovements(IEnumerable<StockMovement> movements)
-        {
-            var grouped = new List<StockHolding>();
-
-            var toGroup = movements.AsList();
-            while (toGroup.Count > 0)
-            {
-                var first = toGroup.First();
-                var selected = toGroup.Where(x => x.IsEqualTo(first)).ToList();
-
-                var holding = grouped.FirstOrDefault(x => x.IsEqualTo(first));
-                if (holding == null)
-                {
-                    holding = new StockHolding();
-                    holding.Location.ID = first.Location.ID;
-                    holding.Product.ID = first.Product.ID;
-                    holding.Style.ID = first.Style.ID;
-                    holding.Job.ID = first.Job.ID;
-                    holding.Dimensions.CopyFrom(first.Dimensions);
-                }
-                holding.Recalculate(selected);
-
-                toGroup.RemoveAll(x => selected.Any(s => s.ID == x.ID));
-            }
-            return grouped;
-        }
-
-        public static bool IsEqualTo(this IStockHolding h1, IStockHolding h2)
-        {
-            return h1.Product.ID == h2.Product.ID
-                && h1.Location.ID == h2.Location.ID
-                && h1.Job.ID == h2.Job.ID
-                && h1.Style.ID == h2.Style.ID
-                && h1.Dimensions.Unit.ID == h2.Dimensions.Unit.ID
-                && h1.Dimensions.Length.IsEffectivelyEqual(h2.Dimensions.Length)
-                && h1.Dimensions.Width.IsEffectivelyEqual(h2.Dimensions.Width)
-                && h1.Dimensions.Height.IsEffectivelyEqual(h2.Dimensions.Height)
-                && h1.Dimensions.Quantity.IsEffectivelyEqual(h2.Dimensions.Quantity)
-                && h1.Dimensions.Weight.IsEffectivelyEqual(h2.Dimensions.Weight);
-        }
-
-        public static void Recalculate(this StockHolding holding, IEnumerable<StockMovement> movements)
-        {
-            movements = movements.AsIList();
-            var units = movements.Sum(x => x.Units);
-            var cost = movements.Select(x => x.Units * x.Cost).Sum();
-            var available = movements.Where(x => x.JobRequisitionItem.ID == Guid.Empty).Sum(x => x.Units);
-            holding.Units = units;
-            holding.Available = available;
-            holding.Qty = movements.Sum(x => x.Units * x.Dimensions.Value);
-            holding.Value = cost;
-
-            holding.AverageValue = units.IsEffectivelyEqual(0.0F) ? 0.0d : cost / units;
-            holding.Weight = holding.Qty * holding.Dimensions.Weight;
-        }
-    }
-    
-}

+ 0 - 0
prs.classes/Entities/Stock/IStockHolding.cs → prs.classes/Entities/Stock/StockHolding/IStockHolding.cs


+ 203 - 0
prs.classes/Entities/Stock/StockHolding/StockHolding.cs

@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using InABox.Core;
+using PRSClasses;
+
+namespace Comal.Classes
+{
+
+    
+    public class StockHoldingLastStocktake : CoreAggregate<StockHolding, StockMovement, DateTime>
+    {
+        public override Expression<Func<StockMovement, DateTime>> Aggregate => x => x.Date;
+
+        public override Filter<StockMovement> Filter => new Filter<StockMovement>(x => x.Type)
+            .IsEqualTo(StockMovementType.StockTake);
+
+        public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
+            new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
+            {
+                { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
+                { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
+                { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
+                { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
+                { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
+                { StockMovement => StockMovement.Dimensions.UnitSize, StockHolding => StockHolding.Dimensions.UnitSize },
+                
+            };
+
+        public override AggregateCalculation Calculation => AggregateCalculation.Maximum;
+    }
+    
+    [UserTracking(typeof(StockMovement))]
+    [Unrecoverable]
+    public class StockHolding : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>, 
+        IStockHolding, ILicense<WarehouseLicense>
+    {
+        
+        [Editable(Editable.Disabled)]
+        [EditorSequence(1)]
+        public StockLocationLink Location { get; set; }
+        
+        private class ProductLookupGenerator : LookupDefinitionGenerator<Product, StockHolding>
+        {
+            public override Filter<Product>? DefineFilter(StockHolding[] items)
+                => LookupFactory.DefineFilter<Product>().And(x => x.NonStock).IsEqualTo(false);
+        }
+        [Editable(Editable.Disabled)]
+        [EditorSequence(2)]
+        [LookupDefinition(typeof(ProductLookupGenerator))]
+        public override ProductLink Product { get; set; }
+        
+        [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
+        [Editable(Editable.Disabled)]
+        [EditorSequence(3)]
+        public override StockDimensions Dimensions { get; set; }
+
+        [Editable(Editable.Disabled)]
+        [EditorSequence(4)]
+        public ProductStyleLink Style { get; set; }
+        
+        [Editable(Editable.Disabled)]
+        [EditorSequence(4)]
+        public JobLink Job { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
+        [EditorSequence(5)]
+        public double Units { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
+        [EditorSequence(6)]
+        public double Qty { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
+        [EditorSequence(7)]
+        public double Weight { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
+        [EditorSequence(8)]
+        public double Value { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Disabled)]
+        [EditorSequence(9)]
+        public double AverageValue { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
+        [EditorSequence(10)]
+        public double Available { get; set; }
+        
+        [Aggregate(typeof(StockHoldingLastStocktake))]
+        [DateEditor(Editable = Editable.Disabled)]
+        [EditorSequence(11)]
+        public DateTime LastStockTake { get; set; }
+        
+        public static Column<IStockHolding>[] Columns => new Column<IStockHolding>[]
+        {
+            new Column<IStockHolding>(x => x.Job.ID),
+            new Column<IStockHolding>(x => x.Location.ID),
+            new Column<IStockHolding>(x => x.Product.ID),
+            new Column<IStockHolding>(x => x.Style.ID),
+            new Column<IStockHolding>(x => x.Dimensions.Unit.ID),
+            new Column<IStockHolding>(x => x.Dimensions.Quantity),
+            new Column<IStockHolding>(x => x.Dimensions.Length),
+            new Column<IStockHolding>(x => x.Dimensions.Width),
+            new Column<IStockHolding>(x => x.Dimensions.Height),
+            new Column<IStockHolding>(x => x.Dimensions.Weight),
+        };
+        
+        public static Filter<StockMovement>? GetFilter(IStockHolding holding)
+        {
+            var filter = new Filters<StockMovement>();
+        
+            foreach(var column in Columns)
+            {
+                filter.Add(new Filter<StockMovement>(column.Cast<StockMovement>()).IsEqualTo(CoreUtils.GetPropertyValue(holding, column.Property)));
+            }
+        
+            return filter.Combine();
+        }
+    }
+
+    public static class StockHoldingExtensions
+    {
+        /// <summary>
+        /// Create a new stock movement from an <see cref="IStockHolding"/>, copying across the "key" properties;
+        /// that is, the job, product, style, location and dimensions.
+        /// </summary>
+        /// <param name="holding"></param>
+        /// <returns></returns>
+        public static StockMovement CreateMovement(this IStockHolding holding)
+        {
+            var movement = new StockMovement();
+            movement.Job.ID = holding.Job.ID;
+            movement.Job.Synchronise(holding.Job);
+            movement.Product.ID = holding.Product.ID;
+            movement.Product.Synchronise(holding.Product);
+            movement.Style.ID = holding.Style.ID;
+            movement.Style.Synchronise(holding.Style);
+            movement.Location.ID = holding.Location.ID;
+            movement.Location.Synchronise(holding.Location);
+            movement.Dimensions.CopyFrom(holding.Dimensions);
+            return movement;
+        }
+
+        public static IEnumerable<StockHolding> GroupMovements(IEnumerable<StockMovement> movements)
+        {
+            var grouped = new List<StockHolding>();
+
+            var toGroup = movements.AsList();
+            while (toGroup.Count > 0)
+            {
+                var first = toGroup.First();
+                var selected = toGroup.Where(x => x.IsEqualTo(first)).ToList();
+
+                var holding = grouped.FirstOrDefault(x => x.IsEqualTo(first));
+                if (holding == null)
+                {
+                    holding = new StockHolding();
+                    holding.Location.ID = first.Location.ID;
+                    holding.Product.ID = first.Product.ID;
+                    holding.Style.ID = first.Style.ID;
+                    holding.Job.ID = first.Job.ID;
+                    holding.Dimensions.CopyFrom(first.Dimensions);
+                }
+                holding.Recalculate(selected);
+
+                toGroup.RemoveAll(x => selected.Any(s => s.ID == x.ID));
+            }
+            return grouped;
+        }
+
+        public static bool IsEqualTo(this IStockHolding h1, IStockHolding h2)
+        {
+            return h1.Product.ID == h2.Product.ID
+                && h1.Location.ID == h2.Location.ID
+                && h1.Job.ID == h2.Job.ID
+                && h1.Style.ID == h2.Style.ID
+                && h1.Dimensions.Unit.ID == h2.Dimensions.Unit.ID
+                && h1.Dimensions.Length.IsEffectivelyEqual(h2.Dimensions.Length)
+                && h1.Dimensions.Width.IsEffectivelyEqual(h2.Dimensions.Width)
+                && h1.Dimensions.Height.IsEffectivelyEqual(h2.Dimensions.Height)
+                && h1.Dimensions.Quantity.IsEffectivelyEqual(h2.Dimensions.Quantity)
+                && h1.Dimensions.Weight.IsEffectivelyEqual(h2.Dimensions.Weight);
+        }
+
+        public static void Recalculate(this StockHolding holding, IEnumerable<StockMovement> movements)
+        {
+            movements = movements.AsIList();
+            var units = movements.Sum(x => x.Units);
+            var cost = movements.Select(x => x.Units * x.Cost).Sum();
+            var available = movements.Where(x => x.JobRequisitionItem.ID == Guid.Empty).Sum(x => x.Units);
+            holding.Units = units;
+            holding.Available = available;
+            holding.Qty = movements.Sum(x => x.Units * x.Dimensions.Value);
+            holding.Value = cost;
+
+            holding.AverageValue = units.IsEffectivelyEqual(0.0F) ? 0.0d : cost / units;
+            holding.Weight = holding.Qty * holding.Dimensions.Weight;
+        }
+    }
+    
+}

+ 0 - 0
prs.classes/Entities/Stock/StockHoldingLink.cs → prs.classes/Entities/Stock/StockHolding/StockHoldingLink.cs


+ 0 - 0
prs.classes/Entities/Stock/StockHoldingLookups.cs → prs.classes/Entities/Stock/StockHolding/StockHoldingLookups.cs


+ 9 - 17
prs.classes/Entities/Stock/StockLocation/StockLocation.cs

@@ -19,23 +19,12 @@ namespace Comal.Classes
     
         public override AggregateCalculation Calculation => AggregateCalculation.Count;
     }
-    
-    public class StockLocationLastStocktake : CoreAggregate<StockLocation, StockMovement, DateTime>
-    {
-        public override Expression<Func<StockMovement, DateTime>> Aggregate => x => x.Date;
-
-        public override Filter<StockMovement> Filter => new Filter<StockMovement>(x => x.Batch).LinkValid().And(x => x.Batch.Type)
-            .IsEqualTo(StockMovementBatchType.Stocktake);
 
-        public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockLocation, object>>> Links =>
-            new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockLocation, object>>>()
-            {
-                { StockMovement => StockMovement.Location.ID, StockLocation => StockLocation.ID }
-            };
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Maximum;
+    public enum StockTakeStatus
+    {
+        None,
+        InProgress
     }
-    
     [UserTracking(typeof(StockMovement))]
     public class StockLocation : Entity, IRemotable, IPersistent, IStockLocation, ILicense<WarehouseLicense>, IExportable, IImportable
     {
@@ -105,9 +94,12 @@ namespace Comal.Classes
         [EditorSequence(10)]
         public double Holdings { get; set; }
 
-        [Aggregate(typeof(StockLocationLastStocktake))]
-        [DateEditor(Editable = Editable.Hidden)]
+        [EnumLookupEditor(typeof(StockTakeStatus))]
         [EditorSequence(11)]
+        public StockTakeStatus StocktakeStatus { get; set; }
+        
+        [DateEditor]
+        [EditorSequence(12)]
         public DateTime LastStocktake { get; set; }
 
     }

+ 0 - 0
prs.classes/Entities/Stock/StockMovement.cs → prs.classes/Entities/Stock/StockMovement/StockMovement.cs


+ 0 - 0
prs.classes/Entities/Stock/StockMovementBatch.cs → prs.classes/Entities/Stock/StockMovement/StockMovementBatch.cs


+ 0 - 0
prs.classes/Entities/Stock/StockMovementLookups.cs → prs.classes/Entities/Stock/StockMovement/StockMovementLookups.cs


+ 0 - 0
prs.classes/Entities/Stock/StockMovementType.cs → prs.classes/Entities/Stock/StockMovement/StockMovementType.cs