Ver código fonte

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

frankvandenbos 1 ano atrás
pai
commit
35e6020523

+ 0 - 1
prs.desktop/Panels/Invoices/ProgressClaim/ProgressClaimGrid.cs

@@ -415,7 +415,6 @@ public class ProgressClaimGrid : DynamicItemsListGrid<ProgressClaim>
                     item.PercentCost = value;
                     item.Cost = item.JobScope.ExTax * item.PercentCost / 100 - item.PreviouslyClaimed;
                     UpdateRow(row, item);
-                    InvalidateRow(row);
                     DoChanged();
                 }
             };

+ 12 - 0
prs.desktop/Panels/PurchaseOrders/SupplierPurchaseOrderItemOneToMany.cs

@@ -166,6 +166,18 @@ public class SupplierPurchaseOrderItemOneToMany : DynamicOneToManyGrid<PurchaseO
         Allocations.RemoveAll(x => !Items.Contains(x.Item1));
     }
 
+    protected override void DoValidate(PurchaseOrderItem[] items, List<string> errors)
+    {
+        base.DoValidate(items, errors);
+        foreach(var item in items)
+        {
+            if(Allocations.Any(x => x.Item1 == item && x.Item2.JobRequisitionItem.ID == Guid.Empty && x.Item2.Job.ID == item.Job.ID))
+            {
+                errors.Add("At least one allocation on this purchase order item is invalid due to having the same [Job] as the purchase order item.");
+            }
+        }
+    }
+
     public override void AfterSave(object item)
     {
         base.AfterSave(item);

+ 7 - 9
prs.desktop/Panels/Reservation Management/ReservationManagementItemGrid.cs

@@ -689,7 +689,6 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
             if(jri is not null)
             {
                 UpdateRow(row, jri);
-                InvalidateRow(row);
             }
         };
         window.ShowDialog();
@@ -932,14 +931,13 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
         grid.ColumnsLoaded += (sender, args) =>
         {
             args.ColumnGroupings.Clear();
-            args.ActionColumns.Clear();
-            args.VisibleColumns.Clear();
-            args.VisibleColumns.Add<StockHolding, string>(x => x.Location.Code, 150, "Location", "", Alignment.MiddleLeft);
-            args.VisibleColumns.Add<StockHolding, string>(x => x.Style.Code, 150, "Style Code", "", Alignment.MiddleLeft);
-            args.VisibleColumns.Add<StockHolding, string>(x => x.Style.Description, 0, "Style", "", Alignment.MiddleLeft);
-            args.VisibleColumns.Add<StockHolding, string>(x => x.Dimensions.UnitSize, 150, "Unit Size", "", Alignment.MiddleLeft);
-            args.VisibleColumns.Add<StockHolding, double>(x => x.Units);
-            args.VisibleColumns.Add<StockHolding, double>(x => x.AverageValue, caption: "Cost $");
+            args.Columns.Clear();
+            args.Add(x => x.Location.Code, 150, "Location", "", Alignment.MiddleLeft);
+            args.Add(x => x.Style.Code, 150, "Style Code", "", Alignment.MiddleLeft);
+            args.Add(x => x.Style.Description, 0, "Style", "", Alignment.MiddleLeft);
+            args.Add(x => x.Dimensions.UnitSize, 150, "Unit Size", "", Alignment.MiddleLeft);
+            args.Add(x => x.Units);
+            args.Add(x => x.AverageValue, caption: "Cost $");
         };
 
         var window = DynamicGridUtils.CreateGridWindow("Stock Holdings", grid);

+ 48 - 104
prs.desktop/Panels/Reservation Management/ReservationManagementPanel.xaml.cs

@@ -303,9 +303,6 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
         var window = new ReservationManagementTreatmentOrderScreen(items);
         if(window.ShowDialog() == true)
         {
-            var orders = new List<Tuple<PurchaseOrder, List<(PurchaseOrderItem, JobRequisitionItemLink, StockForecastTreatmentOrderingResult)>>>();
-            var movements = new List<StockMovement>();
-
             var results = window.Results.GroupBy(x => x.Supplier.ID).ToDictionary(x => x.Key, x => x.ToArray());
             var suppliers = Client.Query(
                 new Filter<Supplier>(x => x.ID).InList(results.Keys.ToArray()),
@@ -313,15 +310,52 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 .ToObjects<Supplier>()
                 .ToDictionary(x => x.ID);
 
+            var doIssue = false;
+            if(Security.IsAllowed<CanIssueTreatmentPurchaseOrders>())
+            {
+                if (_globalSettings.AutoIssueTreatmentPOs)
+                {
+                    doIssue = true;
+                }
+                else if(MessageWindow.ShowYesNo($"Do you wish to mark the purchase order as issued?", "Mark as issued?"))
+                {
+                    doIssue = true;
+                }
+            }
+
+            var grid = new SupplierPurchaseOrders();
+            var windows = new List<ReservationManagementPanelTreatmentPOWindow>();
+
+            grid.OnAfterSave += (editor, items) =>
+            {
+                var order = items.FirstOrDefault();
+                var window = windows.FirstOrDefault(x => x.Order == order);
+                window?.AfterSave();
+            };
+
             foreach(var (supplierID, perSupplier) in results)
             {
-                var order = new PurchaseOrder();
-                order.RaisedBy.ID = App.EmployeeID;
+                var order = perSupplier.First().PurchaseOrder;
+                if(order is null)
+                {
+                    order = new PurchaseOrder();
+                    order.RaisedBy.ID = App.EmployeeID;
+
+                    order.DueDate = DateTime.Today.AddDays(7);
+                    order.Notes = [$"Treatment purchase order raised by {App.EmployeeName} from Reservation Management screen"];
 
-                order.DueDate = DateTime.Today.AddDays(7);
-                order.Notes = [$"Treatment purchase order raised by {App.EmployeeName} from Reservation Management screen"];
+                    LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, supplierID);
+                }
+                else
+                {
+                    Client.EnsureColumns(order, DynamicGridUtils.LoadEditorColumns<PurchaseOrder>());
+                }
 
-                LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, supplierID);
+                if (doIssue)
+                {
+                    order.IssuedBy.ID = App.EmployeeID;
+                    order.IssuedDate = DateTime.Now;
+                }
 
                 var orderItems = new List<(PurchaseOrderItem, JobRequisitionItemLink, StockForecastTreatmentOrderingResult)>();
                 foreach(var item in perSupplier)
@@ -352,92 +386,12 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                     orderItem.Description = $"Treatment for {item.Item.JRI.Product.Name} ({item.Item.Product.Code}/{item.Item.Product.Name})";
                 }
 
