瀏覽代碼

Merge remote-tracking branch 'origin/kenric' into frank

frogsoftware 11 月之前
父節點
當前提交
42f681b8a6

+ 87 - 130
prs.classes/Entities/Job/Requisitions/JobRequisitionItem.cs

@@ -63,112 +63,7 @@ namespace Comal.Classes
         /// </summary>
         Issued
     }
-    public class JobRequisitionItemTotalQtyFormula : IFormula<JobRequisitionItem, double>
-    {
-        public Expression<Func<JobRequisitionItem, double>> Value => x => x.Qty;
-        public Expression<Func<JobRequisitionItem, double>>[] Modifiers => new Expression<Func<JobRequisitionItem, double>>[] { x => x.Dimensions.Value };
-        public FormulaOperator Operator => FormulaOperator.Multiply;
-        public FormulaType Type => FormulaType.Virtual;
-    }
-
-    public class JobRequisitionItemInStockAggregate : CoreAggregate<JobRequisitionItem, StockMovement, double>
-    {
-        public override Expression<Func<StockMovement, double>> Aggregate => x => x.Units;
-
-        public override Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => new Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-        {
-            { x => x.JobRequisitionItem.ID, x => x.ID }
-        };
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    }
-    public class JobRequisitionItemOrdersAggregate : CoreAggregate<JobRequisitionItem, JobRequisitionItemPurchaseOrderItem, double>
-    {
-        public override Expression<Func<JobRequisitionItemPurchaseOrderItem, double>> Aggregate => x => x.PurchaseOrderItem.Qty;
-
-        public override Dictionary<Expression<Func<JobRequisitionItemPurchaseOrderItem, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => new Dictionary<Expression<Func<JobRequisitionItemPurchaseOrderItem, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-        {
-            { x => x.JobRequisitionItem.ID, x => x.ID },
-        };
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-
-        public override Filter<JobRequisitionItemPurchaseOrderItem>? Filter =>
-            new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(null);
-    }
-
-    public class JobRequisitionItemOnOrderAggregate : CoreAggregate<JobRequisitionItem, JobRequisitionItemPurchaseOrderItem, double>
-    {
-        public override Expression<Func<JobRequisitionItemPurchaseOrderItem, double>> Aggregate => x => x.PurchaseOrderItem.Qty;
-
-        public override Dictionary<Expression<Func<JobRequisitionItemPurchaseOrderItem, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => new Dictionary<Expression<Func<JobRequisitionItemPurchaseOrderItem, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-        {
-            { x => x.JobRequisitionItem.ID, x => x.ID },
-            { x => x.PurchaseOrderItem.Product.ID, x => x.Product.ID },
-        };
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-
-        public override Filter<JobRequisitionItemPurchaseOrderItem>? Filter =>
-            new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(null);
-    }
-
-    public class JobRequisitionItemAllocatedAggregate : CoreAggregate<JobRequisitionItem, StockMovement, double>
-    {
-        public override Expression<Func<StockMovement, double>> Aggregate => x => x.Units;
-
-        public override Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => new Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-        {
-            { x => x.JobRequisitionItem.ID, x => x.ID },
-            { x => x.Style.ID, x => x.Style.ID },
-        };
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    }
-    public class JobRequisitionItemTreatmentOnOrderFormula : IFormula<JobRequisitionItem, double>
-    {
-        public Expression<Func<JobRequisitionItem, double>> Value => x => x.TotalOrders;
-        public Expression<Func<JobRequisitionItem, double>>[] Modifiers => new Expression<Func<JobRequisitionItem, double>>[] { x => x.OnOrder };
-        public FormulaOperator Operator => FormulaOperator.Subtract;
-        public FormulaType Type => FormulaType.Virtual;
-    }
-    public class JobRequisitionItemTreatmentRequiredFormula : IFormula<JobRequisitionItem, double>
-    {
-        public Expression<Func<JobRequisitionItem, double>> Value => x => x.InStock;
-        public Expression<Func<JobRequisitionItem, double>>[] Modifiers => new Expression<Func<JobRequisitionItem, double>>[] { x => x.Allocated };
-        public FormulaOperator Operator => FormulaOperator.Subtract;
-        public FormulaType Type => FormulaType.Virtual;
-    }
-    
-    public class JobRequisitionItemPickRequestedAggregate : CoreAggregate<JobRequisitionItem, RequisitionItem, double>
-    {
-        public override Expression<Func<RequisitionItem, double>> Aggregate => x => x.Quantity;
-
-        public override Dictionary<Expression<Func<RequisitionItem, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => 
-            new Dictionary<Expression<Func<RequisitionItem, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-            {
-                { x => x.SourceJRI.ID, x => x.ID },
-            };
-        
-        public override Filter<RequisitionItem>? Filter => new Filter<RequisitionItem>(x => x.RequisitionLink.StockUpdated).IsEqualTo(DateTime.MinValue);
-        
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    }
-    
-    public class JobRequisitionItemIssuedAggregate : CoreAggregate<JobRequisitionItem, StockMovement, double>
-    {
-        public override Expression<Func<StockMovement, double>> Aggregate => x => x.Issued;
-
-        public override Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => new Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-        {
-            { x => x.JobRequisitionItem.ID, x => x.ID },
-            { x => x.Style.ID, x => x.Style.ID },
-        };
-
-        public override Filter<StockMovement>? Filter => new Filter<StockMovement>(x => x.Type).IsEqualTo(StockMovementType.Issue);
 
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    }
     
     public interface IJobRequisitionItem : IEntity
     {
@@ -211,8 +106,13 @@ namespace Comal.Classes
         [RequiredColumn]
         public double Qty { get; set; }        
         
+        private class TotalQtyFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Formula(FormulaOperator.Multiply, Property(x => x.Qty), Property(x => x.Dimensions.Value));
+        }
         [DoubleEditor(Editable = Editable.Hidden)]
