Przeglądaj źródła

Merge commit '9763c428e222312aa330472b8364cc2e4dd900c1' into frank

Frank van den Bos 9 miesięcy temu
rodzic
commit
bea84938bb

+ 43 - 14
prs.classes/EnclosedEntities/Dimensions/DimensionUnit.cs

@@ -130,11 +130,16 @@ namespace Comal.Classes
                 return variables;
             }
         }
+
+        public override string ToString()
+        {
+            return $"(ProductDimensionUnit: {Code})";
+        }
     }
 
     public static class DimensionUnitUtils
     {
-        public static Dictionary<Type, int> UpdateExpressions<T, TLink>(T[] items)
+        public static Dictionary<Type, int> UpdateExpressions<T, TLink>(T[] items, IProgress<string> progress)
             where T : DimensionUnit, new()
             where TLink : DimensionUnitLink<T>
         {
@@ -156,10 +161,6 @@ namespace Comal.Classes
                     && entity.HasInterface<IRemotable>()
                     && entity.HasInterface<IPersistent>())
                 {
-                    if(entity == typeof(SupplierProduct))
-                    {
-                        var props = DatabaseSchema.Properties(entity).OrderBy(x => x.Name).ToArray();
-                    }
                     foreach(var property in DatabaseSchema.Properties(entity))
                     {
                         if (property.Parent is null
@@ -172,8 +173,8 @@ namespace Comal.Classes
                         var dimType = dimensionTypes.FirstOrDefault(x => property.Parent.Parent.PropertyType == x.dimType);
                         if(dimType.dimType != null)
                         {
-                            var dict = updateTypes.GetValueOrAdd(entity);
-                            dict.Add(property.Parent.Parent);
+                            var propList = updateTypes.GetValueOrAdd(entity);
+                            propList.Add(property.Parent.Parent);
                         }
                     }
                 }
@@ -211,26 +212,54 @@ namespace Comal.Classes
                 }
                 if(filter != null)
                 {
-                    var results = Client.Create(type).Query(filter, columns).ToObjects(type).Cast<Entity>().ToArray();
-                    if(results.Length > 0)
+                    progress.Report($"Updating {CoreUtils.Neatify(type.GetCaption())}");
+
+                    var nTotal = Client.Create(type).Query(filter, Columns.None(type).Add("ID")).Rows.Count;
+                    var nProcessed = 0;
+                    var nResult = 0;
+                    var done = false;
+
+                    var percentStep = Math.Max(nTotal / 100, 1);
+                    var range = CoreRange.Database(1000);
+
+                    while(nProcessed < nTotal && !done)
                     {
-                        foreach(var result in results)
+                        var rows = Client.Create(type).Query(filter, columns, range: range).Rows;
+                        if (rows.Count == 0) break;
+                        if(rows.Count < 1000)
                         {
+                            done = true;
+                        }
+                        range.Next();
+
+                        var results = new List<Entity>(rows.Count);
+                        for(int i = 0; i < rows.Count; ++i)
+                        {
+                            if(nProcessed % percentStep == 0)
+                            {
+                                progress.Report($"Updating {CoreUtils.Neatify(type.GetCaption())}: {(double)nProcessed / (double)nTotal * 100:F0}%");
+                            }
+                            var obj = (rows[i].ToObject(type) as Entity)!;
                             foreach(var property in properties)
                             {
-                                var id = CoreUtils.GetPropertyValue(result, property.Name + "." + Dimensions.unitid.Property);
+                                var id = CoreUtils.GetPropertyValue(obj, property.Name + "." + Dimensions.unitid.Property);
                                 if(id is Guid guid)
                                 {
                                     var unit = items.First(x => x.ID == guid);
-                                    var dim = (property.Getter()(result) as IDimensions)!;
+                                    var dim = (property.Getter()(obj) as IDimensions)!;
                                     dim.Calculate(dim.Quantity, dim.Length, dim.Width, dim.Height, dim.Weight, unit.Formula, unit.Format);
                                 }
                             }
+                            if (obj.IsChanged())
+                            {
+                                results.Add(obj);
+                                nResult++;
+                            }
+                            nProcessed++;
                         }
-                        results = results.Where(x => x.IsChanged()).ToArray();
-                        nResults[type] = results.Length;
                         Client.Create(type).Save(results, "Updated Value and UnitSize to match dimension unit.");
                     }
+                    nResults[type] = nResult;
                 }
             }
             return nResults;