-                if(suppliers.TryGetValue(supplierID, out var supplier))
-                {
-                    foreach(var item in perSupplier)
-                    {
-                        var tOut = new StockMovement();
-                        tOut.Job.CopyFrom(item.Item.Job);
-                        tOut.Style.CopyFrom(item.Item.Style);
-                        tOut.Location.CopyFrom(item.Item.Location);
-                        tOut.Product.CopyFrom(item.Item.Product);
-                        tOut.Dimensions.CopyFrom(item.Item.Dimensions);
-
-                        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.CopyFrom(supplier.DefaultLocation);
-
-                        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";
-
-                        movements.Add(tOut);
-                        movements.Add(tIn);
-                    }
-                }
-                else
-                {
-                    MessageWindow.ShowMessage(
-                        $"No default location set up for supplier '{perSupplier[0].Supplier.Code}'; skipping creating stock movements",
-                        "No default location");
-                }
-
-                orders.Add(new(order, orderItems));
-            }
-
-            var doIssue = false;
-            if(Security.IsAllowed<CanIssueTreatmentPurchaseOrders>())
-            {
-                if (_globalSettings.AutoIssueTreatmentPOs)
-                {
-                    doIssue = true;
-                }
-                else if(MessageWindow.ShowYesNo($"Do you wish to mark the purchase order{(orders.Count != 1 ? "s" : "")} as issued?", "Mark as issued?"))
-                {
-                    doIssue = true;
-                }
-            }
-            if (doIssue)
-            {
-                foreach(var (order, _) in orders)
-                {
-                    order.IssuedBy.ID = App.EmployeeID;
-                    order.IssuedDate = DateTime.Now;
-                }
-            }
-
-            var grid = new SupplierPurchaseOrders();
-            var windows = new List<ReservationManagementPanelTreatmentPOWindow>();
-            foreach(var order in orders)
-            {
-                var perSupplier = results.GetValueOrDefault(order.Item1.SupplierLink.ID) ?? [];
-
-                var editorWindow = new ReservationManagementPanelTreatmentPOWindow(grid, order, perSupplier, PanelLink);
+                var editorWindow = new ReservationManagementPanelTreatmentPOWindow(grid, new(order, orderItems), perSupplier, PanelLink);
                 editorWindow.Form.Show();
 
                 ISubPanelHost.Global.AddSubPanel(editorWindow.Form);
                 windows.Add(editorWindow);
             }
-            grid.OnAfterSave += (editor, items) =>
-            {
-                var order = items.FirstOrDefault();
-                var window = windows.FirstOrDefault(x => x.Order == order);
-                window?.AfterSave();
-            };
-
-            JobRequiItems.SelectedRows = [];
         }
     }
 
@@ -466,20 +420,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
 
             Form.Form.DoChanged();
             Form.SetLayoutType<VerticalDynamicEditorGridLayout>();
-            grid.InitialiseEditorForm(Form, [order.Item1], type =>
-            {
-                if(type == typeof(PurchaseOrderItem))
-                {
-                    var table = new CoreTable();
-                    table.LoadColumns(typeof(PurchaseOrderItem));
-                    table.LoadRows(order.Item2.Select(x => x.Item1));
-                    return table;
-                }
-                else
-                {
-                    return null;
-                }
-            }, true);
+            grid.InitialiseEditorForm(Form, [order.Item1], null, true);
 
             var oneToManyPage = Form.Pages?.OfType<SupplierPurchaseOrderItemOneToMany>().FirstOrDefault();
             if(oneToManyPage is null)
@@ -488,6 +429,9 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
             }
             else
             {
+                oneToManyPage.Items.AddRange(order.Item2.Select(x => x.Item1));
+                oneToManyPage.Refresh(false, true);
+
                 oneToManyPage.OnCustomiseEditor += (form, items, column, editor) =>
                 {
                     if(column.ColumnName == $"{nameof(PurchaseOrderItem.Product)}.{nameof(PurchaseOrderItem.Product.ID)}")
@@ -497,7 +441,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 };
                 oneToManyPage.ColumnsLoaded += (o, args) =>
                 {
-                    var column = args.VisibleColumns.FirstOrDefault(x => x.ColumnName == $"{nameof(PurchaseOrderItem.Product)}.{nameof(PurchaseOrderItem.Product.ID)}");
+                    var column = args.DataColumns.FirstOrDefault(x => x.ColumnName == $"{nameof(PurchaseOrderItem.Product)}.{nameof(PurchaseOrderItem.Product.ID)}");
                     if(column is not null)
                     {
                         column.Editor.Editable = column.Editor.Editable.Combine(Editable.Disabled);

+ 204 - 132
prs.desktop/Panels/Reservation Management/Treatment PO/ReservationManagementTreatmentOrderGrid.cs

@@ -71,29 +71,35 @@ public class ReservationManagementTreatmentPOItem : BaseObject
     }
 }
 
-public class StockForecastTreatmentOrderingResult
+public class StockForecastTreatmentOrderingResult(
+    SupplierLink supplier,
+    PurchaseOrder? purchaseOrder,
+    ReservationManagementTreatmentPOItem item,
+    double quantity,
+    SupplierProduct supplierProduct)
 {
-    public SupplierLink Supplier { get; set; }
+    public SupplierLink Supplier { get; set; } = supplier;
 
-    public ReservationManagementTreatmentPOItem Item { get; set; }
+    public PurchaseOrder? PurchaseOrder { get; set; } = purchaseOrder;
 
-    public SupplierProduct SupplierProduct { get; set; }
+    public ReservationManagementTreatmentPOItem Item { get; set; } = item;
 
-    public double Quantity { get; set; }
+    public SupplierProduct SupplierProduct { get; set; } = supplierProduct;
 
-    public StockForecastTreatmentOrderingResult(SupplierLink supplier, ReservationManagementTreatmentPOItem item, double quantity, SupplierProduct supplierProduct)
-    {
-        Supplier = supplier;
-        Item = item;
-        Quantity = quantity;
-        SupplierProduct = supplierProduct;
-    }
+    public double Quantity { get; set; } = quantity;
 }
 
 public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<ReservationManagementTreatmentPOItem>, ISpecificGrid
 {
+    private class OrderSupplier(SupplierLink supplier, PurchaseOrder? purchaseOrder)
+    {
+        public SupplierLink Supplier { get; set; } = supplier;
+
+        public PurchaseOrder? PurchaseOrder { get; set; } = purchaseOrder;
+    }
+
     private List<SupplierProduct> SupplierProducts = [];
-    private SupplierLink[] Suppliers = [];
+    private OrderSupplier[] Suppliers = [];
 
     public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity());
 
@@ -103,6 +109,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
     private readonly Dictionary<Guid, Job> JobDetails = [];
 
     private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
+    private static BitmapImage _search = PRSDesktop.Resources.menu.AsBitmapImage();
 
     public IEnumerable<StockForecastTreatmentOrderingResult> Results
     {
@@ -119,13 +126,13 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
                         continue;
                     }
 
-                    var supplierProduct = GetSupplierProduct(item, supplier.ID);
+                    var supplierProduct = GetSupplierProduct(item, supplier.Supplier.ID);
                     if (supplierProduct is null)
                     {
                         continue;
                     }
                     
-                    yield return new(supplier, item, qty, supplierProduct);
+                    yield return new(supplier.Supplier, supplier.PurchaseOrder, item, qty, supplierProduct);
                 }
             }
         }