-        [Formula(typeof(JobRequisitionItemTotalQtyFormula))]
+        [ComplexFormula(typeof(TotalQtyFormula))]
         public double TotalQty { get; set; }
         
         [EditorSequence(5)]
@@ -229,51 +129,112 @@ namespace Comal.Classes
         [EditorSequence(8)]
         public SupplierLink Supplier { get; set; }
 
-        [EnumLookupEditor(typeof(JobRequisitionItemStatus), Editable = Editable.Disabled)]
+        [NullEditor]
         [EditorSequence(9)]
         [LoggableProperty]
         [RequiredColumn]
         public JobRequisitionItemStatus Status { get; set; } = JobRequisitionItemStatus.NotChecked;
 
+        private class InStockFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Aggregate<StockMovement>(AggregateCalculation.Sum, x => x.Property(x => x.Units))
+                    .WithLink(x => x.JobRequisitionItem.ID, x => x.ID);
+        }
+        [ComplexFormula(typeof(InStockFormula))]
+        [DoubleEditor(Editable = Editable.Disabled)]
+        [EditorSequence(10)]
         /// <summary>
         /// The amount of this requisition item that is currently in stock, which is an aggregate of the <see cref="StockMovement.Units"/> property.
         /// </summary>
-        [Aggregate(typeof(JobRequisitionItemInStockAggregate))]
-        [DoubleEditor(Editable = Editable.Disabled)]
-        [EditorSequence(10)]
         public double InStock { get; set; }
 
-        [Aggregate(typeof(JobRequisitionItemOrdersAggregate))]
+        private class TotalOrdersFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Aggregate<JobRequisitionItemPurchaseOrderItem>(
+                    AggregateCalculation.Sum,
+                    x => x.Property(x => x.PurchaseOrderItem.Qty),
+                    new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(null))
+                .WithLink(x => x.JobRequisitionItem.ID, x => x.ID);
+        }
+        [ComplexFormula(typeof(TotalOrdersFormula))]
         [DoubleEditor(Editable = Editable.Disabled, Visible = Visible.Optional)]
         [EditorSequence(11)]
         public double TotalOrders { get; set; }
 
-        [Aggregate(typeof(JobRequisitionItemOnOrderAggregate))]
+        private class OnOrderFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Aggregate<JobRequisitionItemPurchaseOrderItem>(
+                    AggregateCalculation.Sum,
+                    x => x.Property(x => x.PurchaseOrderItem.Qty),
+                    new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(null))
+                .WithLink(x => x.JobRequisitionItem.ID, x => x.ID)
+                .WithLink(x => x.PurchaseOrderItem.Product.ID, x => x.Product.ID);
+        }
+        [ComplexFormula(typeof(OnOrderFormula))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(12)]
         public double OnOrder { get; set; }
 
-        [Formula(typeof(JobRequisitionItemTreatmentRequiredFormula))]
+        private class TreatmentRequiredFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Formula(FormulaOperator.Subtract, Property(x => x.InStock), Property(x => x.Allocated));
+        }
+        [ComplexFormula(typeof(TreatmentRequiredFormula))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(13)]
         public double TreatmentRequired { get; set; }
 
-        [Formula(typeof(JobRequisitionItemTreatmentOnOrderFormula))]
+        private class TreatmentOnOrderFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Formula(FormulaOperator.Subtract, Property(x => x.TotalOrders), Property(x => x.OnOrder));
+        }
+        [ComplexFormula(typeof(TreatmentOnOrderFormula))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(14)]
         public double TreatmentOnOrder { get; set; }
 
-        [Aggregate(typeof(JobRequisitionItemAllocatedAggregate))]
+        private class AllocatedFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Aggregate<StockMovement>(
+                    AggregateCalculation.Sum,
+                    x => x.Property(x => x.Units))
+                .WithLink(x => x.JobRequisitionItem.ID, x => x.ID)
+                .WithLink(x => x.Style.ID, x => x.Style.ID);
+        }
+        [ComplexFormula(typeof(AllocatedFormula))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(15)]
         public double Allocated { get; set; }
         
-        [Aggregate(typeof(JobRequisitionItemPickRequestedAggregate))]
+        private class PickRequestedFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Aggregate<RequisitionItem>(AggregateCalculation.Sum, x => x.Property(x => x.Quantity), new Filter<RequisitionItem>(x => x.RequisitionLink.StockUpdated).IsEqualTo(DateTime.MinValue))
+                .WithLink(x => x.SourceJRI.ID, x => x.ID);
+        }
+        [ComplexFormula(typeof(PickRequestedFormula))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(16)]
         public double PickRequested { get; set; }
 
-        [Aggregate(typeof(JobRequisitionItemIssuedAggregate))]
+    
+        private class IssuedFormula : ComplexFormulaGenerator<JobRequisitionItem, double>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, double> GetFormula() =>
+                Aggregate<StockMovement>(
+                    AggregateCalculation.Sum,
+                    x => x.Property(x => x.Issued),
+                    new Filter<StockMovement>(x => x.Type).IsEqualTo(StockMovementType.Issue))
+                .WithLink(x => x.JobRequisitionItem.ID, x => x.ID)
+                .WithLink(x => x.Style.ID, x => x.Style.ID);
+        }
+        [ComplexFormula(typeof(IssuedFormula))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(17)]
         public double Issued { get; set; }
@@ -284,7 +245,15 @@ namespace Comal.Classes
         [NullEditor]
         public PurchaseOrderItemLink PurchaseOrderItem { get; set; }
 
