Jelajahi Sumber

Improved StockLocationScreen to allow for JobRequisitionItem management
Added Recalculate button to StockHoldingGrid

frogsoftware 1 tahun lalu
induk
melakukan
06581a412f

+ 31 - 26
prs.classes/Entities/Stock/StockMovement.cs

@@ -87,36 +87,34 @@ namespace Comal.Classes
         [EntityRelationship(DeleteAction.Cascade)]
         [RequiredColumn]
         public override ProductLink Product { get; set; }
-
+        
+                
         [EditorSequence(2)]
-        [EntityRelationship(DeleteAction.SetNull)]
-        public ProductStyleLink Style { get; set; }
+        [RequiredColumn]
+        [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
+        public override StockDimensions Dimensions { get; set; }
 
+        [EditorSequence(3)]
         [EntityRelationship(DeleteAction.SetNull)]
-        public StockLocationLink Location { get; set; }
-
+        public ProductStyleLink Style { get; set; }
+        
         [DoubleEditor(Summary = Summary.Sum)]
-        [EditorSequence(3)]
+        [EditorSequence(4)]
         public double Received { get; set; }
 
         [DoubleEditor(Summary = Summary.Sum)]
-        [EditorSequence(3)]
+        [EditorSequence(5)]
         public double Issued { get; set; }
-
-        [DoubleEditor]
-        [EditorSequence(4)]
+        [DoubleEditor(Editable = Editable.Hidden)]
+        [EditorSequence(6)]
         public double Balance { get; set; }
         
         // Units = Received - Issued
         [Formula(typeof(StockMovementUnitsFormula))]
-        [EditorSequence(5)]
+        [EditorSequence(7)]
         [DoubleEditor(Visible=Visible.Optional, Editable = Editable.Hidden, Summary= Summary.Sum)]
         public double Units { get; set; }
-        
-        [EditorSequence(6)]
-        [RequiredColumn]
-        [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
-        public override StockDimensions Dimensions { get; set; }
+
         
         // IsRemnant = Dimensions.Value < Product.Dimensions.Value
         [CheckBoxEditor(Editable = Editable.Hidden)]
@@ -130,21 +128,31 @@ namespace Comal.Classes
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         public double Qty { get; set; }
         
+        
+        [CurrencyEditor(Visible = Visible.Default)]
         [EditorSequence(9)]
-        [EntityRelationship(DeleteAction.SetNull)]
-        public JobLink Job { get; set; }
-
+        public double Cost { get; set; } = 0.0;
+        
         [EditorSequence(10)]
         [EntityRelationship(DeleteAction.SetNull)]
-        public EmployeeLink Employee { get; set; }
-
-        [MemoEditor]
+        public StockLocationLink Location { get; set; }
+        
         [EditorSequence(11)]
+        [EntityRelationship(DeleteAction.SetNull)]
+        public JobLink Job { get; set; }
+        
+        [MemoEditor]
+        [EditorSequence(12)]
         public string Notes { get; set; }
 
-        [EnumLookupEditor(typeof(StockMovementType))]
+        [EditorSequence(13)]
+        [EnumLookupEditor(typeof(StockMovementType),Editable = Editable.Hidden)]
         public StockMovementType Type { get; set; }
         
+        [EditorSequence(14)]
+        [EntityRelationship(DeleteAction.SetNull)]
+        public EmployeeLink Employee { get; set; }
+        
         [NullEditor]
         public Guid Transaction { get; set; } = Guid.NewGuid();
 
@@ -176,9 +184,6 @@ namespace Comal.Classes
         [Obsolete("Replaced with Dimensions", true)]
         public double UnitSize { get; set; }
         
-        [CurrencyEditor(Visible = Visible.Default)]
-        [EditorSequence(12)]
-        public double Cost { get; set; } = 0.0;
         
         [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary=Summary.Sum)]
         [Formula(typeof(StockMovementValueFormula))]

+ 466 - 168
prs.desktop/Panels/Products/Locations/StockHoldingGrid.cs

@@ -24,7 +24,7 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
     private MovementAction _action;
     private StockHolding? _holding;
 
-    private readonly Guid _employeeid = Guid.Empty;
+    
 
     private Button IssueButton;
 
@@ -35,12 +35,14 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
     //Button ReserveButton = null;
 
     private Button TransferButton;
+    
+    private Button RecalculateButton;
 
     public StockHoldingGrid() : base()
     {         
         ColumnsTag = "StockHolding";
 
-        _employeeid = GetEmployeeID();
+        
     }
     protected override void Init()
     {
@@ -60,6 +62,9 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         TransferButton.Margin = new Thickness(20, TransferButton.Margin.Top, TransferButton.Margin.Right, TransferButton.Margin.Bottom);
         TransferButton.IsEnabled = false;
 
+        RecalculateButton = AddButton("Recalculate", PRSDesktop.Resources.service.AsBitmapImage(), RecalculateHoldings,
+            DynamicGridButtonPosition.Right);
+
         HiddenColumns.Add(x => x.Product.ID);
         HiddenColumns.Add(x => x.Job.ID);
         HiddenColumns.Add(x => x.Job.JobNumber);
@@ -84,11 +89,152 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         HiddenColumns.Add(x => x.Dimensions.Width);
         HiddenColumns.Add(x => x.Dimensions.Height);
         HiddenColumns.Add(x => x.Dimensions.Quantity);
-        HiddenColumns.Add(x => x.Dimensions.Value);
+        HiddenColumns.Add(x => x.Dimensions.Value);        
+        HiddenColumns.Add(x => x.Dimensions.UnitSize);
 
         ActionColumns.Add(new DynamicMenuColumn(BuildMenu) { Position = DynamicActionColumnPosition.End });
     }
 
+    private bool RecalculateHoldings(Button arg1, CoreRow[] arg2)
+    {
+        Dictionary<String, int> messages = new();
+        void AddMessage(String type)
+        {
+            messages.TryGetValue(type, out int count);
+            messages[type] = ++count;
+        }
+        
+        Progress.ShowModal("Recalculating", progress =>
+        {
+            progress.Report("Loading Data");
+            MultiQuery query = new MultiQuery();
+            
+            query.Add(
+                new Filter<StockHolding>(x => x.Location.ID).IsEqualTo(Location.ID),
+                new Columns<StockHolding>(x => x.ID)
+                    .Add(x => x.Product.ID)
+                    .Add(x => x.Job.ID)
+                    .Add(x => x.Style.ID)
+                    .Add(x => x.Dimensions.Unit.ID)
+                    .Add(x => x.Dimensions.Length)
+                    .Add(x => x.Dimensions.Width)
+                    .Add(x => x.Dimensions.Height)
+                    .Add(x => x.Dimensions.Quantity)
+                    .Add(x => x.Dimensions.Value)
+                    .Add(x => x.Dimensions.UnitSize)
+                    .Add(x => x.Units)
+                    .Add(x => x.AverageValue)            
+            );
+            
+            query.Add(
+                new Filter<StockMovement>(x => x.Location.ID).IsEqualTo(Location.ID),
+                new Columns<StockMovement>(x => x.ID)
+                    .Add(x => x.Product.ID)
+                    .Add(x => x.Job.ID)
+                    .Add(x => x.Style.ID)
+                    .Add(x => x.Dimensions.Unit.ID)
+                    .Add(x => x.Dimensions.Length)
+                    .Add(x => x.Dimensions.Width)
+                    .Add(x => x.Dimensions.Height)
+                    .Add(x => x.Dimensions.Quantity)
+                    .Add(x => x.Dimensions.Value)
+                    .Add(x => x.Dimensions.UnitSize)
+                    .Add(x => x.Units)
+                    .Add(x => x.Cost)
+            );
+            query.Query();
+            var holdings = query.Get<StockHolding>().ToObjects<StockHolding>().ToList();
+            var movements = query.Get<StockMovement>().ToObjects<StockMovement>().ToList();
+
+            progress.Report("Processing");
+            var updates = new List<StockHolding>();
+            
+            while (movements.Any())
+            {
+                var first = movements.First();
+                var selected = movements.Where(x =>
+                    x.Product.ID == first.Product.ID
+                    && x.Job.ID == first.Job.ID
+                    && x.Style.ID == first.Style.ID
+                    && x.Dimensions.Unit.ID == first.Dimensions.Unit.ID
+                    && x.Dimensions.Length.EqualsWithTolerance(first.Dimensions.Length)
+                    && x.Dimensions.Width.EqualsWithTolerance(first.Dimensions.Width)
+                    && x.Dimensions.Height.EqualsWithTolerance(first.Dimensions.Height)
+                    && x.Dimensions.Quantity.EqualsWithTolerance(first.Dimensions.Quantity)
+                    && x.Dimensions.Weight.EqualsWithTolerance(first.Dimensions.Weight)
+                    && x.Dimensions.Value.EqualsWithTolerance(first.Dimensions.Value)
+                    && String.Equals(x.Dimensions.UnitSize, first.Dimensions.UnitSize)
+                );
+                var units = selected.Aggregate(0.0d, (t, s) => t += s.Units);
+                var cost = selected.Aggregate(0.0d, (t, s) => t += (s.Units * s.Cost));
+                
+                var holding = holdings.FirstOrDefault(x =>
+                    x.Product.ID == first.Product.ID
+                    && x.Job.ID == first.Job.ID
+                    && x.Style.ID == first.Style.ID
+                    && x.Dimensions.Unit.ID == first.Dimensions.Unit.ID
+                    && x.Dimensions.Length.EqualsWithTolerance(first.Dimensions.Length)
+                    && x.Dimensions.Width.EqualsWithTolerance(first.Dimensions.Width)
+                    && x.Dimensions.Height.EqualsWithTolerance(first.Dimensions.Height)
+                    && x.Dimensions.Quantity.EqualsWithTolerance(first.Dimensions.Quantity)
+                    && x.Dimensions.Weight.EqualsWithTolerance(first.Dimensions.Weight)
+                    && x.Dimensions.Length.EqualsWithTolerance(first.Dimensions.Length)
+                    && String.Equals(x.Dimensions.UnitSize, first.Dimensions.UnitSize)
+                );
+                if (holding == null)
+                {
+                    holding = new StockHolding();
+                    holding.Location.ID = Location.ID;
+                    holding.Product.ID = first.Product.ID;
+                    holding.Style.ID = first.Style.ID;
+                    holding.Job.ID = first.Job.ID;
+                    holding.Dimensions.Unit.ID = first.Dimensions.Unit.ID;
+                    holding.Dimensions.Length = first.Dimensions.Length;
+                    holding.Dimensions.Width = first.Dimensions.Width;
+                    holding.Dimensions.Height = first.Dimensions.Height;
+                    holding.Dimensions.Quantity = first.Dimensions.Quantity;
+                    holding.Dimensions.Weight = first.Dimensions.Weight;
+                    holding.Dimensions.Value = first.Dimensions.Value;
+                    holding.Dimensions.UnitSize = first.Dimensions.UnitSize;
+                }
+                holding.Units = units;
+                holding.AverageValue = units.EqualsWithTolerance(0.0F) ? 0.0d : cost / units;
+
+                if (holdings.Contains(holding))
+                    holdings.Remove(holding);
+
+                if (holding.IsChanged() && !holding.Units.EqualsWithTolerance(0.0f))
+                {
+                    AddMessage(holding.ID != Guid.Empty ? "updated" : "added");
+                    updates.Add(holding);
+                }
+
+                movements.RemoveAll(x => selected.Any(s => s.ID == x.ID));
+            }
+            foreach (var holding in holdings)
+                AddMessage("deleted");
+
+            if (updates.Any())
+            {
+                progress.Report($"Updating {updates.Count} Holdings");
+                new Client<StockHolding>().Save(updates.Where(x => x.IsChanged()), "Updated by Recalculation");
+            }
+
+            if (holdings.Any())
+            {
+                progress.Report($"Deleting {holdings.Count} Holdings");
+                new Client<StockHolding>().Delete(holdings, "Removed by Recalculation");
+            }
+
+        });
+        MessageWindow.ShowMessage(
+            messages.Any() 
+                ? String.Join("\n", messages.Select(x => $"{x.Value} holdings {x.Key}"))
+                : "Nothing to Update!"
+            ,"Recalculate");
+        return true;
+    }
+
     public override DynamicGridColumns GenerateColumns()
     {
         var columns = new DynamicGridColumns<StockHolding>();
@@ -107,14 +253,139 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
     protected override void DoReconfigure(FluentList<DynamicGridOption> options)
     {
         base.DoReconfigure(options);
-        options.AddRange(DynamicGridOption.RecordCount, DynamicGridOption.SelectColumns, DynamicGridOption.FilterRows);
+        options
+            .BeginUpdate()
+            .Remove(DynamicGridOption.AddRows)
+            .Remove(DynamicGridOption.EditRows)
+            .Remove(DynamicGridOption.DeleteRows)
+            .Add(DynamicGridOption.RecordCount)
+            .Add(DynamicGridOption.SelectColumns)
+            .Add(DynamicGridOption.FilterRows)
+            .EndUpdate();
     }
 
     private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
     {
         if (row is null) return;
+        var holding = row.ToObject<StockHolding>();
 
-        column.AddItem("View Requisition Items", null, ViewRequisitions_Click);
+        if (holding.Available.EqualsWithTolerance(holding.Units))
+            column.AddItem("(No Requisitions in this Holding", null, null).IsEnabled = false;
+        else
+            column.AddItem("View Requisition Items", null, ViewRequisitions_Click);
+        
+        column.AddSeparator();
+
+        var requiitems = new Client<JobRequisitionItem>()
+            .Query(
+                new Filter<JobRequisitionItem>(x => x.ID).InQuery(StockHolding.GetFilter(holding), x => x.JobRequisitionItem.ID),
+                new Columns<JobRequisitionItem>(x=>x.ID)
+                    .Add(x=>x.Job.JobNumber)
+                    .Add(x=>x.Requisition.Number)
+                    .Add(x=>x.Requisition.Description)
+                    .Add(x=>x.Qty)
+            ).ToObjects<JobRequisitionItem>()
+            .ToList();
+        if (!holding.Available.EqualsWithTolerance(0.0F))
+            requiitems.Insert(0, new JobRequisitionItem() { Qty = holding.Available });
+        
+        if (requiitems.Count <= 1)
+            column.AddItem("Relocate Items", null, r => RelocateItems(holding, requiitems.ToArray()));
+        else
+        {
+            var header = column.AddItem("Relocate Items", null, null);
+            column.AddItem("All Items", null, r => RelocateItems(holding, requiitems.ToArray()), header);
+            column.AddSeparator(header);
+            
+            if (!holding.Available.EqualsWithTolerance(0.0F))
+                column.AddItem("Un-Requisitioned Items", null, r => RelocateItems(holding, requiitems.Where(x=>x.ID == Guid.Empty).ToArray()), header);
+            
+            foreach (var requiitem in requiitems)
+            {
+                string name =
+                    $"{requiitem.Job.JobNumber}:{requiitem.Requisition.Number} {requiitem.Requisition.Description} ({requiitem.Qty})";
+                column.AddItem(name, null, r => RelocateItems(holding, requiitems.Where(x=>x.ID == requiitem.ID).ToArray()), header);
+            }
+        }
+    }
+
+    private class StockLocationSelection : BaseObject
+    {
+        [EditorSequence(1)]
+        public StockLocationLink Location { get; set; }
+        
+        [EditorSequence(2)]
+        public double Qty { get; set; }
+    }
+
+    private class StockJobSelection : BaseObject
+    {
+        [EditorSequence(1)]
+        public JobLink Job { get; set; }
+        
+        [EditorSequence(2)]
+        public double Qty { get; set; }
+    }
+
+    
+    private void RelocateItems(StockHolding holding, JobRequisitionItem[] requiitems)
+    {
+        var max = requiitems.Aggregate(0.0d, (t, x) => t += x.Qty);
+        var grid = new DynamicItemsListGrid<StockLocationSelection>();
+        grid.OnValidate += (sender, items, errors) =>
+        {
+            var total = items.Aggregate(0.0d, (t, x) => t += x.Qty);
+            if (total > max)
+                errors.Add($"Qty must not exceed {max}");
+        };
+        grid.OnCustomiseEditor += (sender, items, column, editor) =>
+        {
+            if (column.ColumnName.Equals(nameof(StockLocationSelection.Qty)) && requiitems?.Length != 1)
+                editor.Editable = Editable.Disabled;
+        };
+        var selection = new StockLocationSelection() { Qty = max };
+        if (grid.EditItems(new StockLocationSelection[] { selection }))
+        {
+            List<StockMovement> updates = new List<StockMovement>();
+            foreach (var requiitem in requiitems)
+            {
+                var mout = new StockMovement();
+                mout.Location.ID = holding.Location.ID;
+                mout.Product.ID = holding.Product.ID;
+                mout.Style.ID = holding.Style.ID;
+                mout.Dimensions.CopyFrom(holding.Dimensions);
+                mout.Job.ID = holding.Job.ID;
+                mout.Issued = Math.Min(requiitem.Qty, selection.Qty);
+                mout.Cost = holding.AverageValue;
+                mout.JobRequisitionItem.ID = requiitem.ID;
+                mout.Transaction = Guid.NewGuid();
+                mout.Type = StockMovementType.TransferOut;
+                mout.Date = DateTime.Now;
+                mout.IsTransfer = true;
+                mout.Employee.ID = App.EmployeeID;
+                mout.Notes = $"Moved to {selection.Location.Code} by {App.EmployeeName}";
+                updates.Add(mout);
+                
+                var min = new StockMovement();
+                min.Location.ID = selection.Location.ID;
+                min.Product.ID = holding.Product.ID;
+                min.Style.ID = holding.Style.ID;
+                min.Dimensions.CopyFrom(holding.Dimensions);
+                min.Job.ID = holding.Job.ID;
+                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.IsTransfer = true;
+                min.Employee.ID = App.EmployeeID;
+                min.Notes = $"Moved From {holding.Location.Code} by {App.EmployeeName}";
+                updates.Add(min);
+            }
+            new Client<StockMovement>().Save(updates, "Relocated from Stock Locations Screen");
+        }
+    
     }
 
     private void ViewRequisitions_Click(CoreRow? row)
@@ -165,16 +436,7 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         }
         return smg;
     }