@@ -181,7 +188,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
                 var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
                 if(idx != -1)
                 {
-                    var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[idx].ID);
+                    var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[idx].Supplier.ID);
                     if(supplierProduct is null)
                     {
                         return new SolidColorBrush(Colors.Gainsboro);
@@ -198,10 +205,83 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
             }
             return base.GetCellBackground(row, column);
         }
+
+        protected override Style GetColumnGroupHeaderCellStyle(DynamicGridColumnGroup group)
+        {
+            var style = base.GetColumnGroupHeaderCellStyle(group);
+            if(group.Tag is int index)
+            {
+                style.AddSetter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch);
+                style.AddSetter(Control.VerticalContentAlignmentProperty, VerticalAlignment.Stretch);
+                style.AddSetter(Control.PaddingProperty, new Thickness(0.0));
+                style.AddSetter(GridStackedHeaderCellControl.ContentTemplateProperty, TemplateGenerator.CreateDataTemplate(() =>
+                {
+                    var dock = new DockPanel { LastChildFill = true };
+
+                    var button = new Button();
+                    button.Content = new Image
+                    {
+                        Source = _search
+                    };
+                    button.Width = 25;
+                    button.BorderBrush = Colors.Silver.ToBrush();
+                    button.BorderThickness = new Thickness(0.75, 0.0, 0.0, 0.0);
+                    //button.Background = Colors.WhiteSmoke.ToBrush();
+                    DockPanel.SetDock(button, Dock.Right);
+                    dock.Children.Add(button);
+
+                    var border = new Border();
+                    border.Background = Colors.LightYellow.ToBrush();
+                    border.Margin = new Thickness(0.0, 0.0, 0.0, 0.0);
+                    border.BorderThickness = new(0.0);
+
+                    var textBox = new TextBox();
+                    textBox.IsEnabled = false;
+                    textBox.VerticalContentAlignment = VerticalAlignment.Center;
+                    textBox.HorizontalContentAlignment = HorizontalAlignment.Center;
+                    textBox.Padding = new Thickness(2.0, 0.0, 0.0, 0.0);
+                    textBox.Background = Colors.LightYellow.ToBrush();
+                    textBox.BorderThickness = new(0.0);
+                    textBox.Text = Grid.Suppliers[index].PurchaseOrder?.PONumber ?? "(New Order)";
+
+                    border.Child = textBox;
+
+                    DockPanel.SetDock(border, Dock.Left);
+                    dock.Children.Add(border);
+
+                    button.Click += (o, e) =>
+                    {
+                        var supplier = Grid.Suppliers[index];
+                        var dlg = new MultiSelectDialog<PurchaseOrder>(
+                            new Filter<PurchaseOrder>(x => x.ClosedDate).IsEqualTo(DateTime.MinValue)
+                                .And(x => x.SupplierLink.ID).IsEqualTo(supplier.Supplier.ID),
+                            Columns.None<PurchaseOrder>().Add(x => x.ID).Add(x => x.PONumber),
+                            multiselect: false);
+                        var result = dlg.ShowDialog();
+                        if(result == true)
+                        {
+                            var item = dlg.Data().ToObjects<PurchaseOrder>()?.FirstOrDefault();
+                            supplier.PurchaseOrder = item;
+                            textBox.Text = item?.PONumber ?? "(New Order)";
+                        }
+                        else if(result == false)
+                        {
+                            supplier.PurchaseOrder = null;
+                            textBox.Text = "(New Order)";
+                        }
+                    };
+
+                    return dock;
+                }));
+            }
+            return style;
+        }
     }
 
     #endregion
 
+    #region Grid Stuff
+
     private bool _observing = true;
     private void SetObserving(bool observing)
     {
@@ -221,6 +301,10 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         options.FilterRows = true; 
     }
 
+    #endregion
+
+    #region Data
+
     private bool _loadedData = false;
     private void LoadData()
     {
@@ -240,7 +324,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
             new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
             .ToList<SupplierProduct>();
 
-        Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
+        Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).Select(x => new OrderSupplier(x, null)).ToArray();
 
         foreach(var (itemIdx, item) in Items.WithIndex())
         {
@@ -281,7 +365,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
             for(int i = 0; i < Suppliers.Length; ++i)
             {
                 var supplier = Suppliers[i];
-                if(supplierProduct is not null && supplier.ID == supplierProduct.SupplierLink.ID)
+                if(supplierProduct is not null && supplier.Supplier.ID == supplierProduct.SupplierLink.ID)
                 {
                     item.SetQuantity(i, item.RequiredQuantity);
                 }
@@ -297,6 +381,10 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         InvalidateGrid();
     }
 
+    #endregion
+
+    #region Columns
+
     protected override void SaveColumns(DynamicGridColumns columns)
     {
         ColumnsComponent.SaveColumns(columns);
@@ -352,76 +440,21 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         return columns;
     }
 
-    private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
+    protected override void ConfigureColumnGroups()
     {
-        grid.OnCustomiseEditor += (sender, items, column, editor) =>
+        // The first set of header groups is for the supplier Codes
+        var codeGroup = GetColumnGrouping();
+        for(int idx = 0; idx < Suppliers.Length; ++idx)
         {
-            if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
-                || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
-                || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName)
-                || new Column<SupplierProduct>(x => x.Job.ID).IsEqualTo(column.ColumnName)
-                //|| new Column<SupplierProduct>(x => x.Dimensions).IsEqualTo(column.ColumnName)
-            )
-            {
-                editor.Editable = editor.Editable.Combine(Editable.Disabled);
-            }
-        };
-    }
-
-    private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
-    {
-        if (row is null) return;
+            codeGroup.AddGroup(Suppliers[idx].Supplier.Code, QuantityColumns[idx], CostColumns[idx]);
+        }
 