-        [Aggregate(typeof(JobRequisitionItemPurchaseOrderNumberAggregate))]
+        private class PurchaseOrderNumbersFormula : ComplexFormulaGenerator<JobRequisitionItem, string>
+        {
+            public override IComplexFormulaNode<JobRequisitionItem, string> GetFormula() =>
+                Aggregate<JobRequisitionItemPurchaseOrderItem>(
+                    AggregateCalculation.Concat,
+                    x => x.Property(x => x.PurchaseOrderItem.PurchaseOrderLink.PONumber))
+                .WithLink(x => x.JobRequisitionItem.ID, x => x.ID);
+        }
+        [ComplexFormula(typeof(PurchaseOrderNumbersFormula))]
         [TextBoxEditor(Editable = Editable.Hidden)]
         public string PurchaseOrderNumbers { get; set; }
 
@@ -433,16 +402,4 @@ namespace Comal.Classes
             }
         }
     }
-
-    public class JobRequisitionItemPurchaseOrderNumberAggregate : CoreAggregate<JobRequisitionItem, JobRequisitionItemPurchaseOrderItem, string>
-    {
-        public override Expression<Func<JobRequisitionItemPurchaseOrderItem, string>> Aggregate => x => x.PurchaseOrderItem.PurchaseOrderLink.PONumber;
-
-        public override Dictionary<Expression<Func<JobRequisitionItemPurchaseOrderItem, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links { get; } = new Dictionary<Expression<Func<JobRequisitionItemPurchaseOrderItem, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-        {
-            { x => x.JobRequisitionItem.ID, x => x.ID }
-        };
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Concat;
-    }
 }

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

@@ -228,7 +228,6 @@ namespace Comal.Classes
                         .Add(x => x.Job.Name)
                         .Add(x => x.Requisition.Number)
                         .Add(x => x.Requisition.Description)
-                        .Add(x=>x.Status)
                         .Add(x => x.Qty))
                 .ToObjects<JobRequisitionItem>();
             if (holding.Available > 0 || alwaysshowunallocated)

+ 10 - 15
prs.classes/Entities/Stock/StockMovement/StockMovement.cs

@@ -25,18 +25,6 @@ namespace Comal.Classes
         [NullEditor]
         public override Guid ID { get; set; }
     }
-
-    
-    public class StockMovementUnitsFormula : IFormula<StockMovement, double>
-    {
-        public Expression<Func<StockMovement, double>> Value => x => x.Received;
-
-        public Expression<Func<StockMovement, double>>[] Modifiers => new Expression<Func<StockMovement, double>>[] { x => x.Issued };
-
-        public FormulaOperator Operator => FormulaOperator.Subtract;
-        
-        public FormulaType Type => FormulaType.Virtual;
-    }
     
     public class StockMovementValueFormula : IFormula<StockMovement, double>
     {
@@ -105,11 +93,18 @@ namespace Comal.Classes
         [DoubleEditor]
         [EditorSequence(6)]
         public double Balance { get; set; }
-        
-        // Units = Received - Issued
-        [Formula(typeof(StockMovementUnitsFormula))]
+
+        private class StockMovementUnitsFormula : ComplexFormulaGenerator<StockMovement, double>
+        {
+            public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
+                Formula(FormulaOperator.Subtract, Property(x => x.Received), Property(x => x.Issued));
+        }
+        [ComplexFormula(typeof(StockMovementUnitsFormula))]
         [EditorSequence(7)]
         [DoubleEditor(Visible=Visible.Optional, Editable = Editable.Hidden, Summary= Summary.Sum)]
+        /// <summary>
+        /// Units = Received - Issued
+        /// </summary>
         public double Units { get; set; }
 
         private class IsRemnantCondition : ComplexFormulaGenerator<StockMovement, bool>

+ 13 - 16
prs.desktop/Panels/Jobs/Digital Forms/JobFormGrid.cs

@@ -9,9 +9,10 @@ using System.Threading;
 
 namespace PRSDesktop
 {
-    public class JobFormGrid : DynamicDataGrid<JobForm>, IMasterDetailControl<Job,JobForm>, IDataModelSource
+    public class JobFormGrid : DynamicEntityFormGrid<JobForm, Job, JobLink>, IMasterDetailControl<Job,JobForm>, IDataModelSource
     {
-        
+        protected override Job Entity { get => Master; set => Master = value; }
+
         public Job? Master { get; set; }
 
         public Filter<JobForm> MasterDetailFilter => (Master?.ID ?? Guid.Empty) != Guid.Empty
@@ -30,7 +31,6 @@ namespace PRSDesktop
 
         public JobFormGrid()
         {
-            ActionColumns.Add(new DynamicImageColumn(PRSDesktop.Resources.pencil.AsBitmapImage(), EditAction));
         }
 
         protected override void DoReconfigure(DynamicGridOptions options)
@@ -40,19 +40,6 @@ namespace PRSDesktop
             options.FilterRows = true;
         }
 
-        
-        private bool EditAction(CoreRow? row)
-        {
-            if (row == null) return false;
-
-            if(DynamicFormEditWindow.EditDigitalForm<JobForm>(row.Get<JobForm, Guid>(x => x.ID), out var dataModel))
-            {
-                dataModel.Update(null);
-                return true;
-            }
-            return false;
-        }
-
         protected override void Reload(
         	Filters<JobForm> criteria, Columns<JobForm> columns, ref SortOrder<JobForm>? sort,
         	CancellationToken token, Action<CoreTable?, Exception?> action)
@@ -66,6 +53,16 @@ namespace PRSDesktop
             return base.CanCreateItems() && Master != null;
         }
 
+        protected override void OnFormCreated(JobForm form)
+        {
+            base.OnFormCreated(form);
+
+            if (DynamicFormEditWindow.EditDigitalForm(form, out var dataModel))
+            {
+                dataModel.Update(null);
+            }
+        }
+
         public override JobForm CreateItem()
         {
             var result = base.CreateItem();

+ 0 - 1
prs.desktop/Panels/Jobs/Requisitions/JobRequisitionItemGrid.cs

@@ -364,7 +364,6 @@ internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMa
         columns.Add<JobRequisitionItem, double>(x => x.Qty, 50, "Qty", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.Dimensions.UnitSize, 50, "Size", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.PurchaseOrderNumbers, 80, "PO Numbers", "", Alignment.MiddleLeft);
-        columns.Add<JobRequisitionItem, JobRequisitionItemStatus>(x => x.Status, 80, "Status", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.Notes, 300, "Notes", "", Alignment.MiddleLeft);
 
         columns.AddRange(base.GenerateColumns());

+ 0 - 1
prs.desktop/Panels/Jobs/Summary/JobRequisitionItemSummaryGrid.cs

@@ -188,7 +188,6 @@ public class JobRequisitionItemSummaryGrid : DynamicDataGrid<JobRequisitionItem>
         columns.Add<JobRequisitionItem, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.Style.Code, 100, "Style", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.Dimensions.UnitSize, 70, "Size", "", Alignment.MiddleLeft);
-        columns.Add<JobRequisitionItem, JobRequisitionItemStatus>(x => x.Status, 90, "Status", "", Alignment.MiddleCenter);
 
         return columns;
     }

