浏览代码

Added StockMovement.TransferID

Kenric Nugteren 1 周之前
父节点
当前提交
0dff4e02b9

+ 3 - 3
prs.classes/Entities/Stock/StockHolding/StockHoldingExtensions.cs

@@ -156,7 +156,6 @@ namespace Comal.Classes
                     _transout.Employee.ID = batch.Employee.ID;
                     _transout.Issued = _units;
                     _transout.Cost = holding.AverageValue;
-                    _transout.Type = StockMovementType.TransferOut;
                     _transout.JobRequisitionItem.ID = _allocation.Key;
                     _transout.Batch.ID = batch.ID;
                     _transout.Notes = $"Adjusting Average Value from ${holding.AverageValue:F2} to ${unitvalue:F2}";
@@ -167,11 +166,12 @@ namespace Comal.Classes
                     _transout.Employee.ID = batch.Employee.ID;
                     _transin.Received = _units;
                     _transin.Cost = unitvalue;
-                    _transin.Type = StockMovementType.TransferIn;
-                    _transin.Transaction = _transout.Transaction;
                     _transin.JobRequisitionItem.ID = _allocation.Key;
                     _transin.Batch.ID = batch.ID;
                     _transin.Notes = $"Adjusting Average Value from ${holding.AverageValue:F2} to ${unitvalue:F2}";
+
+                    StockMovement.LinkTransfers(_transout, _transin);
+
                     _result.Add(_transin);
                 }
             }

+ 122 - 41
prs.classes/Entities/Stock/StockMovement/StockMovement.cs

@@ -7,46 +7,11 @@ using PRSClasses;
 
 namespace Comal.Classes
 {
-    public class StockMovementDocumentCount : CoreAggregate<StockMovement, StockMovementBatchDocument, Guid>
-    {
-        public override Expression<Func<StockMovementBatchDocument, Guid>> Aggregate => x => x.ID;
-
-        public override AggregateCalculation Calculation => AggregateCalculation.Count;
-
-        public override Dictionary<Expression<Func<StockMovementBatchDocument, object>>, Expression<Func<StockMovement, object>>> Links =>
-            new Dictionary<Expression<Func<StockMovementBatchDocument, object>>, Expression<Func<StockMovement, object>>>()
-            {
-                { StockMovementBatchDocument => StockMovementBatchDocument.EntityLink.ID, StockMovement => StockMovement.Batch.ID }
-            };
-    }
-
     public class StockMovementLink : EntityLink<StockMovement>
     {
         [NullEditor]
         public override Guid ID { get; set; }
     }
-    
-    public class StockMovementValueFormula : IFormula<StockMovement, double>
-    {
-        public Expression<Func<StockMovement, double>> Value => x => x.Units;
-
-        public Expression<Func<StockMovement, double>>[] Modifiers => new Expression<Func<StockMovement, double>>[] { x => x.Cost };
-
-        public FormulaOperator Operator => FormulaOperator.Multiply;
-        
-        public FormulaType Type => FormulaType.Virtual;
-    }
-    
-    public class StockMovementQuantityFormula : IFormula<StockMovement, double>
-    {
-        public Expression<Func<StockMovement, double>> Value => x => x.Units;
-
-        public Expression<Func<StockMovement, double>>[] Modifiers => new Expression<Func<StockMovement, double>>[] { x => x.Dimensions.Value };
-
-        public FormulaOperator Operator => FormulaOperator.Multiply;
-        
-        public FormulaType Type => FormulaType.Virtual;
-    }
 
     [UserTracking("Warehousing")]
     public class StockMovement : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>, 
@@ -118,16 +83,26 @@ namespace Comal.Classes
                 .Then(Constant(true))
                 .Else(Constant(false));
         }
-
-        // IsRemnant = Dimensions.Value < Product.Dimensions.Value
+        /// <summary>
+        /// IsRemnant = Dimensions.Value &lt; Product.Dimensions.Value 
+        /// </summary>
         [CheckBoxEditor(Editable = Editable.Hidden)]
         [ComplexFormula(typeof(IsRemnantCondition))]
         [EditorSequence(7)]
         public bool IsRemnant { get; set; }
         