-        column.AddItem("New Supplier", null, row =>
+        // The next set of header groups is for the purchase order combo-boxes.
+        var poGroup = AddColumnGrouping();
+        for(int idx = 0; idx < Suppliers.Length; ++idx)
         {
-            if (row is null) return;
-
-            var selection = new MultiSelectDialog<Supplier>(
-                new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
-                Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
-            if (selection.ShowDialog() != true)
-            {
-                return;
-            }
-
-            var supplier = selection.Data().Rows.First().ToObject<Supplier>();
-            var item = LoadItem(row);
-
-            var supplierProduct = new SupplierProduct();
-            LookupFactory.DoLookup<SupplierProduct, Product, ProductLink>(supplierProduct, x => x.Product, item.TreatmentProduct.ID);
-            supplierProduct.SupplierLink.CopyFrom(supplier);
-
-            if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
-            {
-                SupplierProducts.Add(supplierProduct);
-                var newSuppliers = new SupplierLink[Suppliers.Length + 1];
-                var newIdx = Suppliers.Length;
-
-                for (int i = 0; i < Suppliers.Length; i++)
-                {
-                    newSuppliers[i] = Suppliers[i];
-                }
-                newSuppliers[newIdx] = supplierProduct.SupplierLink;
-
-                foreach (var (itemIdx, oItem) in Items.WithIndex())
-                {
-                    var populateSupplierProduct = GetSupplierProduct(oItem);
-
-                    var quantities = new double[newSuppliers.Length];
-                    for (int i = 0; i < Suppliers.Length; ++i)
-                    {
-                        quantities[i] = oItem.GetQuantity(i);
-                    }
-                    quantities[newIdx] = 0;
-
-                    oItem.SetQuantities(quantities);
-                }
-
-                Suppliers = newSuppliers;
-
-                Refresh(true, true);
-            }
-        });
+            poGroup.AddGroup(Suppliers[idx].Supplier.Code, QuantityColumns[idx], CostColumns[idx], tag: idx);
+        }
     }
 
     private BitmapImage? Warning_Image(CoreRow? row)
@@ -439,14 +472,6 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         }
     }
 
-    protected override void ConfigureColumnGroups()
-    {
-        for(int idx = 0; idx < Suppliers.Length; ++idx)
-        {
-            GetColumnGrouping().AddGroup(Suppliers[idx].Code, QuantityColumns[idx], CostColumns[idx]);
-        }
-    }
-
     private class QuantityControl : ContentControl
     {
         private readonly ReservationManagementTreatmentPOItem Item;
@@ -464,7 +489,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
 
         public void UpdateControl()
         {
-            var supplierProduct = Parent.GetSupplierProduct(Item, Parent.Suppliers[SupplierIndex].ID);
+            var supplierProduct = Parent.GetSupplierProduct(Item, Parent.Suppliers[SupplierIndex].Supplier.ID);
             if(supplierProduct is null)
             {
                 Content = null;
@@ -502,7 +527,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
 
     private void InitialiseSupplierColumn(int idx)
     {
-        var supplierProducts = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID).ToArray();
+        var supplierProducts = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].Supplier.ID).ToArray();
 
         var contextMenuFunc = (CoreRow[]? rows) =>
         {
@@ -510,7 +535,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
             if (row is null) return null;
 
             var item = LoadItem(row);
-            var supplierProduct = GetSupplierProduct(item, Suppliers[idx].ID);
+            var supplierProduct = GetSupplierProduct(item, Suppliers[idx].Supplier.ID);
             if (supplierProduct is not null)
             {
                 return null;
@@ -544,7 +569,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
 
             var instance = LoadItem(row);
             var qty = instance.GetQuantity(idx);
-            var supplierProduct = GetSupplierProduct(instance, Suppliers[idx].ID);
+            var supplierProduct = GetSupplierProduct(instance, Suppliers[idx].Supplier.ID);
             if(supplierProduct is not null)
             {
                 return $"{qty * supplierProduct.CostPrice * instance.Multiplier:C2}";
@@ -572,7 +597,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         return rows.Sum(row =>
         {
             var item = LoadItem(row);
-            var supplierProduct = GetSupplierProduct(item, Suppliers[supplierIdx].ID);
+            var supplierProduct = GetSupplierProduct(item, Suppliers[supplierIdx].Supplier.ID);
             if (supplierProduct is not null)
             {
                 var qty = item.GetQuantity(supplierIdx);
@@ -585,13 +610,93 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         });
     }
 
+    #endregion
+
+    #region New Suppliers
+
+    private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
+    {
+        grid.OnCustomiseEditor += (sender, items, column, editor) =>
+        {
+            if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
+                || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
+                || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName)
+                || new Column<SupplierProduct>(x => x.Job.ID).IsEqualTo(column.ColumnName)
+                //|| new Column<SupplierProduct>(x => x.Dimensions).IsEqualTo(column.ColumnName)
+            )
+            {
+                editor.Editable = editor.Editable.Combine(Editable.Disabled);
+            }
+        };
+    }
+
+    private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
+    {
+        if (row is null) return;
+
+        column.AddItem("New Supplier", null, row =>
+        {
+            if (row is null) return;
+
+            var selection = new MultiSelectDialog<Supplier>(
+                new Filter<Supplier>(x => x.ID).NotInList(Suppliers.ToArray(x => x.Supplier.ID)),
+                Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
+            if (selection.ShowDialog() != true)
+            {
+                return;
+            }
+
+            var supplier = selection.Data().Rows.First().ToObject<Supplier>();
+            var item = LoadItem(row);
+
+            var supplierProduct = new SupplierProduct();
+            LookupFactory.DoLookup<SupplierProduct, Product, ProductLink>(supplierProduct, x => x.Product, item.TreatmentProduct.ID);
+            supplierProduct.SupplierLink.CopyFrom(supplier);
+
+            if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
+            {
+                SupplierProducts.Add(supplierProduct);
+                var newSuppliers = new OrderSupplier[Suppliers.Length + 1];
+                var newIdx = Suppliers.Length;
+
+                for (int i = 0; i < Suppliers.Length; i++)
+                {
+                    newSuppliers[i] = Suppliers[i];
+                }
+                newSuppliers[newIdx] = new(supplierProduct.SupplierLink, null);
+
+                foreach (var (itemIdx, oItem) in Items.WithIndex())
+                {
+                    var populateSupplierProduct = GetSupplierProduct(oItem);
+
+                    var quantities = new double[newSuppliers.Length];
+                    for (int i = 0; i < Suppliers.Length; ++i)
+                    {
+                        quantities[i] = oItem.GetQuantity(i);
+                    }
+                    quantities[newIdx] = 0;
+
+                    oItem.SetQuantities(quantities);
+                }
+
+                Suppliers = newSuppliers;
+
+                Refresh(true, true);
+            }
+        });
+    }
+
+    #endregion
+
+    #region Supplier Product Matching
+
     private void CreateSupplierProduct_Click(Tuple<ReservationManagementTreatmentPOItem, int> tuple)
     {
         var (item, supplierIdx) = tuple;
 
         var supplierProduct = new SupplierProduct();
         LookupFactory.DoLookup<SupplierProduct, Product, ProductLink>(supplierProduct, x => x.Product, item.TreatmentProduct.ID);
-        supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]);
+        supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx].Supplier);
 
         if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
         {
@@ -623,39 +728,6 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
     {
         return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x, false));
     }