+ 180 - 96
prs.desktop/Panels/Products/Locations/StockHoldingGrid.cs

@@ -37,10 +37,9 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
     //Button ReserveButton = null;
 
     private Button TransferButton;
-    
     private Button RecalculateButton;
-
     private Button AdjustValueButton;
+    private Button RelocateButton;
 
     public StockHoldingGrid() : base()
     {         
@@ -66,6 +65,9 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         TransferButton.Margin = new Thickness(20, TransferButton.Margin.Top, TransferButton.Margin.Right, TransferButton.Margin.Bottom);
         TransferButton.IsEnabled = false;
 
+        RelocateButton = AddButton("Relocate", PRSDesktop.Resources.box.AsBitmapImage(), RelocateStock);
+        RelocateButton.IsEnabled = false;
+
         AdjustValueButton = AddButton("Adjust Value", PRSDesktop.Resources.receipt.AsBitmapImage(), AdjustValues,
             DynamicGridButtonPosition.Right);
         AdjustValueButton.Margin = new Thickness(AdjustValueButton.Margin.Left, AdjustValueButton.Margin.Top, 10, AdjustValueButton.Margin.Bottom);
@@ -144,7 +146,6 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         return false;
     }
 
-    
     private bool RecalculateHoldings(Button arg1, CoreRow[] arg2)
     {
         Dictionary<String, int> messages = new();
@@ -281,8 +282,6 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         options.FilterRows = true;
     }
 
-
-
     private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
     {
         if (row is null) return;
@@ -335,6 +334,58 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
 
     }
 
+    private void DoTransfer(StockHolding holding, IList<JobRequisitionItem> requiitems, Func<JobRequisitionItem, double?> getQuantity, Action<JobRequisitionItem, StockMovement, StockMovement> modify)
+    {
+        var updates = new List<StockMovement>();
+        DoTransfer(holding, requiitems, getQuantity, modify, updates);
+
+        SaveBatch(StockMovementBatchType.Transfer, updates);
+        DoChanged();
+        Refresh(false, true);
+    }
+
+    private static void DoTransfer(StockHolding holding, IList<JobRequisitionItem> requiitems, Func<JobRequisitionItem, double?> getQuantity, Action<JobRequisitionItem, StockMovement, StockMovement> modify, List<StockMovement> updates)
+    {
+        foreach(var requi in requiitems)
+        {
+            var qty = getQuantity(requi);
+            if (!qty.HasValue || qty.Value == 0) continue;
+
+            var mout = holding.CreateMovement();
+            mout.Cost = holding.AverageValue;
+            mout.Date = DateTime.Now;
+            mout.Employee.ID = App.EmployeeID;
+
+            var min = mout.CreateMovement();
+            min.Cost = holding.AverageValue;
+            min.Employee.ID = App.EmployeeID;
+            min.Date = mout.Date;
+            min.Transaction = mout.Transaction;
+
+            if(qty.Value > 0)
+            {
+                mout.Issued = qty.Value;
+                mout.Type = StockMovementType.TransferOut;
+
+                min.Received = qty.Value;
+                min.Type = StockMovementType.TransferIn;
+            }
+            else
+            {
+                min.Issued = -qty.Value;
+                min.Type = StockMovementType.TransferOut;
+
+                mout.Received = -qty.Value;
+                mout.Type = StockMovementType.TransferIn;
+            }
+
+            modify(requi, mout, min);
+
+            updates.Add(mout);
+            updates.Add(min);
+        }
+    }
+
     private void ReleaseAllocatedStock_Click(StockHolding holding)
     {
         var requiitems = holding.LoadRequisitionItems(true).Where(x => x.ID != Guid.Empty).ToList();
@@ -343,35 +394,14 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         {
             var quantities = win.GetQuantities();
 
-            var updates = new List<StockMovement>();
-            foreach(var requi in requiitems)
+            DoTransfer(holding, requiitems, x => quantities.TryGetValue(x.ID, out var value) ? value : null, (requi, mout, min) =>
             {
-                if (!quantities.TryGetValue(requi.ID, out var qty) || qty <= 0) continue;
-
-                var mout = holding.CreateMovement();
-                mout.Issued = qty;
-                mout.Cost = holding.AverageValue;
                 mout.JobRequisitionItem.ID = requi.ID;
-                mout.Date = DateTime.Now;
-                mout.Employee.ID = App.EmployeeID;
                 mout.Notes = $"Released from Job Requisition {requi.Requisition.Number}: {requi.Requisition.Description} for Job {requi.Job.JobNumber}";
-                mout.Type = StockMovementType.TransferOut;
 
-                var min = mout.CreateMovement();
-                min.Received = qty;
-                min.Cost = holding.AverageValue;
                 min.JobRequisitionItem.ID = Guid.Empty;
-                min.Date = DateTime.Now;
-                min.Employee.ID = App.EmployeeID;
                 min.Notes = $"Released from Job Requisition {requi.Requisition.Number}: {requi.Requisition.Description} for Job {requi.Job.JobNumber}";
-                min.Type = StockMovementType.TransferIn;
-
-                updates.Add(mout);
-                updates.Add(min);
-            }
-            SaveBatch(StockMovementBatchType.Transfer, updates);
-            DoChanged();
-            Refresh(false, true);
+            });
         }
     }
     