-        // Qty = Units * Dimensions.Value
+        private class QuantityFormula : ComplexFormulaGenerator<StockMovement, double>
+        {
+            public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
+                Formula(FormulaOperator.Multiply,
+                    Property(x => x.Units),
+                    Property(x => x.Dimensions.Value));
+        }
+        /// <summary>
+        /// Qty = Units * Dimensions.Value
+        /// </summary>
         [EditorSequence(8)]
-        [Formula(typeof(StockMovementQuantityFormula))]
+        [ComplexFormula(typeof(QuantityFormula))]
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         public double Qty { get; set; }
         
@@ -156,9 +131,23 @@ namespace Comal.Classes
         [EntityRelationship(DeleteAction.SetNull)]
         public EmployeeLink Employee { get; set; }
         
+        /// <summary>
+        /// To link StockMovements into conceptual blocks that cannot exist independently of each other; for example,
+        /// an issue may require transfers in and out which are intrinsically tied to the issue.
+        /// </summary>
+        /// <remarks>
+        /// <b>Important:</b> if this stock movement is a transfer, use <see cref="StockMovement.LinkTransfers(StockMovement, StockMovement, Guid?)"/>.
+        /// </remarks>
         [NullEditor]
         public Guid Transaction { get; set; } = Guid.NewGuid();
 
+        /// <summary>
+        /// To link TransferOut/TransferIn together pairwise. <b>Only</b> edit via the Link* methods provided by <see cref="StockMovement"/>, such
+        /// as <see cref="StockMovement.LinkTransfers(StockMovement, StockMovement, Guid?)"/>.
+        /// </summary>
+        [NullEditor]
+        public Guid TransferID { get; set; } = Guid.NewGuid();
+
         [NullEditor]
         public bool System { get; set; }
 
@@ -211,7 +200,15 @@ namespace Comal.Classes
 
         public ActualCharge Charge { get; set; }
 
-        [Aggregate(typeof(StockMovementDocumentCount))]
+        private class DocumentsCount : ComplexFormulaGenerator<StockMovement, int>
+        {
+            public override IComplexFormulaNode<StockMovement, int> GetFormula() =>
+                Count<StockMovementBatchDocument, Guid>(
+                    x => x.Property(x => x.ID))
+                .WithLink(x => x.EntityLink.ID, x => x.Batch.ID);
+        }
+
+        [ComplexFormula(typeof(DocumentsCount))]
         [NullEditor]
         public int Documents { get; set; }
 
@@ -226,11 +223,18 @@ namespace Comal.Classes
         [Obsolete("Replaced with Dimensions", true)]
         public double UnitSize { get; set; }
         
+        private class ValueFormula : ComplexFormulaGenerator<StockMovement, double>
+        {
+            public override IComplexFormulaNode<StockMovement, double> GetFormula() =>
+                Formula(FormulaOperator.Multiply,
+                    Property(x => x.Units),
+                    Property(x => x.Cost));
+        }
         /// <summary>
         /// Value of a stock movement, equal to <see cref="Units"/> * <see cref="Cost"/>.
         /// </summary>
         [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary=Summary.Sum)]
-        [Formula(typeof(StockMovementValueFormula))]
+        [ComplexFormula(typeof(ValueFormula))]
         public double Value { get; set; } = 0.0;
 
         [NullEditor]
@@ -248,6 +252,83 @@ namespace Comal.Classes
         [NullEditor]
         public string PostedReference { get; set; }
 