-    //private double GetQuantity(SupplierProduct product)
-    //{
-    //    var instance = ProductInstances.WithIndex().Where(x => x.Value.Product.ID == product.ID)
-    //}
-
-    private class CostAggregate : ISummaryAggregate
-    {
-        public double Sum { get; private set; }
-
-        private int SupplierIndex;
-
-        private ReservationManagementTreatmentOrderGrid Grid;
-
-        public CostAggregate(int supplierIndex, ReservationManagementTreatmentOrderGrid grid)
-        {
-            SupplierIndex = supplierIndex;
-            Grid = grid;
-        }
 
-        public Action<IEnumerable, string, PropertyDescriptor> CalculateAggregateFunc()
-        {
-            return AggregateFunc;
-        }
-
-        private void AggregateFunc(IEnumerable items, string property, PropertyDescriptor args)
-        {
-            if (items is IEnumerable<DataRowView> rows)
-            {
-            }
-            else
-            {
-                Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'.");
-            }
-        }
-    }
+    #endregion
 }

+ 10 - 3
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderScreen.xaml

@@ -15,7 +15,8 @@
             <RowDefinition Height="Auto"/>
         </Grid.RowDefinitions>
         <local:StockForecastOrderingGrid x:Name="Grid" Margin="5,5,5,0"
-                                         OnChanged="Grid_OnChanged"/>
+                                         OnChanged="Grid_OnChanged"
+                                         OnSelectItem="Grid_OnSelectItem"/>
         <DockPanel Grid.Row="1" LastChildFill="False" x:Name="Buttons">
             <Label DockPanel.Dock="Left" Margin="5"
                    x:Name="OrderTypeLabel"
@@ -35,14 +36,20 @@
                 Content="Order Strategy: "
                 VerticalAlignment="Stretch" 
                 VerticalContentAlignment="Center"/>
-
             <ComboBox 
                 x:Name="OrderStrategyBox" 
                 DockPanel.Dock="Left" 
                 Margin="0,5,5,5"
                 MinWidth="140"
                 SelectionChanged="OrderStrategyBox_OnSelectionChanged"/>
-            
+
+            <Button x:Name="StyleSelectButton"
+                    DockPanel.Dock="Left"
+                    IsEnabled="False"
+                    Margin="5" Padding="5"
+                    Content="Select Style"
+                    Click="StyleSelectButton_Click"/>
+
             <Button x:Name="CancelButton" Click="CancelButton_Click"
                     Content="Cancel"
                     Margin="5" Padding="5" MinWidth="60"

+ 23 - 0
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderScreen.xaml.cs

@@ -19,6 +19,7 @@ using System.Windows.Media.Imaging;
 using System.Windows.Shapes;
 using InABox.Configuration;
 using InABox.Clients;
+using InABox.DynamicGrid;
 
 namespace PRSDesktop;
 
@@ -226,4 +227,26 @@ public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
 
         MessageWindow.ShowMessage($"The following orders were created:\n- {string.Join("\n- ",orders.Select(x=>x.Item1.PONumber))}", $"Created {orders.Count} orders");
     }
+
+    private void StyleSelectButton_Click(object sender, RoutedEventArgs e)
+    {
+        var dlg = new MultiSelectDialog<ProductStyle>(
+            null,
+            Columns.None<ProductStyle>().Add(x => x.ID).Add(x => x.Code),
+            multiselect: false);
+        var result = dlg.ShowDialog();
+        if(result == true)
+        {
+            Grid.SetStyle(dlg.Data().ToObjects<ProductStyle>().First());
+        }
+        else if(result == false)
+        {
+            Grid.SetStyle(null);
+        }
+    }
+
+    private void Grid_OnSelectItem(object sender, InABox.DynamicGrid.DynamicGridSelectionEventArgs e)
+    {
+        StyleSelectButton.IsEnabled = e.Rows is not null && e.Rows.Length > 0;
+    }
 }

+ 216 - 182
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderingGrid.cs

@@ -211,6 +211,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
 {
     #region Internal Data + Caches
 
+    private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
+
     private List<SupplierProduct> SupplierProducts = [];
     private SupplierLink[] Suppliers = [];
 
@@ -223,8 +225,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
 
     #endregion
 
-    private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
-
     #region Public Properties
 
     public IList<StockForecastOrderData> OrderData { get; set; }
@@ -316,6 +316,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
     public StockForecastOrderingGrid()
     {
         HiddenColumns.Add(x => x.Product.Image.ID);
+        HiddenColumns.Add(x => x.Style.Code);
     }
 
     private static SupplierProductOrderStrategy CastOrderStrategyToProductOrderStrategy(StockForecastOrderingStrategy strategy, SupplierProductOrderStrategy defaultValue)
@@ -365,11 +366,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             DataGrid.FrozenColumnCount = 8;
         }
 