-
-    private Guid GetEmployeeID()
-    {
-        var employee = new Client<Employee>().Query(
-            new Filter<Employee>(x => x.UserLink.ID).IsEqualTo(ClientFactory.UserGuid),
-            new Columns<Employee>(x => x.ID)
-        );
-        return employee.Rows.Any() ? employee.Rows.First().Get<Employee, Guid>(x => x.ID) : Guid.Empty;
-    }
-
+    
     private Dictionary<string, object?> StockMovementValueChanged(IDynamicEditorForm form, string name, object value)
     {
         var result = new Dictionary<string, object?>();
@@ -192,8 +454,7 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
 
         return result;
     }
-
-
+    
     private void StockMovementValidate(object sender, StockMovement[] items, List<string> errors)
     {
         if (items.Any(x => x.Received == 0 && x.Issued == 0))
@@ -241,39 +502,33 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
     {
         if (column.ColumnName.Equals("Location.ID"))
             editor.Editable = _action == MovementAction.Transfer ? Editable.Enabled : Editable.Hidden;
-
+        
+        if (column.ColumnName.Equals("Product.ID"))
+            editor.Editable = _action == MovementAction.Receive ? Editable.Enabled : Editable.Disabled;
+        
         if (column.ColumnName.Equals("Product.NettCost"))
             editor.Editable = Editable.Hidden;
-
+        
         if (column.ColumnName.Equals("Style.ID"))
             editor.Editable = _action == MovementAction.Receive || _action == MovementAction.Transfer ? Editable.Enabled : Editable.Hidden;
 
-        if (column.ColumnName.Equals("Received"))
-        {
-            editor.Editable = _action == MovementAction.Receive || _action == MovementAction.Transfer ? Editable.Enabled : Editable.Hidden;
-            editor.Caption = "Quantity";
-        }
-
-        if (column.ColumnName.Equals("Product.ID"))
-            editor.Editable = _action == MovementAction.Receive ? Editable.Enabled : Editable.Hidden;
-
         if (column.ColumnName.Equals("UnitSize"))
             editor.Editable = _action == MovementAction.Receive ? Editable.Enabled : Editable.Hidden;
-
+        
         if (column.ColumnName.Equals("Job.ID"))
             editor.Editable = _action == MovementAction.Receive && items?.FirstOrDefault()?.Job.IsValid() == true ? Editable.Disabled : Editable.Enabled;
-
-        if (column.ColumnName.Equals("Issued"))
+        
+        if (column.ColumnName.Equals(nameof(StockMovement.Received)))
         {
-            editor.Editable = _action == MovementAction.Issue ? Editable.Enabled : Editable.Hidden;
+            editor.Editable = _action == MovementAction.Receive  ? Editable.Enabled : Editable.Hidden;
             editor.Caption = "Quantity";
         }
-
-        if (column.ColumnName.Equals("Employee.ID"))
-            editor.Editable = _action == MovementAction.Transfer ? Editable.Hidden : Editable.Enabled;
-
-        if (column.ColumnName.Equals("Units"))
-            editor.Editable = _action == MovementAction.Receive ? Editable.Enabled : Editable.Hidden;
+        if (column.ColumnName.Equals(nameof(StockMovement.Issued)))
+        {
+            editor.Editable = _action == MovementAction.Issue || _action == MovementAction.Transfer ? Editable.Enabled : Editable.Hidden;
+            editor.Caption = "Quantity";
+        }
+        
     }
 
     private bool ReceiveStock(Button arg1, CoreRow[] rows)
@@ -287,7 +542,7 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         movement.Location.Description = Location.Description;
         movement.Date = DateTime.Now;
         movement.IsTransfer = false;
-        movement.Employee.ID = _employeeid;
+        movement.Employee.ID = App.EmployeeID;
         movement.Type = StockMovementType.Receive;
 
         movement.CommitChanges();
@@ -310,7 +565,7 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
         var batch = new StockMovementBatch();
         batch.Type = type;
         batch.Notes = batch.Type + " batch created from Desktop Stock Location Screen";
-        batch.Employee.ID = _employeeid;
+        batch.Employee.ID = App.EmployeeID;
         new Client<StockMovementBatch>().Save(batch, "created from Desktop Stock Location Screen");
 
         foreach (var mvt in movements)
@@ -323,140 +578,183 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
 
     private bool IssueStock(Button arg1, CoreRow[] rows)
     {
-        if (!rows.Any())
+        if (rows?.Length != 1)
         {
             MessageWindow.ShowMessage("Please select an item to issue", "No selected items");
             return false;
         }
-
+        
         var holding = rows.First().ToObject<StockHolding>();
+        SelectAllocation(holding, (h, items) => DoIssue(h, items));
+        return false;
+        
+    }
 
-        if(holding.Available <= 0)
-        {
-            MessageWindow.ShowMessage("There is no available stock in this holding to issue.", "No available stock");
-            return false;
-        }
+    private void DoIssue(StockHolding holding, JobRequisitionItem[] requiitems)
+    {
 
-        // var jobid = rows.First().Get<StockHolding, Guid>(x => x.Job.ID);
-        // var jobno = rows.First().Get<StockHolding, string>(x => x.Job.JobNumber);
-        // var productid = rows.First().Get<StockHolding, Guid>(x => x.Product.ID);
-        // var styleid = rows.First().Get<StockHolding, Guid>(x => x.Style.ID);
-        // var stylecode = rows.First().Get<StockHolding, string>(x => x.Style.Code);
-        // var size = rows.First().Get<StockHolding, double>(x => x.UnitSize);
+        var updates = new List<StockMovement>();
+        // At this point we either have
+        // 1. All Items (Multiple requiitems) -> no editing available
+        // 2. UnRequid Items (single requiitem, requiid = empty) - Full Editor
+        // 3. Requi'd Item (single requiitem, requiid != empty - Quantity only
 
-        var movement = new StockMovement
+        if (requiitems.Length > 1)
         {
-            Date = DateTime.Now
+            if (MessageWindow.ShowOKCancel(
+                    "This will issue everything from this holding!\nAre you sure you wish to continue?",
+                    "Confirm Issue", null) == true)
+            {
+                foreach (var requiitem in requiitems)
+                {
+                    var movement = CreateMovementFromHolding(holding);
+                    movement.Type = StockMovementType.Issue;
+                    movement.JobRequisitionItem.ID = requiitem.ID;
+                    movement.Issued = requiitem.Qty;
+                    movement.Transaction = Guid.NewGuid();
+                    movement.Notes = $"Issued by {App.EmployeeName}";
+                    updates.Add(movement);
+                }
+                SaveBatch(StockMovementBatchType.Issue, updates.ToArray());
+                DoChanged();
+                Refresh(false,true);
+            }
+            return;
+        }
+        
+        var sjs = new StockJobSelection();
+        sjs.Job.ID = holding.Job.ID;
+        sjs.Qty = requiitems[0].Qty;
+        
+        var sjg = new DynamicItemsListGrid<StockJobSelection>();
+        sjg.OnValidate += (sender, items, errors) =>
+        {
+            if (items[0].Qty > requiitems[0].Qty)
+                    errors.Add($"Qty must not exceed {requiitems[0].Qty}");
         };
-        movement.Location.ID = Location.ID;
-        movement.Location.Code = Location.Code;
-        movement.Location.Description = Location.Description;
-        movement.Product.ID = holding.Product.ID;
-        movement.Job.ID = holding.Job.ID;
-        movement.Job.JobNumber = holding.Job.JobNumber;
-        movement.Style.ID = holding.Style.ID;
-        movement.Style.Code = holding.Style.Code;
-        movement.Dimensions.CopyFrom(holding.Dimensions);
-        movement.IsTransfer = false;
-        movement.Employee.ID = _employeeid;
-        movement.Cost = holding.AverageValue;
-        movement.Type = StockMovementType.Issue;
-
-        movement.CommitChanges();
-
-        var smg = CheckStockMovementGrid(MovementAction.Issue, holding);
-        var result = smg.EditItems(new[] { movement });
-
-        var mvts = new List<StockMovement>
+        sjg.OnCustomiseEditor += (sender, items, column, editor) =>
         {
-            movement
+            if (column.ColumnName.Equals(CoreUtils.GetFullPropertyName<StockJobSelection, Guid>(x => x.Job.ID, ".")))
+                editor.Editable = requiitems[0].ID == Guid.Empty
+                    ? Editable.Enabled
+                    : Editable.Disabled;
         };
-
-        // Issuing to a different job than you received into?
-        if (result && holding.Job.ID != movement.Job.ID)
+        if (sjg.EditItems(new StockJobSelection[] { sjs }))
         {
-            // Issue from Old Job (Hidden)
-            var issue = new StockMovement
+            var issue = CreateMovementFromHolding(holding);
+            issue.Job.ID = sjs.Job.ID;
+            issue.Type = StockMovementType.Issue;
+            issue.JobRequisitionItem.ID = requiitems[0].ID;
+            issue.Issued = sjs.Qty;
+            issue.Transaction = Guid.NewGuid();
+            issue.Notes = $"Issued by {App.EmployeeName}";
+            updates.Add(issue);
+            if (holding.Job.ID != issue.Job.ID)
             {
-                Date = movement.Date
-            };
-            issue.Product.ID = holding.Product.ID;
-            issue.Job.ID = holding.Job.ID;
-            issue.Location.ID = Location.ID;
-            issue.Style.ID = holding.Style.ID;
-            issue.Issued = movement.Issued;
-            issue.Transaction = movement.Transaction;
-            issue.Employee.ID = movement.Employee.ID;
-            issue.Dimensions.CopyFrom(holding.Dimensions);
-            issue.IsTransfer = true;
-            issue.Notes = string.Format("Transferred from {0}", holding.Job.ID.Equals(Guid.Empty) ? "General Stock" : "Job " + holding.Job.JobNumber);
-            issue.System = true;
-            issue.Cost = holding.AverageValue;
-            issue.Type = StockMovementType.TransferOut;
-
-            // Receive to New Job (Hidden)
-            var receive = new StockMovement();
-            receive.Date = movement.Date;
-            receive.Product.ID = holding.Product.ID;
-            receive.Job.ID = movement.Job.ID;
-            receive.Location.ID = Location.ID;
-            receive.Style.ID = holding.Style.ID;
-            receive.Received = movement.Issued;
-            receive.Transaction = movement.Transaction;
-            receive.Employee.ID = movement.Employee.ID;
-            receive.Dimensions.CopyFrom(holding.Dimensions);
-            receive.IsTransfer = true;
-            receive.Notes = string.Format("Transferred to {0}",
-                !movement.Job.IsValid() ? "General Stock" : "Job " + movement.Job.JobNumber);
-            receive.System = true;
-            receive.Cost = holding.AverageValue;
-            receive.Type = StockMovementType.TransferIn;
-
-            new Client<StockMovement>().Save(new[] { issue, receive }, "");
-            mvts.Add(issue);
-            mvts.Add(receive);
+                var xferout = CreateMovementFromHolding(holding);
+                xferout.Type = StockMovementType.TransferOut;
+                xferout.JobRequisitionItem.ID = requiitems[0].ID;
+                xferout.Issued = sjs.Qty;
+                xferout.Transaction = issue.Transaction;
+                xferout.IsTransfer = true;
+                xferout.Notes = $"Issued by {App.EmployeeName}";
+                updates.Add(xferout);
+                
+                var xferin = CreateMovementFromHolding(holding);
+                xferin.Job.ID = sjs.Job.ID;
+                xferin.Type = StockMovementType.TransferOut;
+                xferin.JobRequisitionItem.ID = requiitems[0].ID;
+                xferin.Received = sjs.Qty;
+                xferin.Transaction = issue.Transaction;
+                xferin.IsTransfer = true;
+                xferin.Notes = $"Issued by {App.EmployeeName}";
+                updates.Add(xferin);
+                
+            }
+            SaveBatch(StockMovementBatchType.Issue, updates.ToArray());
+            DoChanged();
+            Refresh(false,true);
         }
+    }
 