+        /// <summary>
+        /// Link this stock movement to <paramref name="other"/>, which must be a <see cref="StockMovementType.TransferIn"/>. This
+        /// will also set this movement to be <see cref="StockMovementType.TransferOut"/>.
+        /// </summary>
+        /// <remarks>
+        /// Links both <see cref="Transaction"/> and <see cref="TransferID"/>; if <paramref name="transaction"/> is provided, then
+        /// uses that instead of <paramref name="other"/>.Transaction.
+        /// </remarks>
+        public void LinkAsTransferOut(StockMovement other, Guid? transaction = null)
+        {
+            if(other.Type != StockMovementType.TransferIn)
+            {
+                throw new ArgumentException($"Provided stock movement is not a {nameof(StockMovementType.TransferIn)}", nameof(other));
+            }
+            else if(other.TransferID != Guid.Empty)
+            {
+                throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(other));
+            }
+            Transaction = transaction ?? other.Transaction;
+
+            var transfer = Guid.NewGuid();
+            other.TransferID = transfer;
+            TransferID = transfer;
+            Type = StockMovementType.TransferOut;
+        }
+
+        /// <summary>
+        /// Link this stock movement to <paramref name="other"/>, which must be a <see cref="StockMovementType.TransferOut"/>. This
+        /// will also set this movement to be <see cref="StockMovementType.TransferIn"/>.
+        /// </summary>
+        /// <remarks>
+        /// Links both <see cref="Transaction"/> and <see cref="TransferID"/>; if <paramref name="transaction"/> is provided, then
+        /// uses that instead of <paramref name="other"/>.Transaction.
+        /// </remarks>
+        public void LinkAsTransferIn(StockMovement other, Guid? transaction = null)
+        {
+            if(other.Type != StockMovementType.TransferOut)
+            {
+                throw new ArgumentException($"Provided stock movement is not a {nameof(StockMovementType.TransferOut)}", nameof(other));
+            }
+            else if(other.TransferID != Guid.Empty)
+            {
+                throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(other));
+            }
+            Transaction = transaction ?? other.Transaction;
+
+            var transfer = Guid.NewGuid();
+            other.TransferID = transfer;
+            TransferID = transfer;
+            Type = StockMovementType.TransferIn;
+        }
+
+        /// <summary>
+        /// Link the provided transfers together, and set them to have type <see cref="StockMovementType.TransferOut"/> and
+        /// <see cref="StockMovementType.TransferIn"/>, respectively.
+        /// </summary>
+        /// <exception cref="ArgumentException">If either transfer has already been linked to a transfer.</exception>
+        public static void LinkTransfers(StockMovement transferOut, StockMovement transferIn, Guid? transaction = null)
+        {
+            if(transferOut.TransferID != Guid.Empty)
+            {
+                throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(transferOut));
+            }
+            else if(transferIn.TransferID != Guid.Empty)
+            {
+                throw new ArgumentException($"Provided stock movement has already been linked to a transfer", nameof(transferIn));
+            }
+
+            transferOut.Type = StockMovementType.TransferOut;
+            if (transaction.HasValue)
+            {
+                transferOut.Transaction = transaction.Value;
+            }
+
+            transferIn.LinkAsTransferIn(transferOut, transaction);
+        }
+
         static StockMovement()
         {
             StockEntity.LinkStockDimensions<StockMovement>();

+ 3 - 4
prs.desktop/Panels/Factory/FactoryPackGrid.cs

@@ -441,9 +441,7 @@ public class FactoryPackGrid : DynamicDataGrid<StockHolding>
             var transferout = holding.CreateMovement();
             transferout.JobRequisitionItem.ID = ffi.RequisitionID;
             transferout.Batch.ID = batchid;
-            transferout.Type = StockMovementType.TransferOut;
             transferout.Issued = ffi.Qty;
-            transferout.Transaction = transactionid;
             transferout.System = true;
             transferout.Date = DateTime.Now;
             transferout.Employee.ID = App.EmployeeID;
@@ -452,14 +450,15 @@ public class FactoryPackGrid : DynamicDataGrid<StockHolding>
             var transferin = holding.CreateMovement();
             transferin.JobRequisitionItem.ID = ffi.RequisitionID;
             transferin.Batch.ID = batchid;
-            transferin.Type = StockMovementType.TransferIn;
             transferin.Received = ffi.Qty;
             transferin.Job.ID = CurrentPacket.SetoutLink.JobLink.ID;
             transferin.Job.Synchronise(CurrentPacket.SetoutLink.JobLink);
-            transferin.Transaction = transactionid;
             transferin.Date = DateTime.Now;
             transferin.System = true;
             transferin.Employee.ID = App.EmployeeID;
+
+            StockMovement.LinkTransfers(transferout, transferin, transactionid);
+
             updates.Add(transferin);
             
         }