-        protected override Brush? GetCellSelectionBackgroundBrush()
-        {
-            return null;
-        }
-
         protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
         {
             var item = Grid.LoadItem(row);
@@ -407,9 +403,12 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
     protected override void DoReconfigure(DynamicGridOptions options)
     {
         options.Clear();
-        options.FilterRows = true; 
+        options.FilterRows = true;
+        options.MultiSelect = true;
     }
 
+    #region Data
+
     private bool _loadedData = false;
     private void LoadData()
     {
@@ -439,7 +438,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         _loadedData = true;
     }
 
-    private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx)
+    private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx, int supplierIdx)
     {
         var qty = new StockForecastOrderingItemQuantity();
         qty.Changed += () =>
@@ -447,12 +446,64 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             if (!_observing) return;
 
             var row = Data.Rows[itemIdx];
-            InvalidateRow(row);
+            foreach(var ac in ActionColumns)
+            {
+                if(ac != QuantityColumns[supplierIdx])
+                {
+                    UpdateCell(row, ac);
+                }
+            }
             DoChanged();
         };
         return qty;
     }
 
+    private void CalculateQuantities(bool recreateItems)
+    {
+        SetObserving(false);
+
+        if (recreateItems)
+        {
+            Items.Clear();
+            foreach(var dataItem in OrderData)
+            {
+                var item = new StockOrderingItem();
+                item.Product.CopyFrom(dataItem.Product);
+                item.Style.CopyFrom(dataItem.Style);
+                item.Dimensions.CopyFrom(dataItem.Dimensions);
+                item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
+                item.RequiredQuantity = dataItem.RequiredQuantity;
+                foreach(var breakup in dataItem.GetRequiredQuantities())
+                {
+                    item.SetJobRequiredQuantity(breakup.JobID, breakup.JobRequiItemID, breakup.Quantity);
+                }
+                Items.Add(item);
+            }
+        }
+
+        foreach(var (itemIdx, item) in Items.WithIndex())
+        {
+            var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
+            for(int i = 0; i < Suppliers.Length; ++i)
+            {
+                quantities[i] = CreateQuantity(itemIdx, i);
+            }
+
+            item.SetQuantities(quantities);
+        }
+
+        foreach(var item in Items)
+        {
+            CalculateSupplierProduct(item);
+        }
+        SetObserving(true);
+        DoChanged();
+    }
+
+    #endregion
+
+    #region Order Strategy
+
     private SupplierProduct? CalculateSupplierProduct(StockOrderingItem item, int supplierIdx)
     {
         var supplierProducts = string.IsNullOrWhiteSpace(item.Dimensions.Unit.Conversion)
@@ -509,50 +560,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         }
     }
 
-    private void CalculateQuantities(bool recreateItems)
-    {
-        SetObserving(false);
-
-        if (recreateItems)
-        {
-            Items.Clear();
-            foreach(var dataItem in OrderData)
-            {
-                var item = new StockOrderingItem();
-                item.Product.CopyFrom(dataItem.Product);
-                item.Style.CopyFrom(dataItem.Style);
-                item.Dimensions.CopyFrom(dataItem.Dimensions);
-                item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
-                item.RequiredQuantity = dataItem.RequiredQuantity;
-                foreach(var breakup in dataItem.GetRequiredQuantities())
-                {
-                    item.SetJobRequiredQuantity(breakup.JobID, breakup.JobRequiItemID, breakup.Quantity);
-                }
-                Items.Add(item);
-            }
-        }
-
-        foreach(var (itemIdx, item) in Items.WithIndex())
-        {
-            var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
-            for(int i = 0; i < Suppliers.Length; ++i)
-            {
-                quantities[i] = CreateQuantity(itemIdx);
-            }
-
-            item.SetQuantities(quantities);
-        }
-
-        foreach(var item in Items)
-        {
-            CalculateSupplierProduct(item);
-        }
-        SetObserving(true);
-        DoChanged();
-    }
-
-    #region Order Strategy
-
     private double CalculateSupplierProductRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
     {
         var supplierIdx = Suppliers.WithIndex().FirstOrDefault(x => x.Value.ID == supplierProduct.SupplierLink.ID).Key;
@@ -623,6 +630,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
 
     #endregion
 
+    #region Columns
+
     private bool _loadedColumns = false;
     protected override DynamicGridColumns LoadColumns()
     {
@@ -690,90 +699,12 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         return columns;
     }
 
-    private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
-    {
-        grid.OnCustomiseEditor += (sender, items, column, editor) =>
-        {
-            if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
-                || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
-                || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName))
-            {
-                editor.Editable = editor.Editable.Combine(Editable.Disabled);
-            }
-        };
-    }
-
-    private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
+    protected override void ConfigureColumnGroups()
     {
-        if (row is null) return;
-
-        column.AddItem("New Supplier", null, row =>
+        for(int idx = 0; idx < Suppliers.Length; ++idx)
         {
-            if (row is null) return;
-
-            var selection = new MultiSelectDialog<Supplier>(
-                new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
-                Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
-            if (selection.ShowDialog() != true)
-            {
-                return;
-            }
-
-            var supplier = selection.Data().Rows.First().ToObject<Supplier>();
-            var orderingItem = LoadItem(row);
-
-            var supplierProduct = new SupplierProduct();
-            supplierProduct.Product.CopyFrom(orderingItem.Product);
-            supplierProduct.Style.CopyFrom(orderingItem.Style);
-            supplierProduct.Dimensions.CopyFrom(orderingItem.Dimensions);
-            supplierProduct.SupplierLink.CopyFrom(supplier);
-
-            if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
-            {
-                SupplierProducts.Add(supplierProduct);
-                var newSuppliers = new SupplierLink[Suppliers.Length + 1];
-                var newIdx = Suppliers.Length;
-
-                for (int i = 0; i < Suppliers.Length; i++)
-                {
-                    newSuppliers[i] = Suppliers[i];
-                }
-                newSuppliers[newIdx] = supplierProduct.SupplierLink;
-
-                foreach (var (itemIdx, item) in Items.WithIndex())
-                {
-                    var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length];
-                    for (int i = 0; i < Suppliers.Length; ++i)
-                    {
-                        quantities[i] = item.GetQuantity(i);
-                    }
-                    var newQty = CreateQuantity(itemIdx);
-
-                    quantities[newIdx] = newQty;
-                    if(OrderType == StockForecastOrderingType.StockOrder)
-                    {
-                        newQty.OrderTotal = 0;
-                    }
-                    else
-                    {
-                        newQty.OrderTotal = 0;
-                        foreach(var id in item.GetJobRequiredQuantities().Keys)
-                        {
-                            newQty.Breakups[id] = 0;
-                        }
-                    }
-                    item.SetQuantities(quantities);
-                }
-                Suppliers = newSuppliers;
-                foreach (var item in Items)
-                {
-                    CalculateSupplierProduct(item, newIdx);
-                }
-
-                _loadedColumns = false;
-                Refresh(true, true);
-            }
-        });
+            GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]);
+        }
     }
 
     private BitmapImage? Warning_Image(CoreRow? row)