-        if (result)
+    private void SelectAllocation(StockHolding holding, Action<StockHolding,JobRequisitionItem[]> action)
+    {
+        
+        var requiitems = new Client<JobRequisitionItem>()
+            .Query(
+                new Filter<JobRequisitionItem>(x => x.ID).InQuery(StockHolding.GetFilter(holding), x => x.JobRequisitionItem.ID),
+                new Columns<JobRequisitionItem>(x=>x.ID)
+                    .Add(x=>x.Job.JobNumber)
+                    .Add(x=>x.Requisition.Number)
+                    .Add(x=>x.Requisition.Description)
+                    .Add(x=>x.Qty)
+            ).ToObjects<JobRequisitionItem>()
+            .ToList();
+        if (!holding.Available.EqualsWithTolerance(0.0F))
+            requiitems.Insert(0, new JobRequisitionItem() { Qty = holding.Available });
+        
+        if (requiitems.Count <= 1)
+            action(holding, requiitems.ToArray());
+        else
         {
-            DoChanged();
-            SaveBatch(StockMovementBatchType.Issue, mvts.ToArray());
+            
+            ContextMenu menu = new ContextMenu();
+            
+            MenuItem all = new MenuItem();
+            all.Header =
+                $"All Items ({holding.Units})";
+            all.Click += (sender, args) => action(holding,requiitems.ToArray());
+            menu.Items.Add(all);
+            menu.Items.Add(new Separator());
+
+            if (!holding.Available.EqualsWithTolerance(0.0F))
+            {
+                MenuItem item = new MenuItem();
+                item.Header =
+                    $"Un-Requisitioned Items ({holding.Available})";
+                item.Click += (sender, args) =>
+                    action(holding, requiitems.Where(x => x.ID == Guid.Empty).ToArray());
+                menu.Items.Add(item);
+            }
+
+            foreach (var requiitem in requiitems)
+            {
+                MenuItem item = new MenuItem();
+                item.Header =
+                    $"{requiitem.Job.JobNumber}:{requiitem.Requisition.Number} {requiitem.Requisition.Description} ({requiitem.Qty})";
+                item.Click += (sender, args) => action(holding, requiitems.Where(x=>x.ID == requiitem.ID).ToArray());
+                menu.Items.Add(item);
+
+            }
+            menu.IsOpen = true;
         }
-        return result;
+        
     }