+ 2 - 3
prs.desktop/Panels/Jobs/Stock Holdings/JobStockGrid.cs

@@ -105,7 +105,6 @@ public class JobStockGrid : DynamicDataGrid<StockHolding>, IMasterDetailControl<
                 from.Cost = StockRelease == StockReleaseWriteDownMethod.AverageCost
                     ? group.Key.Cost
                     : 0.0;
-                from.Type = StockMovementType.TransferOut;
                 from.Job.ID = Master.ID;
                 from.Issued = group.Units;
 
@@ -115,11 +114,11 @@ public class JobStockGrid : DynamicDataGrid<StockHolding>, IMasterDetailControl<
                     : 0.0;
                 to.OrderItem.ID = group.Key.OrderItem;
                 to.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
-                to.Type = StockMovementType.TransferIn;
-                to.Transaction = from.Transaction;
                 to.Units = group.Units;
                 to.Notes = $"Released from job {Master.JobNumber}";
 
+                StockMovement.LinkTransfers(from, to);
+
                 toSave.Add(from);
                 toSave.Add(to);
             }

+ 2 - 3
prs.desktop/Panels/Jobs/Summary/JobSummaryGrid.cs

@@ -107,7 +107,6 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
                     from.Cost = StockRelease == StockReleaseWriteDownMethod.AverageCost
                         ? holding.AverageValue
                         : 0.0;
-                    from.Type = StockMovementType.TransferOut;
                     from.Job.ID = Master.ID;
                     from.Issued = item.Qty;
 
@@ -118,11 +117,11 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
                     //to.OrderItem.ID = group.Key.OrderItem;
                     //to.JobRequisitionItem.ID = item.ID;
                     to.Job.ID = Guid.Empty;
-                    to.Type = StockMovementType.TransferIn;
-                    to.Transaction = from.Transaction;
                     to.Received = item.Qty;
                     to.Notes = $"Released from job {Master.JobNumber}";
 
+                    StockMovement.LinkTransfers(from, to);
+
                     updates.Add(from);
                     updates.Add(to);
                 }

+ 9 - 14
prs.desktop/Panels/Products/Locations/StockHoldingGrid.cs

@@ -229,15 +229,13 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
                     var transferout = holding.CreateMovement();
                     transferout.Date = DateTime.Now;
                     transferout.Issued = calculator.OldAvailable;
-                    transferout.Transaction = Guid.NewGuid();
-                    transferout.Type = StockMovementType.TransferOut;
 
                     var transferin = holding.CreateMovement();
                     transferin.Date = transferout.Date.AddTicks(1);
                     transferin.Dimensions.CopyFrom(calculator.Dimensions);
                     transferin.Received = calculator.NewAvailable;
-                    transferin.Transaction = transferout.Transaction;
-                    transferin.Type = StockMovementType.TransferIn;
+
+                    StockMovement.LinkTransfers(transferout, transferin);
 
                     SaveBatch(StockMovementBatchType.Transfer, [transferout, transferin], message: "Converted Dimensions");
                     DoChanged();
@@ -274,13 +272,12 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
             min.Cost = holding.AverageValue;
             min.Employee.ID = App.EmployeeID;
             min.Date = mout.Date;
-            min.Transaction = mout.Transaction;
 
             mout.Issued = qty.Value;
-            mout.Type = StockMovementType.TransferOut;
 
             min.Received = qty.Value;
