Browse Source

Bulk stock holding relocation on Stock Holdings grid

Kenric Nugteren 6 days ago
parent
commit
324085acdb

+ 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>

+ 167 - 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,45 @@ 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.Issued = qty.Value;
+            mout.Cost = holding.AverageValue;
+            mout.Date = DateTime.Now;
+            mout.Employee.ID = App.EmployeeID;
+            mout.Type = StockMovementType.TransferOut;
+
+            var min = mout.CreateMovement();
+            min.Received = qty.Value;
+            min.Cost = holding.AverageValue;
+            min.Employee.ID = App.EmployeeID;
+            min.Type = StockMovementType.TransferIn;
+            min.Date = mout.Date;
+            min.Transaction = mout.Transaction;
+
+            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 +381,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 +414,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 +481,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 +572,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, mout, min) =>
+            {
+                mout.JobRequisitionItem.ID = requi.ID;
+                mout.Notes = $"Moved to {target.Code} by {App.EmployeeName}";
+
+                min.JobRequisitionItem.ID = requi.ID;
+                min.Notes = $"Moved to {target.Code} by {App.EmployeeName}";
+                min.Location.Clear();
+                min.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 +676,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 +743,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 +873,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