-
+    
     private bool TransferStock(Button arg1, CoreRow[] rows)
     {
+        if (rows?.Length != 1)
+            return false;
+        
         var holding = rows.First().ToObject<StockHolding>();
+        SelectAllocation(holding, (h,items) => DoTransfer(h, items) );
+        return false;
+    }
 
-        if(holding.Available <= 0)
+    private void DoTransfer(StockHolding holding, JobRequisitionItem[] requiitems)
+    {
+        if (requiitems.Length > 1 || requiitems[0].Requisition.ID != Guid.Empty)
         {
-            MessageWindow.ShowMessage("There is no available stock in this holding to transfer.", "No available stock");
-            return false;
+            RelocateItems(holding, requiitems);
+            return;
         }
-
-        var movement = new StockMovement();
-        movement.Date = DateTime.Now;
-
-        movement.Location.ID = Location.ID;
-        movement.Location.Code = Location.Code;
-        movement.Location.Description = Location.Description;
-
-        movement.Product.ID = holding.Product.ID;
-        movement.Job.ID = holding.Job.ID;
-        movement.Job.JobNumber = holding.Job.JobNumber;
-        movement.Style.ID = holding.Style.ID;
-        movement.Style.Code = holding.Style.Code;
-        movement.Employee.ID = _employeeid;
-        // Must happen before Received gets set so that OnPropertyChanged has stuff to work with and thus Qty is not zero.
-        movement.Dimensions.CopyFrom(holding.Dimensions);
+        
+        var movement = CreateMovementFromHolding(holding);
+        movement.JobRequisitionItem.ID = requiitems[0].ID;
         movement.Received = holding.Units;
         movement.IsTransfer = true;
-        movement.Cost = holding.AverageValue;
         movement.Type = StockMovementType.TransferIn;
 
-        movement.CommitChanges();
-
         var smg = CheckStockMovementGrid(MovementAction.Transfer, holding);
         var result = smg.EditItems(new[] { movement });
 
@@ -467,23 +765,12 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
 
         if (result)
         {
-            var other = new StockMovement();
-            other.Date = movement.Date;
-            other.Product.ID = holding.Product.ID;
-            other.Job.ID = holding.Job.ID;
-            other.Cost = holding.AverageValue;
-
-            other.Location.ID = Location.ID;
-            //other.Location.Code = Location.Code;
-            //other.Location.Description = Location.Description;
-
-            other.Style.ID = holding.Style.ID;
+            var other = CreateMovementFromHolding(holding);
             other.Issued = movement.Received;
             other.Transaction = movement.Transaction;
-            other.Employee.ID = movement.Employee.ID;
-            other.Dimensions.CopyFrom(holding.Dimensions);
             other.IsTransfer = true;
             other.Type = StockMovementType.TransferOut;
+            movement.JobRequisitionItem.ID = requiitems[0].ID;
 
             var changes = new List<string>();
             if (movement.Location.ID != other.Location.ID)
@@ -511,7 +798,26 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
             DoChanged();
             SaveBatch(StockMovementBatchType.Transfer, mvts.ToArray());
         }
-        return result;
+        Refresh(true,false);
+    }
+
+    private StockMovement CreateMovementFromHolding(StockHolding holding)
+    {
+        var movement = new StockMovement();
+        movement.Date = DateTime.Now;
+        movement.Location.ID = Location.ID;
+        movement.Location.Code = Location.Code;
+        movement.Location.Description = Location.Description;
+        movement.Product.ID = holding.Product.ID;
+        movement.Job.ID = holding.Job.ID;
+        movement.Job.JobNumber = holding.Job.JobNumber;
+        movement.Style.ID = holding.Style.ID;
+        movement.Style.Code = holding.Style.Code;
+        movement.Employee.ID = App.EmployeeID;
+        movement.Dimensions.CopyFrom(holding.Dimensions);
+        movement.Cost = holding.AverageValue;
+        movement.CommitChanges();
+        return movement;
     }
 
     protected override void Reload(Filters<StockHolding> criteria, Columns<StockHolding> columns, ref SortOrder<StockHolding>? sort, Action<CoreTable?, Exception?> action)