-            min.Type = StockMovementType.TransferIn;
+
+            StockMovement.LinkTransfers(mout, min);
 
             modify(requi, mout, min);
 
@@ -433,24 +430,23 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         if (holding.Job.ID != issue.Job.ID)
         {
             var xferout = CreateMovementFromHolding(holding);
-            xferout.Type = StockMovementType.TransferOut;
             xferout.JobRequisitionItem.ID = requi.ID;
             xferout.Issued = qty;
-            xferout.Transaction = issue.Transaction;
             xferout.System = true;
             xferout.Notes = $"Issued by {App.EmployeeName}";
             xferout.Date = issue.Date;
-            yield return xferout;
             
             var xferin = CreateMovementFromHolding(holding);
             xferin.Job.ID = issue.Job.ID;
-            xferin.Type = StockMovementType.TransferIn;
             xferin.JobRequisitionItem.ID = requi.ID;
             xferin.Received = qty;
-            xferin.Transaction = issue.Transaction;
             xferin.System = true;
             xferin.Notes = $"Issued by {App.EmployeeName}";
             xferin.Date = issue.Date;
+
+            StockMovement.LinkTransfers(xferout, xferin, issue.Transaction);
+
+            yield return xferout;
             yield return xferin;
         }
     }
@@ -567,8 +563,7 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         {
             var other = CreateMovementFromHolding(holding);
             other.Issued = movement.Received;
-            other.Transaction = movement.Transaction;
-            other.Type = StockMovementType.TransferOut;
+            other.LinkAsTransferOut(movement);
 
             var changes = new List<string>();
             if (movement.Location.ID != other.Location.ID)

+ 3 - 3
prs.desktop/Panels/Reservation Management/ReservationManagementItemGrid.cs

@@ -310,7 +310,6 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
                         _transout.Employee.ID = _batch.Employee.ID;
                         _transout.Issued = _holding.Units;
                         _transout.Cost = _holding.AverageValue;
-                        _transout.Type = StockMovementType.TransferOut;
                         _transout.JobRequisitionItem.ID = _id;
                         _transout.Batch.ID = _batch.ID;
                         _transout.Notes = $"Consolidating requisition item holdings from {_holding.Location.Code} to {location.Code}";
@@ -322,11 +321,12 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
                         _transin.Location.ID = location.ID;
                         _transin.Received = _holding.Units;
                         _transin.Cost = _holding.AverageValue;
-                        _transin.Type = StockMovementType.TransferIn;
-                        _transin.Transaction = _transout.Transaction;
                         _transin.JobRequisitionItem.ID = _id;
                         _transin.Batch.ID = _batch.ID;
                         _transin.Notes = $"Consolidating requisition item holdings from {_holding.Location.Code} to {location.Code}";
+
+                        StockMovement.LinkTransfers(_transout, _transin);
+
                         _updates.Add(_transin);
                     }
                     

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

@@ -490,12 +490,10 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                     tOut.Employee.ID = App.EmployeeID;
                     tOut.Date = DateTime.Now;
                     tOut.Issued = item.Quantity;
-                    tOut.Type = StockMovementType.TransferOut;
                     tOut.JobRequisitionItem.CopyFrom(item.Item.JRI);
                     tOut.Notes = "Stock movement for treatment purchase order created from Reservation Management";
 
                     var tIn = tOut.CreateMovement();
-                    tIn.Transaction = tOut.Transaction;
 
                     tIn.Style.CopyFrom(item.Item.JRI.Style);
                     tIn.Location.ID = locationID;
@@ -503,10 +501,11 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                     tIn.Employee.ID = App.EmployeeID;
                     tIn.Date = tOut.Date;
                     tIn.Received = item.Quantity;
-                    tIn.Type = StockMovementType.TransferIn;
                     tIn.JobRequisitionItem.CopyFrom(item.Item.JRI);
                     tIn.Notes = "Stock movement for treatment purchase order created from Reservation Management";
 