@@ -397,39 +427,17 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
             var quantities = win.GetQuantities();
             var target = win.GetTargetLocation();
 
-            var updates = new List<StockMovement>();
-            foreach (var requiitem in requiitems)
+            DoTransfer(holding, requiitems, x => quantities.TryGetValue(x.ID, out var qty) ? qty : null, (requi, mout, min) =>
             {
-                if (!quantities.TryGetValue(requiitem.ID, out var qty)) continue;
-
-                var mout = holding.CreateMovement();
-                mout.Issued = qty;
-                mout.Cost = holding.AverageValue;
-                mout.JobRequisitionItem.ID = requiitem.ID;
-                mout.Type = StockMovementType.TransferOut;
-                mout.Date = DateTime.Now;
-                mout.Employee.ID = App.EmployeeID;
+                mout.JobRequisitionItem.ID = requi.ID;
                 mout.Notes = $"Moved to {target.Code} by {App.EmployeeName}";
-                updates.Add(mout);
 
-                var min = holding.CreateMovement();
                 min.Location.Clear();
                 min.Location.ID = target.ID;
                 min.Job.CopyFrom(win.Job ?? new());
-
-                min.Received = mout.Issued;
-                min.Cost = holding.AverageValue;
-                min.JobRequisitionItem.ID = requiitem.ID;
-                min.Transaction = mout.Transaction;
-                min.Type = StockMovementType.TransferIn;
-                min.Date = mout.Date;
-                min.Employee.ID = App.EmployeeID;
+                min.JobRequisitionItem.ID = requi.ID;
                 min.Notes = $"Moved From {holding.Location.Code} by {App.EmployeeName}";
-                updates.Add(min);
-            }
-            SaveBatch(StockMovementBatchType.Transfer, updates);
-            DoChanged();
-            Refresh(false, true);
+            });
         }
     }
 
@@ -486,23 +494,6 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
            
         return result;
     }
-
-    private static void SaveBatch(StockMovementBatchType type, IList<StockMovement> movements)
-    {
-        var batch = new StockMovementBatch();
-        batch.Type = type;
-        batch.Notes = batch.Type + " batch created from Desktop Stock Location Screen";
-        batch.Employee.ID = App.EmployeeID;
-        new Client<StockMovementBatch>().Save(batch, "created from Desktop Stock Location Screen");
-
-        foreach (var mvt in movements)
-        { 
-            mvt.Batch.ID = batch.ID;
-        }
-
-        new Client<StockMovement>().Save(movements, "Updating batch from Desktop Stock Location Screen");
-    }
-
     private bool IssueStock(Button arg1, CoreRow[] rows)
     {
         if (rows?.Length != 1)
@@ -594,6 +585,57 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         return false;
     }
 