@@ -791,51 +722,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         }
     }
 
-    protected override void ConfigureColumnGroups()
-    {
-        for(int idx = 0; idx < Suppliers.Length; ++idx)
-        {
-            GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]);
-        }
-    }
-
-    #region Job Data Cache
-
-    private void LoadJobData(IEnumerable<Guid> ids)
-    {
-        var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray();
-        if(neededIDs.Length > 0)
-        {
-            var details = Client.Query(
-                new Filter<Job>(x => x.ID).InList(neededIDs),
-                Columns.None<Job>().Add(x => x.ID)
-                    .Add(x => x.JobNumber)
-                    .Add(x => x.Name));
-            foreach(var job in details.ToObjects<Job>())
-            {
-                JobDetails[job.ID] = job;
-            }
-        }
-    }
-    private void LoadJobRequiData(IEnumerable<Guid> ids)
-    {
-        var neededIDs = ids.Where(x => x != Guid.Empty && !JobRequiDetails.ContainsKey(x)).ToArray();
-        if(neededIDs.Length > 0)
-        {
-            var details = Client.Query(
-                new Filter<JobRequisitionItem>(x => x.ID).InList(neededIDs),
-                Columns.None<JobRequisitionItem>().Add(x => x.ID)
-                    .Add(x => x.Requisition.Number)
-                    .Add(x => x.Requisition.Description));
-            foreach(var requi in details.ToObjects<JobRequisitionItem>())
-            {
-                JobRequiDetails[requi.ID] = requi;
-            }
-        }
-    }
-
-    #endregion
-
     private class QuantityControl : ContentControl
     {
         private readonly StockOrderingItem Item;
@@ -1015,7 +901,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
                 ? SupplierProducts.Where(x => x.Dimensions.Equals(instance.Dimensions))
                 : SupplierProducts;
 
-            var items = supplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID)
+            var items = supplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID && x.Style.ID == instance.Style.ID)
                 .Select(x => new KeyValuePair<SupplierProduct?, string>(x, x.Job.ID == Guid.Empty ? x.Dimensions.UnitSize : $"Job {x.Job.JobNumber}: {x.Dimensions.UnitSize}"));
             if (items.Any())
                 items = items.Prepend(new KeyValuePair<SupplierProduct?, string>(null, ""));
@@ -1112,6 +998,97 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         });
     }
 
+
+    #endregion
+
+    #region New Suppliers and Supplier Products
+
+    private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
+    {
+        grid.OnCustomiseEditor += (sender, items, column, editor) =>
+        {
+            if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
+                || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
+                || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName))
+            {
+                editor.Editable = editor.Editable.Combine(Editable.Disabled);
+            }
+        };
+    }
+
+    private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
+    {
+        if (row is null) return;
+
+        column.AddItem("New Supplier", null, row =>
+        {
+            if (row is null) return;
+
+            var selection = new MultiSelectDialog<Supplier>(
+                new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
+                Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
+            if (selection.ShowDialog() != true)
+            {
+                return;
+            }
+
+            var supplier = selection.Data().Rows.First().ToObject<Supplier>();
+            var orderingItem = LoadItem(row);
+
+            var supplierProduct = new SupplierProduct();
+            supplierProduct.Product.CopyFrom(orderingItem.Product);
+            supplierProduct.Style.CopyFrom(orderingItem.Style);
+            supplierProduct.Dimensions.CopyFrom(orderingItem.Dimensions);
+            supplierProduct.SupplierLink.CopyFrom(supplier);
+
+            if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
+            {
+                SupplierProducts.Add(supplierProduct);
+                var newSuppliers = new SupplierLink[Suppliers.Length + 1];
+                var newIdx = Suppliers.Length;
+
+                for (int i = 0; i < Suppliers.Length; i++)
+                {
+                    newSuppliers[i] = Suppliers[i];
+                }
+                newSuppliers[newIdx] = supplierProduct.SupplierLink;
+
+                foreach (var (itemIdx, item) in Items.WithIndex())
+                {
+                    var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length];
+                    for (int i = 0; i < Suppliers.Length; ++i)
+                    {
+                        quantities[i] = item.GetQuantity(i);
+                    }
+                    var newQty = CreateQuantity(itemIdx, newSuppliers.Length - 1);
+
+                    quantities[newIdx] = newQty;
+                    if(OrderType == StockForecastOrderingType.StockOrder)
+                    {
+                        newQty.OrderTotal = 0;
+                    }
+                    else
+                    {
+                        newQty.OrderTotal = 0;
+                        foreach(var id in item.GetJobRequiredQuantities().Keys)
+                        {
+                            newQty.Breakups[id] = 0;
+                        }
+                    }
+                    item.SetQuantities(quantities);
+                }
+                Suppliers = newSuppliers;
+                foreach (var item in Items)
+                {
+                    CalculateSupplierProduct(item, newIdx);
+                }
+
+                _loadedColumns = false;
+                Refresh(true, true);
+            }
+        });
+    }
+
     private void CreateSupplierProduct_Click(Tuple<StockOrderingItem, int> tuple)
     {
         var (item, supplierIdx) = tuple;
@@ -1138,4 +1115,61 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             InvalidateGrid();
         }
     }