+                    StockMovement.LinkTransfers(tOut, tIn);
+
                     if(holdings.TryGetValue((tOut.Product.ID, tOut.Style.ID, tOut.Location.ID, tOut.Job.ID, tOut.Dimensions), out var holding))
                     {
                         tOut.Cost = holding.AverageValue;

+ 2 - 3
prs.desktop/Panels/Reservation Management/StockSelectionPage.xaml.cs

@@ -208,14 +208,13 @@ public partial class StockSelectionPage : ThemableWindow, INotifyPropertyChanged
         issuing.Job.ID = IssuingJobID;
         issuing.Issued = item.Issued;
         issuing.JobRequisitionItem.ID = item.JRI?.ID ?? Guid.Empty;
-        issuing.Type = StockMovementType.TransferOut;
 
         var receiving = CreateBaseMovement(item, batch.ID, item.JRI);
         receiving.Job.ID = Item.Job.ID;
         receiving.Received = item.Issued;
         receiving.JobRequisitionItem.ID = Item.ID;
-        receiving.Type = StockMovementType.TransferIn;
-        receiving.Transaction = issuing.Transaction;
+
+        StockMovement.LinkTransfers(issuing, receiving);
 
         Client.Save(new StockMovement[] { issuing, receiving }, "Created from Reservation Management Screen");
     }

+ 2 - 4
prs.desktop/Panels/Reservation Management/Substitution/ReservationManagementSubstitutionWindow.xaml.cs

@@ -55,8 +55,6 @@ public partial class ReservationManagementSubstitutionWindow : Window
             var _xferout = _holding.CreateMovement();
             _xferout.Issued = Math.Min(_holding.Available, substitutions.JRI.Qty);
             _xferout.Notes = "Substituted by Requisition Management Screen";
-            _xferout.Transaction = transaction;
-            _xferout.Type = StockMovementType.TransferOut;
             _xferout.Employee.ID = App.EmployeeID;
             _xferout.Cost = _holding.AverageValue;
         
@@ -65,10 +63,10 @@ public partial class ReservationManagementSubstitutionWindow : Window
             _xferin.Job.CopyFrom(substitutions.JRI.Job);
             _xferin.Received = Math.Min(_holding.Available, substitutions.JRI.Qty);
             _xferin.Notes = "Substituted by Requisition Management Screen";
-            _xferin.Transaction = transaction;
-            _xferin.Type = StockMovementType.TransferIn;
             _xferin.Employee.ID = App.EmployeeID;
             _xferin.Cost = _holding.AverageValue;
+
+            StockMovement.LinkTransfers(_xferout, _xferin, transaction);
         
             Client.Save(new[] { _xferout, _xferin}, "Substituted by Requisition Management Screen");
         });

+ 1 - 0
prs.shared/Database Update Scripts/DatabaseUpdateScripts.cs

@@ -62,5 +62,6 @@ public static class DatabaseUpdateScripts
         DataUpdater.RegisterUpdateScript<Update_8_34>();
         DataUpdater.RegisterUpdateScript<Update_8_47>();
         DataUpdater.RegisterUpdateScript<Update_8_49>();
+        DataUpdater.RegisterUpdateScript<Update_8_55>();
     }
 }

+ 105 - 0
prs.shared/Database Update Scripts/Update_8_55.cs