@@ -523,13 +829,5 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
             criteria.Add(new Filter<StockHolding>(x => x.Location.ID).IsEqualTo(Location.ID));
         base.Reload(criteria, columns, ref sort, action);
     }
-
-    protected override bool FilterRecord(CoreRow row)
-    {
-        // Hackety Hackety Hack Hack stupid doubles not totalling zero when they're supposed to
-        var result = base.FilterRecord(row);
-        if (result)
-            result = Math.Abs(row.Get<StockHolding, double>(x => x.Qty)) >= 0.000001;
-        return result;
-    }
+    
 }

+ 20 - 5
prs.desktop/Panels/Products/Locations/StockLocationPanel.xaml

@@ -8,11 +8,11 @@
              mc:Ignorable="d"
              d:DesignHeight="450" d:DesignWidth="800">
     <dynamic:DynamicSplitPanel MasterCaption="Stock Location List" DetailCaption="Location Summary" AnchorWidth="500" AllowableViews="Combined"
-                               View="Combined">
+                               View="Combined" DetailHeight="400">
         <dynamic:DynamicSplitPanel.Header>
             <Border CornerRadius="5,5,0,0" BorderBrush="Gray" BorderThickness="0.75" DockPanel.Dock="Top"
                     Background="WhiteSmoke">