+    private bool RelocateStock(Button btn, CoreRow[] rows)
+    {
+        StockLocation? target = null;
+        while(true)
+        {
+            target = StockHoldingRelocationWindow.LookupLocation();
+            if(target is null)
+            {
+                return false;
+            }
+            else if(target.ID == Location.ID)
+            {
+                MessageWindow.ShowMessage($"These items are already in {target.Code}; please select a different location.", "Invalid transfer");
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        var holdings = rows.ToArray<StockHolding>();
+        var updates = new List<StockMovement>();
+        foreach(var holding in holdings)
+        {
+            var items = holding.LoadRequisitionItems(true).AsIList();
+
+            var rIDs = items.Select(x => x.ID).Where(x => x != Guid.Empty).ToArray();
+            var quantities = Client.Query(
+                StockHolding.GetFilter(holding)
+                    .Combine(new Filter<StockMovement>(x => x.JobRequisitionItem.ID).InList(rIDs)),
+                Columns.None<StockMovement>().Add(x => x.Units).Add(x => x.JobRequisitionItem.ID))
+                .ToObjects<StockMovement>().GroupBy(x => x.JobRequisitionItem.ID).ToDictionary(x => x.Key, x => x.Sum(x => x.Units));
+
+            DoTransfer(holding, items, x => x.ID == Guid.Empty ? x.Qty : quantities.GetValueOrDefault(x.ID), (requi, mThis, mThat) =>
+            {
+                mThis.JobRequisitionItem.ID = requi.ID;
+                mThis.Notes = $"Moved to {target.Code} by {App.EmployeeName}";
+
+                mThat.JobRequisitionItem.ID = requi.ID;
+                mThat.Notes = $"Moved to {target.Code} by {App.EmployeeName}";
+                mThat.Location.Clear();
+                mThat.Location.ID = target.ID;
+            }, updates);
+        }
+
+        SaveBatch(StockMovementBatchType.Transfer, updates);
+        DoChanged();
+        Refresh(false, true);
+        return true;
+    }
+
     protected override void DoEdit()
     {
         var holding = SelectedRows.FirstOrDefault()?.ToObject<StockHolding>();
@@ -647,6 +689,55 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         Refresh(false, true);
     }
 
+    public IStockLocation Location { get; set; }
+
+    protected override void SelectItems(CoreRow[]? rows)
+    {
+        base.SelectItems(rows);
+
+        var nRows = rows?.Length ?? 0;
+
+        ReceiveButton.IsEnabled = Location != null && Location.ID != Guid.Empty;
+        IssueButton.IsEnabled = Location != null && Location.ID != Guid.Empty && nRows > 0;
+
+        if(Location is null || Location.ID == Guid.Empty || nRows == 0)
+        {
+            TransferButton.IsEnabled = false;
+            RelocateButton.IsEnabled = false;
+        }
+        else if(nRows == 1)
+        {
+            TransferButton.IsEnabled = true;
+            RelocateButton.IsEnabled = true;
+        }
+        else
+        {
+            TransferButton.IsEnabled = false;
+            RelocateButton.IsEnabled = true;
+        }
+
+        var _groups = rows?.GroupBy(x => new Tuple<Guid, double>(
+            x.Get<StockHolding, Guid>(c => c.Product.ID),
+            x.Get<StockHolding, double>(c => c.Dimensions.Value))
+        );
+        AdjustValueButton.IsEnabled = Location != null && Location.ID != Guid.Empty && _groups?.Count() == 1;
+        RecalculateButton.IsEnabled = Location != null && Location.ID != Guid.Empty;
+    }
+
+    protected override void Reload(
+    	Filters<StockHolding> criteria, Columns<StockHolding> columns, ref SortOrder<StockHolding>? sort,
+    	CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        ReceiveButton.IsEnabled = Location != null && Location.ID != Guid.Empty;
+        if (Location == null)
+            criteria.Add(new Filter<StockHolding>().None());
+        else
+            criteria.Add(new Filter<StockHolding>(x => x.Location.ID).IsEqualTo(Location.ID));
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
+
+    #region Internal Utilities
+
     private StockMovement CreateMovementFromHolding(StockHolding holding)
     {
         var movement = new StockMovement();
@@ -665,23 +756,28 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         movement.CommitChanges();
         return movement;
     }
-    public IStockLocation Location { get; set; }
 
-    protected override void SelectItems(CoreRow[]? rows)
+    private static void SaveBatch(StockMovementBatchType type, IList<StockMovement> movements)
     {
-        base.SelectItems(rows);
+        var batch = new StockMovementBatch();
+        batch.Type = type;
+        batch.Notes = batch.Type + " batch created from Desktop Stock Location Screen";
+        batch.Employee.ID = App.EmployeeID;
+        new Client<StockMovementBatch>().Save(batch, "created from Desktop Stock Location Screen");
 
-        ReceiveButton.IsEnabled = Location != null && Location.ID != Guid.Empty;
-        IssueButton.IsEnabled = Location != null && Location.ID != Guid.Empty && rows?.Any() == true;
-        TransferButton.IsEnabled = Location != null && Location.ID != Guid.Empty && rows?.Any() == true;
-        var _groups = rows?.GroupBy(x => new Tuple<Guid, double>(
-            x.Get<StockHolding, Guid>(c => c.Product.ID),
-            x.Get<StockHolding, double>(c => c.Dimensions.Value))
-        );
-        AdjustValueButton.IsEnabled = Location != null && Location.ID != Guid.Empty && _groups?.Count() == 1;
-        RecalculateButton.IsEnabled = Location != null && Location.ID != Guid.Empty;
+        foreach (var mvt in movements)
+        { 
+            mvt.Batch.ID = batch.ID;
+        }
+
+        new Client<StockMovement>().Save(movements, "Updating batch from Desktop Stock Location Screen");
     }
 
+
+    #endregion
+
+    #region StockMovementGrid
+
     private DynamicDataGrid<StockMovement> CheckStockMovementGrid(MovementAction action, StockHolding holding)
     {
         _action = action;
@@ -790,17 +886,5 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         
     }
 
-
-    protected override void Reload(
-    	Filters<StockHolding> criteria, Columns<StockHolding> columns, ref SortOrder<StockHolding>? sort,
-    	CancellationToken token, Action<CoreTable?, Exception?> action)
-    {
-        ReceiveButton.IsEnabled = Location != null && Location.ID != Guid.Empty;
-        if (Location == null)
-            criteria.Add(new Filter<StockHolding>().None());
-        else
-            criteria.Add(new Filter<StockHolding>(x => x.Location.ID).IsEqualTo(Location.ID));
-        base.Reload(criteria, columns, ref sort, token, action);
-    }
-    
+    #endregion
 }

+ 14 - 2
prs.desktop/Panels/Products/Locations/StockHoldingRelocationWindow.xaml.cs

@@ -323,7 +323,7 @@ public partial class StockHoldingRelocationWindow : Window, INotifyPropertyChang
 
     #region Target Location
 
-    private bool DoLookupLocation(string? column, string? value)
+    public static StockLocation? LookupLocation(string? column = null, string? value = null)
     {
         var grid = new MultiSelectDialog<StockLocation>(
             LookupFactory.DefineFilter<StockLocation>(),
@@ -331,7 +331,19 @@ public partial class StockHoldingRelocationWindow : Window, INotifyPropertyChang
             multiselect: false);
         if (grid.ShowDialog(column, value))
         {
-            To = grid.Data().Rows.First().ToObject<StockLocation>();
+            return grid.Data().Rows.First().ToObject<StockLocation>();
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    private bool DoLookupLocation(string? column, string? value)
+    {
+        if(LookupLocation(column, value) is StockLocation location)
+        {
+            To = location;
             return true;
         }
         else

+ 0 - 1
prs.desktop/Panels/Reservation Management/ReservationManagementItemGrid.cs

@@ -530,7 +530,6 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
         columns.Add<JobRequisitionItem, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.Style.Code, 100, "Style", "", Alignment.MiddleLeft);
         columns.Add<JobRequisitionItem, string>(x => x.Dimensions.UnitSize, 70, "Size", "", Alignment.MiddleLeft);
-        columns.Add<JobRequisitionItem, JobRequisitionItemStatus>(x => x.Status, 90, "Status", "", Alignment.MiddleCenter);
 
         return columns;
     }

+ 17 - 3
prs.desktop/Panels/Reservation Management/ReservationManagementPanel.xaml.cs

@@ -139,7 +139,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
         ProductSetupActions.Standard(host);
         host.CreateSetupAction(new PanelAction() { Caption = "Reservation Management Settings", Image = PRSDesktop.Resources.specifications, OnExecute = ConfigSettingsClick });
 
-        //host.CreatePanelAction(new PanelAction("Treatment PO", PRSDesktop.Resources.purchase, TreatmentPO_Click));
+        // host.CreatePanelAction(new PanelAction("Treatment PO", PRSDesktop.Resources.purchase, TreatmentPO_Click));
 
         if(Mode == PanelMode.Purchase && SplitPanel.IsDetailVisible())
         {
@@ -361,8 +361,16 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
             return;
         }
 
-        Client.EnsureColumns(jris.Values, Columns.None<JobRequisitionItem>().Add(x => x.Product.Name));
+        Client.EnsureColumns(jris.Values, Columns.None<JobRequisitionItem>()
+            .Add(x => x.Product.Code)
+            .Add(x => x.Product.Name)
+            .Add(x => x.Requisition.Number)
+            .Add(x => x.Job.JobNumber)
+            .Add(x => x.Dimensions.Value));
 
+        // Here, we grab every stock movement for the selected JRIs, and group them per JRI. For each JRI, any stock movements that are in the wrong style
+        // are grouped according to their holding key. Note that this mimics precisely the TreatmentRequired aggregate on JRI. Hence, these holdings represent
+        // the TreatmentRequired amounts; the 'Units' field is equivalent to TreatmentRequired.
         var holdings = Client.Query(
             new Filter<StockMovement>(x => x.JobRequisitionItem.ID).InList(jris.Keys.ToArray()),
             Columns.None<StockMovement>()
@@ -417,6 +425,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 .Add(x => x.Code)
                 .Add(x => x.StockTreatmentProduct.ID))
             .ToObjects<ProductStyle>().ToDictionary(x => x.ID);
+        // We need to load the treatment product for the styles that we need.
         var treatmentProducts = Client.Query(
             new Filter<Product>(x => x.ID).InList(styles.Select(x => x.Value.StockTreatmentProduct.ID).ToArray()),
             Columns.None<Product>()
@@ -428,6 +437,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 .Add(x => x.TreatmentType.Description)
                 .Add(x => x.TreatmentType.Calculation))
             .ToObjects<Product>().ToDictionary(x => x.ID);
+        // Also, the ProductTreatment contains the parameter we need.
         var jriProductsParameters = Client.Query(
             new Filter<ProductTreatment>(x => x.Product.ID).InList(jris.Values.Select(x => x.Product.ID).ToArray()),
             Columns.None<ProductTreatment>()
@@ -463,6 +473,8 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
             }
 
             var jriHoldings = holdings.GetValueOrDefault(id);
+            // We know here that the TreatmentRequired > 0, because of the check at the top of the function. Hence, there definitely should be holdings.
+            // This therefore shouldn't ever happen, but if it does, we've made a logic mistake, and this error will tell us that.
             if(jriHoldings is null || jriHoldings.Count == 0)
             {
                 MessageWindow.ShowError($"Internal error for requisition {jri.Requisition.Number} for job {jri.Job.JobNumber}", $"No holdings even though TreatmentRequired is greater than 0.");
@@ -472,6 +484,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
             double multiplier;
             if (treatmentProduct.TreatmentType.Calculation.IsNullOrWhiteSpace())
             {
+                // This is the default calculation.
                 multiplier = treatment.Parameter * jri.Dimensions.Value;
             }
             else
@@ -505,7 +518,8 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 item.JRI.CopyFrom(jri);
 
                 item.Multiplier = multiplier;
-                item.RequiredQuantity = holding.Units;// jri.TreatmentRequired - jri.TreatmentOnOrder;
+                // holding.Units should be TreatmentRequired
+                item.RequiredQuantity = holding.Units;
 
                 items.Add(item);
             }

+ 1 - 1
prs.desktop/Panels/Suppliers/Bills/SupplierBillLineGrid.cs

@@ -370,7 +370,7 @@ public class SupplierBillLineGrid : DynamicOneToManyGrid<Bill, BillLine>
         if (dlg.ShowDialog() == true)
         {
             var imports = dlg.Data();
-            var consids = imports.ExtractValues<PurchaseOrderItem, Guid>(x => x.Consignment.ID).Distinct().ToArray();
+            var consids = imports.ExtractValues<PurchaseOrderItem, Guid>(x => x.Consignment.ID).Where(x => x != Guid.Empty).Distinct().ToArray();
 
             var results = Client.QueryMultiple(
                 new KeyedQueryDef<Consignment>(

+ 13 - 2
prs.desktop/Panels/Suppliers/Bills/SupplierBillPanel.xaml.cs

@@ -143,6 +143,7 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
     }
 
     private Bill[]? _bills = null;
+    private CoreRow[]? _editRows = null;
     
     private void Bills_OnOnSelectItem(object sender, DynamicGridSelectionEventArgs e)
     {
@@ -154,15 +155,17 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
 
     private void ReloadBills()
     {
-        if (Bills.SelectedRows?.Any() == true)
+        if (Bills.SelectedRows.Length != 0)
         {
-            _bills = Bills.LoadBills(Bills.SelectedRows);
+            _editRows = Bills.SelectedRows;
+            _bills = Bills.LoadBills(_editRows);
             Bills.InitialiseEditorForm(Bill, _bills, null, true);
             Bill.Visibility = Visibility.Visible;
         }
         else
         {
             _bills = null;
+            _editRows = null;
             Bill.Visibility = Visibility.Hidden;
         }
     }
@@ -175,6 +178,14 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
             Bill.SaveItem(cancel);
             if (!cancel.Cancel)
             {
+                if(_editRows is not null && _bills is not null)
+                {
+                    for(var i = 0; i < _editRows.Length; ++i)
+                    {
+                        Bills.UpdateRow(_editRows[i], _bills[i]);
+                        Bills.InvalidateRow(_editRows[i]);
+                    }
+                }
                 ReloadBills();
                 SetChanged(false);
             }

+ 24 - 11
prs.stores/BillStore.cs

@@ -1,22 +1,35 @@
 using Comal.Classes;
+using InABox.Core;
 using System;
 
-namespace Comal.Stores
+namespace Comal.Stores;
+
+internal class BillStore : BaseStore<Bill>
 {
-    internal class BillStore : BaseStore<Bill>
+    protected override void BeforeSave(Bill entity)
     {
-        protected override void BeforeSave(Bill entity)
+        base.BeforeSave(entity);
+
+        if(entity.HasOriginalValue(x => x.Number) || entity.SupplierLink.HasOriginalValue(x => x.ID))
         {
-            base.BeforeSave(entity);
-            //UpdateAggregate<Supplier>(entity, entity.SupplierLink, Sum<Supplier>(b => b.Balance, s => s.Balance));
+            var existing = Provider.Query<Bill>(
+                new Filter<Bill>(x => x.Number).IsEqualTo(entity.Number)
+                    .And(x => x.SupplierLink.ID).IsEqualTo(entity.SupplierLink.ID)
+                    .And(x => x.ID).IsNotEqualTo(entity.ID),
+                Columns.None<Bill>().Add(x => x.ID));
+            if(existing.Rows.Count > 0)
+            {
+                throw new DuplicateCodeException(typeof(Bill), new Dictionary<string, object> {{ nameof(Bill.Number), entity.Number }});
+            }
         }
+        //UpdateAggregate<Supplier>(entity, entity.SupplierLink, Sum<Supplier>(b => b.Balance, s => s.Balance));
+    }
 
-        protected override void BeforeDelete(Bill entity)
-        {
-            base.BeforeDelete(entity);
+    protected override void BeforeDelete(Bill entity)
+    {
+        base.BeforeDelete(entity);
 
-            entity.SupplierLink.ID = Guid.Empty;
-            //UpdateAggregate<Supplier>(entity, entity.SupplierLink, Sum<Supplier>(b => b.Balance, s => s.Balance));
-        }
+        entity.SupplierLink.ID = Guid.Empty;
+        //UpdateAggregate<Supplier>(entity, entity.SupplierLink, Sum<Supplier>(b => b.Balance, s => s.Balance));
     }
 }

+ 13 - 3
prs.stores/StockMovementStore.cs

@@ -16,6 +16,7 @@ public class StockMovementStore : BaseStore<StockMovement>
     // These will be initialised in BeforeSave
     HoldingDictionary holdingData = null!;
     StockMovement[] mvtData = null!;
+    HashSet<Guid> currentIDs = new();
 
     protected override void BeforeSave(IEnumerable<StockMovement> entities)
     {
@@ -25,7 +26,9 @@ public class StockMovementStore : BaseStore<StockMovement>
             base.BeforeSave(entity);
         }
 
-        mvtData = StockHoldingStore.LoadMovementData(this, entities.Select(x => x.ID).ToArray());
+        currentIDs = entities.Select(x => x.ID).Where(x => x != Guid.Empty).ToHashSet();
+
+        mvtData = StockHoldingStore.LoadMovementData(this, currentIDs.ToArray());
         holdingData = StockHoldingStore.LoadStockHoldings(this, mvtData);
 
         StockHoldingStore.ModifyHoldings(mvtData, holdingData, StockHoldingStore.Action.Decrease);
@@ -68,8 +71,15 @@ public class StockMovementStore : BaseStore<StockMovement>
 
     protected override void AfterSave(IEnumerable<StockMovement> entities)
     {
-        mvtData = StockHoldingStore.LoadMovementData(this, entities.Select(x => x.ID).ToArray());
-        holdingData = StockHoldingStore.LoadStockHoldings(this, mvtData);
+        // Find all movements that weren't loaded in BeforeSave - i.e., they are new stock movements.
+        var newIDs = entities.Select(x => x.ID).Where(x => !currentIDs.Contains(x)).ToArray();
+        // Grab the data for these movements.
+        var newMvts = StockHoldingStore.LoadMovementData(this, newIDs);
+        // Load all the stock holdings for these movements, but only if we haven't loaded them already.
+        StockHoldingStore.LoadStockHoldings(this, newMvts, holdingData);
+
+        // Add the new movement data to our data. Note that the above line does the same for holdings.
+        mvtData = mvtData.Concatenate(newMvts);
         
         // Update the Relevant StockHolding with the details of this movement
         StockHoldingStore.ModifyHoldings(mvtData, holdingData, StockHoldingStore.Action.Increase);