@@ -0,0 +1,105 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.Database;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRS.Shared;
+
+internal class Update_8_55 : DatabaseUpdateScript
+{
+    public override VersionNumber Version => new(8, 55);
+
+    public override bool Update()
+    {
+        Logger.Send(LogType.Information, "", $"Updating uninitialised stock movement TransferIDs");
+
+        var transactionIDs = DbFactory.NewProvider(Logger.Main)
+            .Query(
+                Filter<StockMovement>.Where(x => x.Type).InList(StockMovementType.TransferOut, StockMovementType.TransferIn)
+                    .And(x => x.TransferID).IsEqualTo(null),
+                Columns.None<StockMovement>()
+                    .Add(x => x.Transaction))
+            .Rows
+            .Select(x => x.Get<StockMovement, Guid>(x => x.Transaction))
+            .ToHashSet();
+
+        Logger.Send(LogType.Information, "", $"Found {transactionIDs.Count} uninitialised transactions");
+
+        var total = transactionIDs.Count;
+
+        while (transactionIDs.Count > 0)
+        {
+            Logger.Send(LogType.Information, "", $"Updating Stock Movements: {(double)(total - transactionIDs.Count) / total * 100:F2}%");
+
+            var nIDs = Math.Min(transactionIDs.Count, 100);
+            var ids = new Guid[nIDs];
+
+            var i = 0;
+            foreach(var id in transactionIDs)
+            {
+                if (i == nIDs) break;
+
+                ids[i] = id;
+                ++i;
+            }
+            foreach(var id in ids)
+            {
+                transactionIDs.Remove(id);
+            }
+
+            var movementsTable = DbFactory.NewProvider(Logger.Main)
+                .Query(
+                    Filter<StockMovement>.Where(x => x.Transaction).InList(ids)
+                        .And(x => x.Type).InList(StockMovementType.TransferOut, StockMovementType.TransferIn)
+                        .And(x => x.TransferID).IsEqualTo(null),
+                    Columns.None<StockMovement>()
+                        .Add(x => x.ID)
+                        .Add(x => x.Transaction)
+                        .Add(x => x.Type)
+                        .Add(x => x.Date)
+                        .Add(x => x.System));
+            var movements = 
+                movementsTable
+                .ToObjects<StockMovement>()
+                .GroupByDictionary(x => x.Transaction);
+            
+            Logger.Send(LogType.Information, "", $"Updating {movementsTable.Rows.Count} movements");
+
+            var updates = new List<StockMovement>();
+            foreach(var (transaction, transactionMovements) in movements)
+            {
+                transactionMovements.SortBy(x => x.Date);
+
+                while(transactionMovements.Count > 0)
+                {
+                    var movement = transactionMovements[0];
+                    transactionMovements.RemoveAt(0);
+
+                    var otherType = movement.Type == StockMovementType.TransferOut ? StockMovementType.TransferIn : StockMovementType.TransferOut;
+
+                    var transfer = Guid.NewGuid();
+                    movement.TransferID = transfer;
+
+                    var first = transactionMovements.FirstOrDefault(x => x.Type == otherType);
+                    if(first is not null)
+                    {
+                        first.TransferID = transfer;
+                        transactionMovements.Remove(first);
+
+                        updates.Add(movement);
+                        updates.Add(first);
+                    }
+                }
+            }
+            DbFactory.NewProvider(Logger.Main).Save(updates);
+        }
+
+        Logger.Send(LogType.Information, "", $"Finished updating uninitialised stock movement TransferIDs");
+        return true;
+    }
+}

+ 6 - 5
prs.stores/JobRequisitionItemStore.cs

@@ -71,7 +71,6 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
             to.System = true;
             to.Notes = "Requisition item cancelled";
             to.OrderItem.ID = movement.OrderItem.ID;
-            to.Transaction = from.Transaction;
 
             if(movement.Units > 0)
             {
@@ -79,8 +78,7 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
                 from.Issued = movement.Units;
                 to.Received = movement.Units;
 
-                from.Type = StockMovementType.TransferOut;
-                to.Type = StockMovementType.TransferIn;
+                StockMovement.LinkTransfers(transferOut: from, transferIn: to);
             }
             else if(movement.Units < 0)
             {
@@ -88,8 +86,11 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
                 from.Received = -movement.Units;
                 to.Issued = -movement.Units;
 
-                from.Type = StockMovementType.TransferIn;
-                to.Type = StockMovementType.TransferOut;
+                StockMovement.LinkTransfers(transferIn: from, transferOut: to);
+            }
+            else
+            {
+                continue;
             }
 
             newMovements.Add(from);