-                <Label Content="Stock Location List" HorizontalContentAlignment="Center" />
+                    <Label Content="Stock Location List" HorizontalContentAlignment="Center" DockPanel.Dock="Left" />
             </Border>
         </dynamic:DynamicSplitPanel.Header>
         <dynamic:DynamicSplitPanel.Master>
@@ -26,17 +26,32 @@
             </Border>
         </dynamic:DynamicSplitPanel.DetailHeader>
         <dynamic:DynamicSplitPanel.Detail>
-            <local:StockHoldingGrid x:Name="Holdings" DockPanel.Dock="Top" Margin="0,2,0,0" />
+            <local:StockHoldingGrid x:Name="Holdings" DockPanel.Dock="Top" Margin="0,2,0,0" OnSelectItem="Holdings_OnOnSelectItem" />
         </dynamic:DynamicSplitPanel.Detail>
 
         <dynamic:DynamicSplitPanel.SecondaryDetail>
             <DockPanel>
                 <Border CornerRadius="0,0,0,0" BorderBrush="Gray" BorderThickness="0.75" DockPanel.Dock="Top"
                     Background="WhiteSmoke">
-                    <Label Content="Stock Movements" HorizontalContentAlignment="Center" />
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="150"/>
+                            <ColumnDefinition Width="*"/>
+                            <ColumnDefinition Width="150"/>
+                        </Grid.ColumnDefinitions>
+                        <DockPanel Grid.Column="0" Margin="5,0,0,0">
+                            <CheckBox x:Name="_syncMovements" DockPanel.Dock="Left" VerticalContentAlignment="Center" Checked="_syncMovements_OnChecked" Unchecked="_syncMovements_OnChecked" />
+                            <Label Content="Sync Movements?" DockPanel.Dock="Left" VerticalContentAlignment="Center" />
+                        </DockPanel>
+                        <Label
+                            Grid.Column="1"
+                            Content="Stock Movements" 
+                            HorizontalContentAlignment="Center"
+                            VerticalContentAlignment="Center"/>
+                    </Grid>
                 </Border>
                 <local:StockMovementGrid x:Name="Movements" Margin="0,2,0,0" DockPanel.Dock="Top" AllowNullLocation="False"