+ 36 - 2
prs.desktop/Grids/ProductDimensionUnitGrid.cs

@@ -16,6 +16,13 @@ public class ProductDimensionUnitGrid : DynamicDataGrid<ProductDimensionUnit>
     {
         base.Init();
 
+        HiddenColumns.Add(x => x.Code);
+        HiddenColumns.Add(x => x.HasQuantity);
+        HiddenColumns.Add(x => x.HasLength);
+        HiddenColumns.Add(x => x.HasWeight);
+        HiddenColumns.Add(x => x.HasWidth);
+        HiddenColumns.Add(x => x.HasHeight);
+
         AddButton("Update Expressions", null, (button, rows) =>
         {
             UpdateExpressions(rows.ToArray<ProductDimensionUnit>());
@@ -38,7 +45,7 @@ public class ProductDimensionUnitGrid : DynamicDataGrid<ProductDimensionUnit>
         {
             try
             {
-                results = DimensionUnitUtils.UpdateExpressions<ProductDimensionUnit, ProductDimensionUnitLink>(items);
+                results = DimensionUnitUtils.UpdateExpressions<ProductDimensionUnit, ProductDimensionUnitLink>(items, progress);
             }
             catch(Exception e)
             {
@@ -83,7 +90,34 @@ public class ProductDimensionUnitGrid : DynamicDataGrid<ProductDimensionUnit>
 
     protected override bool DoMerge(CoreRow[] rows)
     {
-        return base.DoMerge(rows);
+        var columns = new Column<ProductDimensionUnit>[]
+        {
+            new(x => x.HasLength),
+            new(x => x.HasQuantity),
+            new(x => x.HasHeight),
+            new(x => x.HasWeight),
+            new(x => x.HasWidth),
+        };
+
+        var target = rows[^1].ToObject<ProductDimensionUnit>();
+        if(columns.Any(c => rows.Select(r => r[c.Property]).Distinct().Skip(1).Any()))
+        {
+            MessageWindow.ShowMessage("These dimension units are incompatible, and cannot be merged.\n\n(Dimension units can only be merged if they have the same [HasQuantity], [HasLength], [HasWidth], [HasHeight] and [HasWeight] values).", "Incompatible units", image: MessageWindow.WarningImage);
+            return false;
+        }
+
+        if (base.DoMerge(rows))
+        {
+            if(MessageWindow.ShowYesNo(
+                $"Do you wish to update the UnitSize/Value for every item that uses {target.Code}? (This may take a while)",
+                "Update Expressions?"))
+            {
+                UpdateExpressions([target]);
+            }
+
+            return true;
+        }
+        return false;
     }
 
     protected override void CustomiseEditor(ProductDimensionUnit[] items, DynamicGridColumn column, BaseEditor editor)

+ 40 - 1
prs.desktop/Panels/Jobs/Summary/JobSummaryGrid.cs

@@ -209,6 +209,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         HiddenColumns.Add(x => x.Product.ID);
         HiddenColumns.Add(x => x.Style.ID);
         HiddenColumns.Add(x => x.Dimensions.UnitSize);
+        HiddenColumns.Add(x => x.Dimensions.Value);
 
         HiddenColumns.Add(x => x.BillOfMaterials);
         HiddenColumns.Add(x => x.Requisitions);
@@ -260,6 +261,25 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         return VisibleColumns.Any(x => x.ColumnName.StartsWith(dimColumn));
     }
 
+    private bool? _hasUnitSize;
+    private bool UnitSizeColumnVisible()
+    {
+        if (_hasUnitSize.HasValue)
+        {
+            return _hasUnitSize.Value;
+        }
+        var dimColumn = CoreUtils.GetFullPropertyName<JobMaterial, string>(x => x.Dimensions.UnitSize, ".");
+
+        _hasUnitSize = VisibleColumns.Any(x => x.ColumnName.Equals(dimColumn));
+        return _hasUnitSize.Value;
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        _hasUnitSize = null;
+        return base.LoadColumns();
+    }
+
     public override DynamicGridColumns GenerateColumns()
     {
         var columns = new DynamicGridColumns();
@@ -751,14 +771,33 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
 
                     var hasStyle = StyleColumnVisible();
                     var hasDimensions = DimensionsColumnVisible();
+                    var hasUnitSize = UnitSizeColumnVisible();
                     var keys = GetKeys(data.Rows, columns, hasStyle, hasDimensions);
 
                     foreach (var key in keys)
                     {
 
                         var rows = GetRows(data.Rows, columns, key.JobID, key);
-                        if (rows.Any())
+                        if (rows.Length != 0)
                         {
+                            if (!hasUnitSize)
+                            {
+                                foreach(var row in rows)
+                                {
+                                    var multFactor = row.Get<JobMaterial, double>(x => x.Dimensions.Value);
+                                    row.Update<JobMaterial, double>(x => x.BillOfMaterials, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.Requisitions, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.PickingLists, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.Issued, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.ReservedStock, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.OnOrder, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.JobShortage, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.FreeOnHand, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.FreeOnOrder, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.FreeStockTotal, x => x * multFactor);
+                                    row.Update<JobMaterial, double>(x => x.FreeStockShortage, x => x * multFactor);
+                                }
+                            }
 
                             CoreRow newrow = table.NewRow();
                             newrow.LoadValues(rows.First().Values);

+ 8 - 1
prs.shared/Posters/Timberline/BillTimberlinePoster.cs

@@ -33,9 +33,16 @@ public class BillTimberlineHeader
     [TypeConverter(typeof(TimberlinePosterStringConverter), 10)]
     public string Vendor { get; set; } = "";
 
+    [Ignore]
+    private string _invoice = "";
+
     [Index(2)]
     [TypeConverter(typeof(TimberlinePosterStringConverter), 15)]
-    public string Invoice { get; set; } = "";
+    public string Invoice
+    {
+        get => _invoice;
+        set => _invoice = TimberlinePosterStringConverter.ConvertString(value, 15);
+    }
 
     [Index(3)]
     [TypeConverter(typeof(TimberlinePosterStringConverter), 30)]

+ 1 - 1
prs.stores/EmailStore.cs

@@ -49,7 +49,7 @@ namespace Comal.Stores
             //base.OnSave(entity);
         }
 
-        protected override void OnSave(IEnumerable<Email> entities, ref string auditnote)
+        protected override void OnSave(Email[] entities, ref string auditnote)
         {
             //base.OnSave(entities);
         }

+ 1 - 1
prs.stores/GPSTrackerLocationStore.cs

@@ -132,7 +132,7 @@ namespace Comal.Stores
             auditnote = null;
         }
 
-        protected override void OnSave(IEnumerable<GPSTrackerLocation> entities, ref string auditnote)
+        protected override void OnSave(GPSTrackerLocation[] entities, ref string auditnote)
         {
             var updates = entities.Where(x => !Equals(x.Tracker.ID, Guid.Empty)).ToArray();
             if (updates.Any())

+ 1 - 1
prs.stores/ManufacturingHistoryStore.cs

@@ -139,7 +139,7 @@ namespace Comal.Stores
             //base.OnSave(entity);
         }
 
-        protected override void OnSave(IEnumerable<ManufacturingHistory> entities, ref string auditnote)
+        protected override void OnSave(ManufacturingHistory[] entities, ref string auditnote)
         {
             //base.OnSave(entities);
         }

+ 1 - 1
prs.stores/ModuleTrackingStore.cs

@@ -51,7 +51,7 @@ namespace Comal.Stores
             //base.OnSave(entity);
         }
 
-        protected override void OnSave(IEnumerable<ModuleTracking> entities, ref string auditnote)
+        protected override void OnSave(ModuleTracking[] entities, ref string auditnote)
         {
             //base.OnSave(entities);
         }

+ 1 - 9
prs.stores/PurchaseOrderItemStore.cs

@@ -267,15 +267,7 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
                 instance = new ProductInstance();
                 instance.Product.ID = entity.Product.ID;
                 instance.Style.ID = entity.Style.ID;
-                instance.Dimensions.Unit.ID = entity.Dimensions.Unit.ID;
-                instance.Dimensions.Height = entity.Dimensions.Height;
-                instance.Dimensions.Length = entity.Dimensions.Length;
-                instance.Dimensions.Width = entity.Dimensions.Width;
-                instance.Dimensions.Weight = entity.Dimensions.Weight;
-                instance.Dimensions.Quantity = entity.Dimensions.Quantity;
-                instance.Dimensions.UnitSize = entity.Dimensions.UnitSize;
-                instance.Dimensions.Value = entity.Dimensions.Value;
-                instance.Dimensions.UnitSize = entity.Dimensions.UnitSize;
+                instance.Dimensions.CopyFrom(entity.Dimensions);
             }
 
             instance.LastCost = entity.Cost;

+ 116 - 0
prs.stores/StockHoldingStore.cs

@@ -3,9 +3,12 @@ using InABox.Core;
 using InABox.Database;
 using System.Linq;
 using System;
+using MathNet.Numerics;
 
 namespace Comal.Stores;
 
+using HoldingDictionary = Dictionary<(Guid product, Guid style, Guid location, Guid job, StockDimensions dimensions), StockHolding>;
+
 public class StockHoldingStore : BaseStore<StockHolding>
 {
 
@@ -15,6 +18,119 @@ public class StockHoldingStore : BaseStore<StockHolding>
         Decrease
     }
 
+    public static StockMovement[] LoadMovementData(IStore store, Guid[] ids)
+    {
+        return store.Provider.Query(
+            new Filter<StockMovement>(x => x.ID).InList(ids),
+            Columns.None<StockMovement>().Add(x => x.ID)
+                .Add(x => x.Location.ID)
+                .Add(x => x.Product.ID)
+                .Add(x => x.Style.ID)
+                .Add(x => x.Job.ID)
+                .Add(x => x.Dimensions.Unit.ID)
+                .Add(x => x.Dimensions.Quantity)
+                .Add(x => x.Dimensions.Height)
+                .Add(x => x.Dimensions.Width)
+                .Add(x => x.Dimensions.Length)
+                .Add(x => x.Dimensions.Weight)
+                .Add(x => x.Dimensions.UnitSize)
+                .Add(x => x.Dimensions.Value)
+                .Add(x => x.JobRequisitionItem.ID)
+                .Add(x => x.Units)
+                .Add(x => x.Cost)
+        ).ToArray<StockMovement>();
+    }
+
+    public static HoldingDictionary LoadStockHoldings(IStore store, StockMovement[] mvts, HoldingDictionary? holdings = null)
+    {
+        if(holdings is not null)
+        {
+            mvts = mvts.Where(mvt =>
+            {
+                var key = (mvt.Product.ID, mvt.Style.ID, mvt.Location.ID, mvt.Job.ID, mvt.Dimensions);
+                return !holdings.ContainsKey(key);
+            }).ToArray();
+        }
+        else
+        {
+            holdings = new();
+        }
+
+        var productIDs = mvts.Select(x => x.Product.ID).Distinct().ToArray();
+        var locationIDs = mvts.Select(x => x.Location.ID).Distinct().ToArray();
+        var styleIDs = mvts.Select(x => x.Style.ID).Distinct().ToArray();
+        var jobIDs = mvts.Select(x => x.Job.ID).Distinct().ToArray();
+
+        var newHoldings = store.Provider.Query(new Filter<StockHolding>(x => x.Product.ID).InList(productIDs)
+                .And(x => x.Location.ID).InList(locationIDs)
+                .And(x => x.Style.ID).InList(styleIDs)
+                .And(x => x.Job.ID).InList(jobIDs),
+                Columns.None<StockHolding>().Add(x => x.ID)
+                    .Add(x => x.Units)
+                    .Add(x => x.Qty)
+                    .Add(x => x.Value)
+                    .Add(x => x.Available)
+                    .Add(x => x.Weight)
+                    .Add(x => x.AverageValue)
+                    .Add(x => x.Product.ID)
+                    .Add(x => x.Location.ID)
+                    .Add(x => x.Style.ID)
+                    .Add(x => x.Job.ID)
+                    .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
+            ).ToObjects<StockHolding>();
+        foreach(var holding in newHoldings)
+        {
+            holdings[(holding.Product.ID, holding.Style.ID, holding.Location.ID, holding.Job.ID, holding.Dimensions)] = holding;
+        }
+
+        return holdings;
+    }
+
+    public static void ModifyHoldings(StockMovement[] mvts, HoldingDictionary holdings, Action action)
+    {
+        foreach(var mvt in mvts)
+        {
+            var key = (mvt.Product.ID, mvt.Style.ID, mvt.Location.ID, mvt.Job.ID, mvt.Dimensions);
+            var holding = holdings.GetValueOrDefault(key);
+            if(holding is null)
+            {
+                holding = new();
+                holding.Location.ID = mvt.Location.ID;
+                holding.Product.ID = mvt.Product.ID;
+                holding.Style.ID = mvt.Style.ID;
+                holding.Job.ID = mvt.Job.ID;
+                holding.Dimensions.CopyFrom(mvt.Dimensions);
+                holdings[key] = holding;
+            }
+
+            double multiplier = action == Action.Increase ? 1F : -1F;
+            holding.Units += (multiplier * mvt.Units);
+            holding.Qty += (multiplier * mvt.Units * mvt.Dimensions.Value);
+            holding.Value += (multiplier * mvt.Units * mvt.Cost);
+            holding.Available += (multiplier * (mvt.JobRequisitionItem.ID == Guid.Empty ? mvt.Units : 0.0));
+            
+            holding.Weight = holding.Qty * holding.Dimensions.Weight;
+            holding.AverageValue = holding.Units != 0 ? holding.Value / holding.Units : 0.0F;
+        }
+    }
+
+    public static void SaveHoldings(IStore store, HoldingDictionary holdings)
+    {
+        var holdingStore = store.FindSubStore<StockHolding>();
+        holdingStore.Delete(
+            holdings.Values.Where(x => x.ID != Guid.Empty && x.Units.IsEffectivelyEqual(0.0) && x.Available.IsEffectivelyEqual(0.0)), "");
+        holdingStore.Save(
+            holdings.Values.Where(x => x.IsChanged() && (!x.Units.IsEffectivelyEqual(0.0) || !x.Available.IsEffectivelyEqual(0.0))), "");
+    }
+
+    public static void UpdateStockHoldings(IStore store, Guid[] ids, Action action)
+    {
+        var movements = LoadMovementData(store, ids);
+        var holdings = LoadStockHoldings(store, movements);
+        ModifyHoldings(movements, holdings, action);
+        SaveHoldings(store, holdings);
+    }
+
     /// <summary>
     /// Maintains the Stock Holding Table when manipulating Stock Movements
     /// We only accept an ID, because the rest of the movement is pulled from the database

+ 74 - 42
prs.stores/StockMovementStore.cs

@@ -9,60 +9,92 @@ using InABox.Database;
 
 namespace PRSStores;
 
+using HoldingDictionary = Dictionary<(Guid product, Guid style, Guid location, Guid job, StockDimensions dimensions), StockHolding>;
+
 public class StockMovementStore : BaseStore<StockMovement>
 {
-    protected override void BeforeSave(StockMovement sm)
-    {
-        base.BeforeSave(sm);
-        
-        // If this movement is an Update (instead of Insert),
-        // we need to reduce the old stock holding before updating the new one
-        if (sm.ID != Guid.Empty)
-            StockHoldingStore.UpdateStockHolding(this, sm.ID,StockHoldingStore.Action.Decrease);
+    // These will be initialised in BeforeSave
+    HoldingDictionary holdingData = null!;
+    StockMovement[] mvtData = null!;
 
-        if(sm.Job.HasOriginalValue(x => x.ID) && sm.Job.ID != Guid.Empty && sm.JobScope.ID == Guid.Empty)
+    protected override void BeforeSave(IEnumerable<StockMovement> entities)
+    {
+        foreach(var entity in entities)
         {
-            // If we have updated the Job.ID to a non-empty value, we should
-            // update the JobScope to the default job scope for that job.
+            // Calling base BeforeSave so that it doesn't call our other BeforeSave method.
+            base.BeforeSave(entity);
+        }
 
-            var scopeID = Guid.Empty;
-            if(sm.ID != Guid.Empty)
-            {
-                // It's possible that the JobScope ID just wasn't passed up, if
-                // this entity already exists; hence, we shall load the scopeID
-                // from the database just in case.
-                scopeID = Provider.Query(
-                    new Filter<StockMovement>(x => x.ID).IsEqualTo(sm.ID),
-                    Columns.None<StockMovement>().Add(x => x.JobScope.ID))
-                    .Rows.FirstOrDefault()?.Get<StockMovement, Guid>(x => x.JobScope.ID) ?? Guid.Empty;
-            }
-            if(scopeID == Guid.Empty)
+        mvtData = StockHoldingStore.LoadMovementData(this, entities.Select(x => x.ID).ToArray());
+        holdingData = StockHoldingStore.LoadStockHoldings(this, mvtData);
+
+        StockHoldingStore.ModifyHoldings(mvtData, holdingData, StockHoldingStore.Action.Decrease);
+
+        foreach(var sm in entities)
+        {
+            if(sm.Job.HasOriginalValue(x => x.ID) && sm.Job.ID != Guid.Empty && sm.JobScope.ID == Guid.Empty)
             {
-                // No scope has been assigned; however, we have a job, so we
-                // load the default scope for the job.
-                sm.JobScope.ID = Provider.Query(
-                    new Filter<Job>(x => x.ID).IsEqualTo(sm.Job.ID),
-                    Columns.None<Job>().Add(x => x.DefaultScope.ID))
-                    .Rows.FirstOrDefault()?.Get<Job, Guid>(x => x.DefaultScope.ID) ?? Guid.Empty;
+                // If we have updated the Job.ID to a non-empty value, we should
+                // update the JobScope to the default job scope for that job.
+
+                var scopeID = Guid.Empty;
+                if(sm.ID != Guid.Empty)
+                {
+                    // It's possible that the JobScope ID just wasn't passed up, if
+                    // this entity already exists; hence, we shall load the scopeID
+                    // from the database just in case.
+                    scopeID = Provider.Query(
+                        new Filter<StockMovement>(x => x.ID).IsEqualTo(sm.ID),
+                        Columns.None<StockMovement>().Add(x => x.JobScope.ID))
+                        .Rows.FirstOrDefault()?.Get<StockMovement, Guid>(x => x.JobScope.ID) ?? Guid.Empty;
+                }
+                if(scopeID == Guid.Empty)
+                {
+                    // No scope has been assigned; however, we have a job, so we
+                    // load the default scope for the job.
+                    sm.JobScope.ID = Provider.Query(
+                        new Filter<Job>(x => x.ID).IsEqualTo(sm.Job.ID),
+                        Columns.None<Job>().Add(x => x.DefaultScope.ID))
+                        .Rows.FirstOrDefault()?.Get<Job, Guid>(x => x.DefaultScope.ID) ?? Guid.Empty;
+                }
             }
         }
     }
 
-    protected override void AfterSave(StockMovement sm)
+    protected override void BeforeSave(StockMovement sm)
+    {
+        BeforeSave(CoreUtils.One(sm));
+    }
+
+    protected override void AfterSave(IEnumerable<StockMovement> entities)
     {
         // Update the Relevant StockHolding with the details of this movement
-        StockHoldingStore.UpdateStockHolding(this, sm.ID,StockHoldingStore.Action.Increase);
-        
-        // Update the Job requisition item status (if applicable)
-        if (sm.JobRequisitionItem.ID != Guid.Empty)
-            JobRequisitionItemStore.UpdateStatus(
-                this, 
-                sm.JobRequisitionItem.ID, 
-                sm.HasOriginalValue(x=>x.ID) 
-                    ? JobRequisitionItemAction.Created 
-                    : JobRequisitionItemAction.Updated
-            );
-        base.AfterSave(sm);
+        StockHoldingStore.ModifyHoldings(mvtData, holdingData, StockHoldingStore.Action.Increase);
+        StockHoldingStore.SaveHoldings(this, holdingData);
+
+        foreach(var sm in entities)
+        {
+            // Update the Job requisition item status (if applicable)
+            if (sm.JobRequisitionItem.ID != Guid.Empty)
+                JobRequisitionItemStore.UpdateStatus(
+                    this, 
+                    sm.JobRequisitionItem.ID, 
+                    sm.HasOriginalValue(x=>x.ID) 
+                        ? JobRequisitionItemAction.Created 
+                        : JobRequisitionItemAction.Updated
+                );
+        }
+
+        foreach(var entity in entities)
+        {
+            // Calling base AfterSave so that it doesn't call our other AfterSave method.
+            base.AfterSave(entity);
+        }
+    }
+
+    protected override void AfterSave(StockMovement sm)
+    {
+        AfterSave(CoreUtils.One(sm));
     }
 
     protected override void BeforeDelete(StockMovement entity)