+
+    #endregion
+
+    #region Job Data Cache
+
+    private void LoadJobData(IEnumerable<Guid> ids)
+    {
+        var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray();
+        if(neededIDs.Length > 0)
+        {
+            var details = Client.Query(
+                new Filter<Job>(x => x.ID).InList(neededIDs),
+                Columns.None<Job>().Add(x => x.ID)
+                    .Add(x => x.JobNumber)
+                    .Add(x => x.Name));
+            foreach(var job in details.ToObjects<Job>())
+            {
+                JobDetails[job.ID] = job;
+            }
+        }
+    }
+    private void LoadJobRequiData(IEnumerable<Guid> ids)
+    {
+        var neededIDs = ids.Where(x => x != Guid.Empty && !JobRequiDetails.ContainsKey(x)).ToArray();
+        if(neededIDs.Length > 0)
+        {
+            var details = Client.Query(
+                new Filter<JobRequisitionItem>(x => x.ID).InList(neededIDs),
+                Columns.None<JobRequisitionItem>().Add(x => x.ID)
+                    .Add(x => x.Requisition.Number)
+                    .Add(x => x.Requisition.Description));
+            foreach(var requi in details.ToObjects<JobRequisitionItem>())
+            {
+                JobRequiDetails[requi.ID] = requi;
+            }
+        }
+    }
+
+    #endregion
+
+    public void SetStyle(ProductStyle? style)
+    {
+        foreach(var row in SelectedRows)
+        {
+            var item = LoadItem(row);
+            if(style is null)
+            {
+                item.Style.CopyFrom(new ProductStyleLink());
+            }
+            else
+            {
+                item.Style.CopyFrom(style);
+            }
+            CalculateSupplierProduct(item);
+            UpdateRow(row, item);
+        }
+    }
 }

+ 2 - 2
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderingJobGrid.cs

@@ -66,8 +66,8 @@ public class StockForecastOrderingJobGrid : DynamicItemsListGrid<StockForecastOr
     {
         var columns = new DynamicGridColumns();
         columns.Add<StockForecastOrderingJobItem, string>(x => x.Description, 0, "Description", "", Alignment.MiddleLeft);
-        columns.Add<StockForecastOrderingJobItem, double>(x => x.RequiredQuantity, 70, "Req. Qty.", "F2", Alignment.MiddleCenter);
-        columns.Add<StockForecastOrderingJobItem, double>(x => x.Quantity, 70, "Qty.", "F2", Alignment.MiddleCenter);
+        columns.Add<StockForecastOrderingJobItem, double>(x => x.RequiredQuantity, 70, "Req. Qty.");
+        columns.Add<StockForecastOrderingJobItem, double>(x => x.Quantity, 70, "Qty.");
         return columns;
     }
 

+ 7 - 10
prs.desktop/Panels/Tasks/TaskPanel.xaml.cs

@@ -114,8 +114,7 @@ namespace PRSDesktop
 
         private void CompleteTask(ITaskControl control, RoutedEventArgs e, DateTime completed)
         {
-            if (MessageBox.Show($"Are you sure you want to complete the selected tasks?", "Confirm Completion",
-                    MessageBoxButton.YesNo) != MessageBoxResult.Yes)
+            if (!MessageWindow.ShowYesNo("Are you sure you want to complete the selected tasks?", "Confirm Completion"))
                 return;
             var tasks = (((FrameworkElement)e.Source).Tag as IEnumerable<TaskModel>)!;
             Progress.ShowModal("Completing Tasks", progress =>
@@ -139,8 +138,7 @@ namespace PRSDesktop
         private void ChangeStatus_Click(Tuple<ITaskControl, TaskModel[], KanbanStatus> obj)
         {
             var (control, tasks, status) = obj;
-            if (MessageBox.Show($"Are you sure you want to mark the selected tasks as {status}?", "Confirm Change Status",
-                    MessageBoxButton.YesNo) != MessageBoxResult.Yes)
+            if (!MessageWindow.ShowYesNo($"Are you sure you want to mark the selected tasks as {status}?", "Confirm Change Status"))
                 return;
             Progress.ShowModal("Changing Status", progress =>
             {
@@ -239,14 +237,14 @@ namespace PRSDesktop
 
                     menu.AddItem("Create Setout from Task", null, models.First(), task =>
                     {
-                        if (MessageBox.Show("This will convert this task into a Setout.\n\nDo you wish to continue?", "Confirmation", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
+                        if (!MessageWindow.ShowYesNo("This will convert this task into a Setout.\n\nDo you wish to continue?", "Confirmation"))
                             return;
 
                         ManufacturingTemplate? template = new Client<ManufacturingTemplate>()
                             .Load(new Filter<ManufacturingTemplate>(x => x.Code).IsEqualTo("PRS")).FirstOrDefault();
                         if (template == null)
                         {
-                            MessageBox.Show("[Pressing] Template does not exist!");
+                            MessageWindow.ShowMessage("[Pressing] Template does not exist!", "No template");
                             return;
                         }
 
@@ -278,7 +276,7 @@ namespace PRSDesktop
                             kanban = tables["Kanban"].Rows.FirstOrDefault()?.ToObject<Kanban>();
                             if (kanban == null)
                             {
-                                MessageBox.Show("Task does not exist!");
+                                MessageWindow.ShowMessage("Task does not exist!", "No task");
                                 return;
                             }
 
@@ -446,7 +444,7 @@ namespace PRSDesktop
                                 kanban.Title += $" (Requi #{result.Number})";
                                 new Client<Kanban>().Save(kanban, "Converted to Requisition", (_, __) => { });
                             });
-                            MessageBox.Show(String.Format("Created Requisition {0}", result.Number));
+                            MessageWindow.ShowMessage($"Created Requisition {result.Number}", "Success");
                             control.Refresh();
                         }
                     });
@@ -533,8 +531,7 @@ namespace PRSDesktop
                 {
                     menu.AddItem(models.Length > 1 ? "Archive Tasks" : "Archive Task", null, models, tasks =>
                     {
-                        if (MessageBox.Show("Are you sure you want to remove the selected tasks from the list?", "Confirm removal",
-                                MessageBoxButton.YesNo) != MessageBoxResult.Yes)
+                        if (!MessageWindow.ShowYesNo("Are you sure you want to remove the selected tasks from the list?", "Confirm removal"))
                             return;
 
                         Progress.ShowModal("Closing Kanbans", progress =>