-                                     AllowNullBatch="True" />
+                                     AllowNullBatch="True" OnFilterRecord="Movements_OnOnFilterRecord" />
 
             </DockPanel>
         </dynamic:DynamicSplitPanel.SecondaryDetail>

+ 54 - 0
prs.desktop/Panels/Products/Locations/StockLocationPanel.xaml.cs

@@ -2,12 +2,15 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
+using System.Linq.Expressions;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Threading;
 using Comal.Classes;
 using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
+using Syncfusion.Data.Extensions;
 
 namespace PRSDesktop
 {
@@ -356,5 +359,56 @@ namespace PRSDesktop
             };
             timer.Start();
         }
+        
+        private void _syncMovements_OnChecked(object sender, RoutedEventArgs e)
+        {
+            Movements.Refresh(false,false);
+        }
+
+        private void Holdings_OnOnSelectItem(object sender, DynamicGridSelectionEventArgs e)
+        {
+            if (_syncMovements.IsChecked == true)
+                Movements.Refresh(false, false);
+        }
+
+        private int MOVEMENT_PRODUCTID = -1;
+        private int MOVEMENT_STYLEID = -1;
+        private int MOVEMENT_JOBID = -1;
+        private int MOVEMENT_LOCATIONID = -1;
+        private int MOVEMENT_UNITID = -1;
+        private int MOVEMENT_UNITSIZE = -1;
+        
+        private int GetColumnNumber<TEntity, TType>(Expression<Func<TEntity, TType>> expression)
+        {
+            var columnnames = Movements.Data.Columns.Select(x => x.ColumnName).ToArray();
+            var name = CoreUtils.GetFullPropertyName(expression, ".");
+            return columnnames.IndexOf(name);
+        }
+        
+        private bool Movements_OnOnFilterRecord(CoreRow row)
+        {
+            if (_syncMovements.IsChecked != true)
+                return true;
+            if (Holdings.SelectedRows.Length != 1)
+                return false;
+            if (MOVEMENT_PRODUCTID == -1)
+            {
+                MOVEMENT_PRODUCTID = GetColumnNumber<StockMovement, Guid>(x => x.Product.ID);
+                MOVEMENT_STYLEID = GetColumnNumber<StockMovement, Guid>(x => x.Style.ID);
+                MOVEMENT_JOBID = GetColumnNumber<StockMovement, Guid>(x => x.Job.ID);
+                MOVEMENT_LOCATIONID = GetColumnNumber<StockMovement, Guid>(x => x.Location.ID);
+                MOVEMENT_UNITID = GetColumnNumber<StockMovement, Guid>(x => x.Dimensions.Unit.ID);
+                MOVEMENT_UNITSIZE = GetColumnNumber<StockMovement, string>(x => x.Dimensions.UnitSize);
+            }
+
+            var holding = Holdings.SelectedRows[0].ToObject<StockHolding>();
+            return Guid.Equals(row.Values[MOVEMENT_PRODUCTID],holding.Product.ID)
+                && Guid.Equals(row.Values[MOVEMENT_STYLEID],holding.Style.ID)
+                && Guid.Equals(row.Values[MOVEMENT_JOBID],holding.Job.ID)
+                && Guid.Equals(row.Values[MOVEMENT_UNITID],holding.Dimensions.Unit.ID)
+                && string.Equals(row.Values[MOVEMENT_UNITSIZE],holding.Dimensions.UnitSize);
+        }
+
+
     }
 }