+ 2 - 39
prs.stores/PurchaseOrderItemStore.cs

@@ -320,58 +320,21 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
         tOut.Notes = "Internal transfer from cancelled requisition";
         tOut.System = true;
         tOut.Cost = entity.Cost;
-        tOut.Transaction = transactionID;
-        tOut.Type = StockMovementType.TransferOut;
 
         var tIn = movement.CreateMovement();
-        tIn.Transaction = transactionID;
         tIn.Date = tOut.Date.AddTicks(1);
         tIn.Received = qty;
         tIn.OrderItem.ID = entity.ID;
         tOut.Notes = "Internal transfer from cancelled requisition";
         tOut.System = true;
         tIn.Cost = entity.Cost;
-        tIn.Type = StockMovementType.TransferIn;
+
+        StockMovement.LinkTransfers(tOut, tIn, transactionID);
 
         movements.Add(tOut);
         movements.Add(tIn);
     }
 
-    private static StockMovement CreateStockMovement(
-        List<StockMovement> movements,
-        PurchaseOrderItem entity,
-        Guid locationID,
-        IJob? job, IJobRequisitionItem? jri,
-        double cost, Guid transactionID)
-    {
-        var movement = new StockMovement();
-        movement.Product.ID = entity.Product.ID;
-        movement.Location.ID = locationID;
-        movement.Style.ID = entity.Style.ID;
-        if(job is not null)
-        {
-            movement.Job.ID = job.ID;
-        }
-        movement.Dimensions.CopyFrom(entity.Dimensions);
-
-        var lastMovement = movements.Count > 0 ? movements[^1] : null;
-
-        movement.Date = lastMovement is not null ? lastMovement.Date.AddTicks(1) : entity.ReceivedDate;
-        movement.Employee.ID = Guid.Empty;
-        movement.Cost = cost;
-
-        movement.Transaction = transactionID;
-
-        if (jri is not null)
-        {
-            movement.JobRequisitionItem.ID = jri.ID;
-        }
-
-        movements.Add(movement);
-
-        return movement;
-    }
-
     private static StockMovement CreateReceive(List<StockMovement> movements, PurchaseOrderItem entity, Guid locationid, IJob? job, IJobRequisitionItem? jri, double qty, double cost, Guid transactionID)
     {
         var movement = new StockMovement();

+ 4 - 4
prs.stores/RequisitionStore.cs

@@ -321,7 +321,6 @@ namespace Comal.Stores
                     var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, null, null,
                         dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer");
                     from.Issued = extraRequired;
-                    from.Type = StockMovementType.TransferOut;
                     from.Cost = item.Cost;
                     timestamp = timestamp.AddTicks(1);
 
@@ -329,10 +328,11 @@ namespace Comal.Stores
                     var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid, item.Charge, item.JobScope, true,
                         $"Requisition #{entity.Number} Internal Transfer");
                     to.Received = extraRequired;
-                    to.Type = StockMovementType.TransferIn;
                     to.Cost = item.Cost;
                     timestamp = timestamp.AddTicks(1);
 
+                    StockMovement.LinkTransfers(from, to, txnid);
+
                     updates.Add(from);
                     updates.Add(to);
 
@@ -346,7 +346,6 @@ namespace Comal.Stores
                     var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, item.JobLink, item.SourceJRI,
                         dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer");
                     from.Issued = qty;
-                    from.Type = StockMovementType.TransferOut;
                     from.Cost = item.Cost;
                     timestamp = timestamp.AddTicks(1);
 
@@ -354,10 +353,11 @@ namespace Comal.Stores
                     var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid, item.Charge, item.JobScope, true,
                         $"Requisition #{entity.Number} Internal Transfer");
                     to.Received = qty;
-                    to.Type = StockMovementType.TransferIn;
                     to.Cost = item.Cost;
                     timestamp = timestamp.AddTicks(1);
 
+                    StockMovement.LinkTransfers(from, to, txnid);
+
                     updates.Add(from);
                     updates.Add(to);
                 }