+ 6 - 0
prs.desktop/Panels/Products/Locations/StockMovementGrid.cs

@@ -54,6 +54,12 @@ public class StockMovementGrid : DynamicDataGrid<StockMovement>, IDataModelSourc
         base.Init();
         HiddenColumns.Add(x => x.System);
         HiddenColumns.Add(x => x.Transaction);
+        HiddenColumns.Add(x=>x.Product.ID);
+        HiddenColumns.Add(x=>x.Location.ID);
+        HiddenColumns.Add(x=>x.Style.ID);
+        HiddenColumns.Add(x=>x.Job.ID);
+        HiddenColumns.Add(x=>x.Dimensions.Unit.ID);
+        HiddenColumns.Add(x=>x.Dimensions.UnitSize);
 
         ActionColumns.Add(new DynamicImageColumn(DocumentsImage, DocumentsClick) { Position = DynamicActionColumnPosition.Start });
         HiddenColumns.Add(x => x.Documents);

+ 8 - 6
prs.stores/JobRequisitionItemStore.cs

@@ -56,12 +56,14 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
             item.Status = JobRequisitionItemStatus.Cancelled;
         else
         {
-            var stockMovements = store.Provider.Query(
-                new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID),
-                new Columns<StockMovement>(
-                    x => x.Units,
-                    x => x.Style.ID))
-                .ToObjects<StockMovement>();
+            var stockMovements = store.Provider
+                .Query(
+                    new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID).And(x=>x.Type).IsNotEqualTo(StockMovementType.Issue),
+                    new Columns<StockMovement>(x => x.Units)
+                        .Add(x => x.Style.ID)
+                        .Add(x=>x.Type)
+                    )
+                    .ToObjects<StockMovement>();
             var styleTotal = 0.0;
             var total = 0.0;
             foreach (var mvt in stockMovements)

+ 1 - 1
prs.stores/StockHoldingStore.cs

@@ -76,7 +76,7 @@ public class StockHoldingStore : BaseStore<StockHolding>
         holding.AverageValue = holding.Units != 0 ? holding.Value / holding.Units : 0.0F;
 
         // Automagically clean up empty holdings
-        if (Math.Abs(holding.Units) < 0.000001F)
+        if (holding.Units.EqualsWithTolerance(0.0F))
         {
             if (holding.ID != Guid.Empty)
                 DbFactory.Provider.Delete(holding, "");