Bladeren bron

New StockSummary Order screen

Kenric Nugteren 1 jaar geleden
bovenliggende
commit
9cef61eb62

+ 26 - 0
prs.desktop/Panels/StockSummary/OrderScreen/StockSummaryOrderJobScreen.xaml

@@ -0,0 +1,26 @@
+<Window x:Class="PRSDesktop.Panels.StockSummary.OrderScreen.StockSummaryOrderJobScreen"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:PRSDesktop.Panels.StockSummary.OrderScreen"
+        mc:Ignorable="d"
+        Title="Select Job Quantities" Height="450" Width="600">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="*"/>
+            <RowDefinition Height="Auto"/>
+        </Grid.RowDefinitions>
+        <local:StockSummaryOrderingJobGrid x:Name="Grid" Margin="5,5,5,0"/>
+        <DockPanel Grid.Row="1" LastChildFill="False" x:Name="Buttons">
+            <Button x:Name="CancelButton" Click="CancelButton_Click"
+                    Content="Cancel"
+                    Margin="5" Padding="5" MinWidth="60"
+                    DockPanel.Dock="Right"/>
+            <Button x:Name="OKButton" Click="OKButton_Click"
+                    Content="OK"
+                    Margin="5,5,0,5" Padding="5" MinWidth="60"
+                    DockPanel.Dock="Right"/>
+        </DockPanel>
+    </Grid>
+</Window>

+ 49 - 0
prs.desktop/Panels/StockSummary/OrderScreen/StockSummaryOrderJobScreen.xaml.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace PRSDesktop.Panels.StockSummary.OrderScreen;
+/// <summary>
+/// Interaction logic for StockSummaryOrderJobScreen.xaml
+/// </summary>
+public partial class StockSummaryOrderJobScreen : Window
+{
+    public List<StockSummaryOrderingJobItem> Items
+    {
+        get => Grid.Items;
+        set
+        {
+            Grid.Items = value;
+            Grid.Refresh(false, true);
+        }
+    }
+
+    public StockSummaryOrderJobScreen()
+    {
+        InitializeComponent();
+
+        Grid.Refresh(true, false);
+    }
+
+    private void CancelButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = false;
+        Close();
+    }
+
+    private void OKButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = true;
+        Close();
+    }
+}

+ 38 - 0
prs.desktop/Panels/StockSummary/OrderScreen/StockSummaryOrderScreen.xaml

@@ -0,0 +1,38 @@
+<Window x:Class="PRSDesktop.Panels.StockSummary.OrderScreen.StockSummaryOrderScreen"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:PRSDesktop.Panels.StockSummary.OrderScreen"
+        mc:Ignorable="d"
+        Title="Order Stock" Height="450" Width="1000"
+        x:Name="Window">
+    <Grid DataContext="{Binding ElementName=Window}">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="*"/>
+            <RowDefinition Height="Auto"/>
+        </Grid.RowDefinitions>
+        <local:StockSummaryOrderingGrid x:Name="Grid" Margin="5,5,5,0"
+                                        OnChanged="Grid_OnChanged"/>
+        <DockPanel Grid.Row="1" LastChildFill="False" x:Name="Buttons">
+            <Label DockPanel.Dock="Left" Margin="5"
+                   Content="Order Type: "
+                   VerticalAlignment="Stretch" VerticalContentAlignment="Center"/>
+            <ComboBox x:Name="OrderTypeBox" DockPanel.Dock="Left" Margin="0,5,5,5" Width="90"
+                      VerticalContentAlignment="Center"
+                      SelectedValuePath="Tag" SelectedValue="{Binding OrderType}">
+                <ComboBoxItem Content="Job Order" Tag="{x:Static local:StockSummaryOrderingType.JobOrder}"/>
+                <ComboBoxItem Content="Stock Order" Tag="{x:Static local:StockSummaryOrderingType.StockOrder}"/>
+            </ComboBox>
+            <Button x:Name="CancelButton" Click="CancelButton_Click"
+                    Content="Cancel"
+                    Margin="5" Padding="5" MinWidth="60"
+                    DockPanel.Dock="Right"/>
+            <Button x:Name="OKButton" Click="OKButton_Click"
+                    Content="OK"
+                    Margin="5,5,0,5" Padding="5" MinWidth="60"
+                    DockPanel.Dock="Right"
+                    IsEnabled="{Binding CanSave}"/>
+        </DockPanel>
+    </Grid>
+</Window>

+ 75 - 0
prs.desktop/Panels/StockSummary/OrderScreen/StockSummaryOrderScreen.xaml.cs

@@ -0,0 +1,75 @@
+using Comal.Classes;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace PRSDesktop.Panels.StockSummary.OrderScreen;
+
+/// <summary>
+/// Interaction logic for StockSummaryOrderScreen.xaml
+/// </summary>
+public partial class StockSummaryOrderScreen : Window, INotifyPropertyChanged
+{
+    private bool _canSave;
+    public bool CanSave
+    {
+        get => _canSave;
+        set
+        {
+            _canSave = true;
+            OnPropertyChanged();
+        }
+    }
+
+    public StockSummaryOrderingType OrderType
+    {
+        get => Grid.OrderType;
+        set => Grid.OrderType = value;
+    }
+
+    public IEnumerable<StockSummaryOrderingResult> Results => Grid.Results;
+
+    public StockSummaryOrderScreen(List<StockSummaryOrderingItem> items)
+    {
+        InitializeComponent();
+
+        Grid.Items = items;
+        Grid.Refresh(true, true);
+    }
+
+    private void CancelButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = false;
+        Close();
+    }
+
+    private void OKButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = true;
+        Close();
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+
+    private void Grid_OnChanged(object sender, EventArgs e)
+    {
+        CanSave = Grid.TotalQuantity > 0;
+    }
+}

+ 679 - 0
prs.desktop/Panels/StockSummary/OrderScreen/StockSummaryOrderingGrid.cs

@@ -0,0 +1,679 @@
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.Wpf;
+using InABox.WPF;
+using NPOI.SS.Formula.Functions;
+using Syncfusion.Data;
+using Syncfusion.Data.Extensions;
+using Syncfusion.UI.Xaml.Grid;
+using Syncfusion.Windows.Shared;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace PRSDesktop.Panels.StockSummary.OrderScreen;
+
+public enum StockSummaryOrderingType
+{
+    StockOrder,
+    JobOrder
+}
+
+public class StockSummaryOrderingItemQuantity
+{
+    public event Action? Changed;
+
+    private double _stockTotal;
+    public double StockTotal
+    {
+        get => _stockTotal;
+        set
+        {
+            _stockTotal = value;
+            Changed?.Invoke();
+        }
+    }
+
+    public Dictionary<Guid, double> JobTotals { get; init; } = [];
+
+    public void DoChanged()
+    {
+        Changed?.Invoke();
+    }
+
+    public double JobTotal => JobTotals.Sum(x => x.Value);
+
+    public double GetTotal(StockSummaryOrderingType type) => type == StockSummaryOrderingType.StockOrder
+        ? StockTotal
+        : JobTotal;
+}
+
+public class StockSummaryOrderingItem : BaseObject
+{
+    [EditorSequence(1)]
+    public ProductLink Product { get; set; }
+
+    [EditorSequence(2)]
+    public ProductStyleLink Style { get; set; }
+
+    [EditorSequence(3)]
+    public StockDimensions Dimensions { get; set; }
+
+    [EditorSequence(4)]
+    [DoubleEditor]
+    public double RequiredQuantity { get; set; }
+
+    private Dictionary<Guid, double> JobRequiredQuantities { get; set; } = [];
+
+    public Dictionary<Guid, double> GetJobRequiredQuantities()
+    {
+        return JobRequiredQuantities;
+    }
+    public void SetJobRequiredQuantity(Guid jobID, double requiredQty)
+    {
+        JobRequiredQuantities[jobID] = requiredQty;
+    }
+
+    private StockSummaryOrderingItemQuantity[] Quantities = [];
+
+    public StockSummaryOrderingItemQuantity GetQuantity(int i) => Quantities[i];
+
+    public double GetTotalQuantity(StockSummaryOrderingType type) => type == StockSummaryOrderingType.StockOrder
+        ? Quantities.Sum(x => x.StockTotal)
+        : Quantities.Sum(x => x.JobTotal);
+
+    public void SetQuantities(StockSummaryOrderingItemQuantity[] quantities)
+    {
+        Quantities = quantities;
+    }
+}
+
+public class StockSummaryOrderingResult
+{
+    public SupplierLink Supplier { get; set; }
+
+    public JobLink? Job { get; set; }
+
+    public StockSummaryOrderingItem Item { get; set; }
+
+    public SupplierProduct SupplierProduct { get; set; }
+
+    public double Quantity { get; set; }
+
+    public StockSummaryOrderingResult(SupplierLink supplier, JobLink? job, StockSummaryOrderingItem item, double quantity, SupplierProduct supplierProduct)
+    {
+        Supplier = supplier;
+        Job = job;
+        Item = item;
+        Quantity = quantity;
+        SupplierProduct = supplierProduct;
+    }
+}
+
+public class StockSummaryOrderingGrid : DynamicItemsListGrid<StockSummaryOrderingItem>, ISpecificGrid
+{
+    private SupplierProduct[] SupplierProducts = [];
+    private SupplierLink[] Suppliers = [];
+
+    public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType));
+
+    private DynamicActionColumn[] QuantityColumns = [];
+    private DynamicActionColumn[] CostColumns = [];
+
+    private readonly Dictionary<Guid, Job> JobDetails = [];
+
+    private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
+
+    private StockSummaryOrderingType _orderType = StockSummaryOrderingType.JobOrder;
+    public StockSummaryOrderingType OrderType
+    {
+        get => _orderType;
+        set
+        {
+            if(_orderType != value)
+            {
+                _orderType = value;
+
+                CalculateQuantities();
+
+                foreach(var control in QuantityControls)
+                {
+                    control.UpdateControl(OrderType);
+                }
+            }
+        }
+    }
+
+    public IEnumerable<StockSummaryOrderingResult> Results
+    {
+        get
+        {
+            for(int i = 0; i < Suppliers.Length; ++i)
+            {
+                var supplier = Suppliers[i];
+                foreach(var item in Items)
+                {
+                    var qty = item.GetQuantity(i);
+                    var supplierProduct = GetSupplierProduct(item, supplier.ID);
+                    if (supplierProduct is null)
+                    {
+                        // If this is true, then the quantities also will have to be true.
+                        continue;
+                    }
+
+                    if(OrderType == StockSummaryOrderingType.StockOrder && qty.StockTotal > 0)
+                    {
+                        yield return new(supplier, null, item, qty.StockTotal, supplierProduct);
+                    }
+                    else
+                    {
+                        foreach(var (jobID, q) in qty.JobTotals)
+                        {
+                            if(q > 0)
+                            {
+                                yield return new(supplier, new() { ID = jobID }, item, q, supplierProduct);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    #region UI Component
+
+    private Component? _uiComponent;
+    private Component UIComponent
+    {
+        get
+        {
+            _uiComponent ??= new Component(this);
+            return _uiComponent;
+        }
+    }
+    protected override IDynamicGridUIComponent<StockSummaryOrderingItem> CreateUIComponent()
+    {
+        return UIComponent;
+    }
+    private class Component : DynamicGridGridUIComponent<StockSummaryOrderingItem>
+    {
+        private StockSummaryOrderingGrid Grid;
+
+        public Component(StockSummaryOrderingGrid grid)
+        {
+            Parent = grid;
+            Grid = grid;
+        }
+
+        protected override Brush? GetCellSelectionBackgroundBrush()
+        {
+            return null;
+        }
+
+        protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
+        {
+            var item = Grid.LoadItem(row);
+            if(column is DynamicActionColumn ac)
+            {
+                var qIdx = Grid.QuantityColumns.IndexOf(ac);
+                var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
+                if(idx != -1)
+                {
+                    var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[idx].ID);
+                    if(supplierProduct is null)
+                    {
+                        return new SolidColorBrush(Colors.Gainsboro);
+                    }
+                    //if(item.GetTotalQuantity(Grid.OrderType) < item.RequiredQuantity)
+                    //{
+                    //    return new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 };
+                    //}
+                    //else
+                    //{
+                    //    return new SolidColorBrush(Colors.LightGreen) { Opacity = 0.5 };
+                    //}
+                }
+            }
+            return base.GetCellBackground(row, column);
+        }
+    }
+
+    #endregion
+
+    private bool _observing = true;
+    private void SetObserving(bool observing)
+    {
+        _observing = observing;
+    }
+    public override void DoChanged()
+    {
+        if (_observing)
+        {
+            base.DoChanged();
+        }
+    }
+
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        options.Clear().Add(DynamicGridOption.FilterRows);
+    }
+
+    private void LoadData()
+    {
+        var supplierColumns = new Columns<SupplierProduct>(x => x.ID)
+            .Add(x => x.SupplierLink.ID)
+            .Add(x => x.Product.ID)
+            .Add(x => x.Style.ID)
+            .Add(x => x.CostPrice)
+            .AddDimensionsColumns(x => x.Dimensions)
+            .Add(x => x.SupplierLink.Code);
+
+        SupplierProducts = Client.Query(
+            new Filter<SupplierProduct>(x => x.Product.ID).InList(Items.Select(x => x.Product.ID).ToArray())
+                .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
+            supplierColumns,
+            new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
+            .ToArray<SupplierProduct>();
+
+        Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
+
+        foreach(var (itemIdx, item) in Items.WithIndex())
+        {
+            var quantities = new StockSummaryOrderingItemQuantity[Suppliers.Length];
+            for(int i = 0; i < Suppliers.Length; ++i)
+            {
+                var qty = new StockSummaryOrderingItemQuantity();
+                quantities[i] = qty;
+                qty.Changed += () =>
+                {
+                    var row = Data.Rows[itemIdx];
+                    InvalidateRow(row);
+                    DoChanged();
+                };
+            }
+
+            item.SetQuantities(quantities);
+        }
+
+        CalculateQuantities();
+    }
+
+    private void CalculateQuantities()
+    {
+        SetObserving(false);
+        foreach(var item in Items)
+        {
+            var supplierProduct = GetSupplierProduct(item);
+            for(int i = 0; i < Suppliers.Length; ++i)
+            {
+                var qty = item.GetQuantity(i);
+
+                var supplier = Suppliers[i];
+                if(supplierProduct is not null && supplier.ID == supplierProduct.SupplierLink.ID)
+                {
+                    if(OrderType == StockSummaryOrderingType.StockOrder)
+                    {
+                        qty.StockTotal = qty.JobTotal;
+                    }
+                    else
+                    {
+                        qty.JobTotals.Clear();
+                        foreach(var (id, q) in item.GetJobRequiredQuantities())
+                        {
+                            qty.JobTotals[id] = q;
+                        }
+                    }
+                }
+                else
+                {
+                    if(OrderType == StockSummaryOrderingType.StockOrder)
+                    {
+                        qty.StockTotal = 0;
+                    }
+                    else
+                    {
+                        foreach(var id in qty.JobTotals.Keys)
+                        {
+                            qty.JobTotals[id] = 0;
+                        }
+                    }
+                }
+            }
+        }
+        SetObserving(true);
+        DoChanged();
+
+        InvalidateGrid();
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        LoadData();
+
+        ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
+
+        var columns = new DynamicGridColumns();
+        columns.Add<StockSummaryOrderingItem, string>(x => x.Product.Code, 120, "Product", "", Alignment.MiddleCenter);
+        columns.Add<StockSummaryOrderingItem, string>(x => x.Style.Code, 120, "Style", "", Alignment.MiddleCenter);
+        columns.Add<StockSummaryOrderingItem, string>(x => x.Dimensions.UnitSize, 0, "Size", "", Alignment.MiddleLeft);
+        columns.Add<StockSummaryOrderingItem, double>(x => x.RequiredQuantity, 0, "Required", "", Alignment.MiddleLeft);
+
+        QuantityColumns = new DynamicActionColumn[Suppliers.Length];
+        CostColumns = new DynamicActionColumn[Suppliers.Length];
+        QuantityControls.Clear();
+
+        for(int i = 0; i < Suppliers.Length; ++i)
+        {
+            InitialiseSupplierColumn(i);
+        }
+
+        return columns;
+    }
+
+    private BitmapImage? Warning_Image(CoreRow? row)
+    {
+        if (row is null) return _warning;
+
+        var item = LoadItem(row);
+        if(item.GetTotalQuantity(OrderType) < item.RequiredQuantity)
+        {
+            return _warning;
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    protected override void ConfigureColumnGroups()
+    {
+        for(int idx = 0; idx < Suppliers.Length; ++idx)
+        {
+            GetColumnGrouping().AddGroup(Suppliers[idx].Code, QuantityColumns[idx], CostColumns[idx]);
+        }
+    }
+
+    private void LoadJobData(IEnumerable<Guid> ids)
+    {
+        var neededIDs = ids.Where(x => !JobDetails.ContainsKey(x)).ToArray();
+        if(neededIDs.Length > 0)
+        {
+            var details = Client.Query(
+                new Filter<Job>(x => x.ID).InList(neededIDs),
+                new Columns<Job>(x => x.ID)
+                    .Add(x => x.JobNumber)
+                    .Add(x => x.Name));
+            foreach(var job in details.ToObjects<Job>())
+            {
+                JobDetails[job.ID] = job;
+            }
+        }
+    }
+
+    private class QuantityControl : ContentControl
+    {
+        private readonly StockSummaryOrderingItem Item;
+        private readonly int SupplierIndex;
+        private readonly StockSummaryOrderingGrid Parent;
+
+        public QuantityControl(StockSummaryOrderingGrid parent, StockSummaryOrderingItem item, int supplierIndex, StockSummaryOrderingType mode)
+        {
+            Parent = parent;
+            Item = item;
+            SupplierIndex = supplierIndex;
+
+            UpdateControl(mode);
+        }
+
+        public void UpdateControl(StockSummaryOrderingType mode)
+        {
+            var supplierProduct = Parent.GetSupplierProduct(Item, Parent.Suppliers[SupplierIndex].ID);
+            if(supplierProduct is null)
+            {
+                Content = null;
+                return;
+            }
+
+            if(mode == StockSummaryOrderingType.StockOrder)
+            {
+                var editor = new DoubleTextBox
+                {
+                    VerticalAlignment = VerticalAlignment.Stretch,
+                    HorizontalAlignment = HorizontalAlignment.Stretch,
+                    Background = new SolidColorBrush(Colors.LightYellow),
+                    BorderThickness = new Thickness(0.0),
+                    MinValue = 0.0,
+                    Value = Item.GetQuantity(SupplierIndex).StockTotal
+                };
+                editor.ValueChanged += (o, e) =>
+                {
+                    Item.GetQuantity(SupplierIndex).StockTotal = editor.Value ?? default;
+                };
+                Content = editor;
+            }
+            else if(mode == StockSummaryOrderingType.JobOrder)
+            {
+                var grid = new Grid();
+                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
+
+                var editor = new TextBox
+                {
+                    VerticalAlignment = VerticalAlignment.Stretch,
+                    HorizontalAlignment = HorizontalAlignment.Stretch,
+                    VerticalContentAlignment = VerticalAlignment.Center,
+                    HorizontalContentAlignment = HorizontalAlignment.Center,
+                    Background = new SolidColorBrush(Colors.White),
+                    BorderThickness = new Thickness(0.0),
+                    IsReadOnly = true,
+                    Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal)
+                };
+                Grid.SetColumn(editor, 0);
+                grid.Children.Add(editor);
+
+                var btn = new Button
+                {
+                    VerticalAlignment = VerticalAlignment.Stretch,
+                    VerticalContentAlignment = VerticalAlignment.Center,
+                    HorizontalAlignment = HorizontalAlignment.Stretch,
+                    Content = "..",
+                    Margin = new Thickness(1),
+                    Focusable = false
+                };
+                btn.SetValue(Grid.ColumnProperty, 1);
+                btn.SetValue(Grid.RowProperty, 0);
+                btn.Click += (o, e) =>
+                {
+                    var qty = Item.GetQuantity(SupplierIndex);
+
+                    Parent.LoadJobData(qty.JobTotals.Keys);
+
+                    var items = qty.JobTotals.Select(x =>
+                    {
+                        var item = new StockSummaryOrderingJobItem
+                        {
+                            JobID = x.Key,
+                            RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key),
+                            Quantity = x.Value
+                        };
+                        if(item.JobID == Guid.Empty)
+                        {
+                            item.Job = "General Stock";
+                        }
+                        else if(Parent.JobDetails.TryGetValue(item.JobID, out var job))
+                        {
+                            item.Job = $"{job.JobNumber}: {job.Name}";
+                        }
+                        return item;
+                    }).ToList();
+
+                    var window = new StockSummaryOrderJobScreen();
+                    window.Items = items;
+                    if(window.ShowDialog() == true)
+                    {
+                        foreach(var item in items)
+                        {
+                            qty.JobTotals[item.JobID] = item.Quantity;
+                        }
+                        qty.DoChanged();
+                        editor.Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal);
+                    }
+                };
+                grid.Children.Add(btn);
+
+                Content = grid;
+            }
+        }
+    }
+
+    private List<QuantityControl> QuantityControls = [];
+
+    private void InitialiseSupplierColumn(int idx)
+    {
+        var supplierProducts = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID).ToArray();
+
+        // Making local copy of index so that the lambda can use it, and not the changed value of 'i'.
+        var qtyColumn = new Tuple<DynamicActionColumn, QuantityControl?>(null!, null);
+        QuantityColumns[idx] = new DynamicTemplateColumn(row =>
+        {
+            var instance = LoadItem(row);
+            var control = new QuantityControl(this, instance, idx, OrderType);
+            QuantityControls.Add(control);
+            return control;
+        })
+        {
+            HeaderText = "Qty.",
+            Width = 80
+        };
+        CostColumns[idx] = new DynamicTextColumn(row =>
+        {
+            if(row is null)
+            {
+                return "Cost";
+            }
+
+            var instance = LoadItem(row);
+            var qty = OrderType == StockSummaryOrderingType.StockOrder
+                ? instance.GetQuantity(idx).StockTotal
+                : instance.GetQuantity(idx).JobTotal;
+            var supplierProduct = GetSupplierProduct(instance, Suppliers[idx].ID);
+            if(supplierProduct is not null)
+            {
+                return $"{qty * supplierProduct.CostPrice:C2}";
+            }
+            else
+            {
+                return "";
+            }
+        })
+        {
+            HeaderText = "Cost",
+            Width = 80,
+            GetSummary = () =>
+            {
+                var i = idx * 2 + 1;
+                var summary = new GridSummaryColumn
+                {
+                    Format = "{Sum:C2}",
+                    SummaryType = Syncfusion.Data.SummaryType.Custom,
+                    CustomAggregate = new CostAggregate(idx, this)
+                };
+                return summary;
+            }
+        };
+        ActionColumns.Add(QuantityColumns[idx]);
+        ActionColumns.Add(CostColumns[idx]);
+    }
+
+    private static bool Matches(StockSummaryOrderingItem item, SupplierProduct supplierProduct)
+    {
+        return item.Product.ID == supplierProduct.Product.ID
+            && item.Style.ID == supplierProduct.Style.ID
+            && item.Dimensions.Equals(supplierProduct.Dimensions);
+    }
+    private static bool Matches(ProductInstance instance, SupplierProduct supplierProduct)
+    {
+        return instance.Product.ID == supplierProduct.Product.ID
+            && instance.Style.ID == supplierProduct.Style.ID
+            && instance.Dimensions.Equals(supplierProduct.Dimensions);
+    }
+
+    private SupplierProduct? GetSupplierProduct(StockSummaryOrderingItem item)
+    {
+        var defaultSupplierProduct = SupplierProducts.FirstOrDefault(x => x.ID == item.Product.Supplier.ID);
+        if(defaultSupplierProduct is not null && Matches(item, defaultSupplierProduct))
+        {
+            return defaultSupplierProduct;
+        }
+        else
+        {
+            return SupplierProducts.FirstOrDefault(x => Matches(item, x));
+        }
+    }
+    private SupplierProduct? GetSupplierProduct(ProductInstance instance, Guid supplierID)
+    {
+        return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(instance, x));
+    }
+    private SupplierProduct? GetSupplierProduct(StockSummaryOrderingItem item, Guid supplierID)
+    {
+        return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x));
+    }
+    //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 StockSummaryOrderingGrid Grid;
+
+        public CostAggregate(int supplierIndex, StockSummaryOrderingGrid 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)
+            {
+                Sum = 0;
+                foreach (var dataRow in rows)
+                {
+                    var rowIdx = dataRow.Row.Table.Rows.IndexOf(dataRow.Row);
+                    var item = Grid.LoadItem(Grid.Data.Rows[rowIdx]);
+                    var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[SupplierIndex].ID);
+                    if(supplierProduct is not null)
+                    {
+                        var qty = item.GetQuantity(SupplierIndex);
+                        Sum += qty.GetTotal(Grid.OrderType) * supplierProduct.CostPrice;
+                    }
+                }
+            }
+            else
+            {
+                Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'.");
+            }
+        }
+    }
+}

+ 85 - 0
prs.desktop/Panels/StockSummary/OrderScreen/StockSummaryOrderingJobGrid.cs

@@ -0,0 +1,85 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.DynamicGrid;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Media;
+
+namespace PRSDesktop.Panels.StockSummary.OrderScreen;
+
+public class StockSummaryOrderingJobItem : BaseObject
+{
+    [EditorSequence(1)]
+    public string Job { get; set; }
+
+    [NullEditor]
+    public Guid JobID { get; set; }
+
+    [EditorSequence(2)]
+    [DoubleEditor]
+    public double RequiredQuantity { get; set; }
+
+    [EditorSequence(2)]
+    [DoubleEditor]
+    public double Quantity { get; set; }
+}
+
+
+public class StockSummaryOrderingJobGrid : DynamicItemsListGrid<StockSummaryOrderingJobItem>
+{
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        base.DoReconfigure(options);
+
+        options.Clear().AddRange(DynamicGridOption.DirectEdit);
+    }
+
+    private class UIComponent : DynamicGridGridUIComponent<StockSummaryOrderingJobItem>
+    {
+        public StockSummaryOrderingJobGrid Grid { get; set; }
+
+        public UIComponent(StockSummaryOrderingJobGrid grid)
+        {
+            Grid = grid;
+            Parent = grid;
+        }
+
+        protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
+        {
+            return base.GetCellBackground(row, column);
+        }
+    }
+    protected override IDynamicGridUIComponent<StockSummaryOrderingJobItem> CreateUIComponent()
+    {
+        return new UIComponent(this);
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        var columns = new DynamicGridColumns();
+        columns.Add<StockSummaryOrderingJobItem, string>(x => x.Job, 0, "Job", "", Alignment.MiddleLeft);
+        columns.Add<StockSummaryOrderingJobItem, double>(x => x.RequiredQuantity, 70, "Req. Qty.", "F2", Alignment.MiddleCenter);
+        columns.Add<StockSummaryOrderingJobItem, double>(x => x.Quantity, 70, "Qty.", "F2", Alignment.MiddleCenter);
+        return columns;
+    }
+
+    public override void ConfigureColumns(DynamicGridColumns columns)
+    {
+        base.ConfigureColumns(columns);
+
+        foreach(var column in columns)
+        {
+            if(new Column<StockSummaryOrderingJobItem>(x => x.Quantity).IsEqualTo(column.ColumnName))
+            {
+                column.Editor.Editable = Editable.Enabled;
+            }
+            else
+            {
+                column.Editor.Editable = Editable.Disabled;
+            }
+        }
+    }
+}

+ 297 - 122
prs.desktop/Panels/StockSummary/StockSummaryGrid.cs

@@ -7,11 +7,15 @@ using System.Windows.Controls;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using Comal.Classes;
+using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.Wpf;
 using InABox.WPF;
 using Newtonsoft.Json;
+using PRSDesktop.Panels.StockSummary.OrderScreen;
+using Syncfusion.UI.Xaml.Diagram.Controls;
+using Syncfusion.Windows.PdfViewer;
 
 namespace PRSDesktop;
 
@@ -37,12 +41,15 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
     private CoreTable? _supplierProducts = null;
     
     private static readonly BitmapImage _warning = InABox.Wpf.Resources.warning.AsBitmapImage();
+    private static readonly BitmapImage _tick = InABox.Wpf.Resources.tick.AsBitmapImage();
+    private static readonly BitmapImage _cart = PRSDesktop.Resources.purchase.AsBitmapImage();
 
     public Guid[] GroupIDs { get; set; } = [];
     public Guid[] JobIDs { get; set; } = [];
     public HashSet<Guid> SupplierIDs { get; set; } = [];
 
-    private readonly Button? OrderButton;
+    private readonly Button OrderButton;
+    private HashSet<Guid> SelectedForOrder = [];
     
     public StockSummaryGrid() : base()
     {
@@ -56,6 +63,7 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         HiddenColumns.Add(x => x.Dimensions.UnitSize);
         HiddenColumns.Add(x => x.Product.Image.ID);
         HiddenColumns.Add(x => x.Product.Image.FileName);
+        HiddenColumns.Add(x=>x.Product.Supplier.ID);
         HiddenColumns.Add(x=>x.Product.Supplier.SupplierLink.ID);
         HiddenColumns.Add(x=>x.MinimumStockLevel);
         
@@ -76,30 +84,14 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         CreateColumn(GetReservedStock, ColumnTag.JobStockHoldings, "Job Hld.","F2");
         CreateColumn(GetReservedPurchaseOrder, ColumnTag.JobPurchaseOrders, "Job PO.","F2");
         CreateColumn(GetBalanceRequired, ColumnTag.BalanceRequired,"Required","");
-        
-        OrderButton = AddButton("Order Stock", PRSDesktop.Resources.purchase.ToBitmapImage(), OrderStock_Click);
-        OrderButton.IsEnabled = false;
-    }
 
-    private void CreateColumn(DynamicTextColumn.GetTextDelegate calculate, ColumnTag tag, string header, string format)
-    {
-        var column = new DynamicTextColumn(calculate)
+        ActionColumns.Add(new DynamicImageColumn(SelectForOrder_Image, SelectForOrder_Click)
         {
-            Width = 60, 
-            Format=format, 
-            Position = DynamicActionColumnPosition.End, 
-            Tag = tag,
-            HeaderText = header
-        };
-        ActionColumns.Add(column);
-    }
-
-
-    protected override void SelectItems(CoreRow[]? rows)
-    {
-        base.SelectItems(rows);
-
-        OrderButton.IsEnabled = rows is not null && rows.Length > 0;
+            Position = DynamicActionColumnPosition.End
+        });
+        
+        OrderButton = AddButton("Order Stock", _cart, OrderStock_Click);
+        OrderButton.IsEnabled = false;
     }
     
     private BitmapImage? Issues_Image(CoreRow? row)
@@ -118,6 +110,8 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
             : column.TextToolTip(row.Get<ProductInstance, string>(x => x.Product.Issues));
     }
 
+    #region UIComponent
+
     private UIComponent? _uicomponent = null;
     private class UIComponent : DynamicGridGridUIComponent<ProductInstance>
     {
@@ -209,7 +203,9 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
     {
         return _uicomponent ??= new UIComponent(this);
     }
-    
+
+    #endregion
+
     protected override void DoReconfigure(FluentList<DynamicGridOption> options)
     {
         base.DoReconfigure(options);
@@ -235,6 +231,70 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         return columns;
     }
 
+    #region Column Data and Details
+
+    private void CreateColumn(DynamicTextColumn.GetTextDelegate calculate, ColumnTag tag, string header, string format)
+    {
+        var column = new DynamicTextColumn(calculate)
+        {
+            Width = 60, 
+            Format=format, 
+            Position = DynamicActionColumnPosition.End, 
+            Tag = tag,
+            HeaderText = header
+        };
+        ActionColumns.Add(column);
+    }
+
+    private object GetMinimumStockLevel(CoreRow? row) => row?.Get<ProductInstance, double>(x => x.MinimumStockLevel) ?? 0.0F;
+    
+    private object GetGeneralStockLevel(CoreRow? row)
+    {
+        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
+            return info.GenStock;
+        return 0.0F;
+    }
+
+    private object GetGeneralPurchaseOrder(CoreRow? row)
+    {
+        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
+            return info.GenPO;
+        return 0.0F;
+    }
+    
+    private object GetBOMBalance(CoreRow? row)
+    {
+        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
+            return info.JobBOM;
+        return 0.0F;
+    }
+
+    private object GetReservedStock(CoreRow? row)
+    {
+        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
+            return info.JobStock;
+        return 0.0F;
+    }
+
+    private object GetReservedPurchaseOrder(CoreRow? row)
+    {
+        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
+            return info.JobPO;
+        return 0.0F;
+    }
+
+    private object GetBalanceRequired(CoreRow? row)
+    {
+        if (row != null &&
+            _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
+            return Optimise
+                ? info.Optimised.IsEffectivelyEqual(0.0F) ? "" : $"{info.Optimised:F2}"
+                : info.Required.IsEffectivelyEqual(0.0F)
+                    ? ""
+                    : $"{info.Required:F2}";
+        return "";
+    }
+
     private void ShowDetailGrid<TEntity>(
         String tag, 
         Expression<Func<TEntity,object?>> productcol, 
@@ -368,6 +428,10 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         
     }
 
+    #endregion
+
+    #region Refresh
+
     private bool HasStyle()
     {
         return DataColumns().ColumnNames().Any(x => x.StartsWith("Style.") && !x.Equals("Style.ID"));
@@ -436,6 +500,17 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         }
         return total;
     }
+
+    private class StockSummaryJobInfo
+    {
+        public double BOM { get; set; }
+
+        public double Stock { get; set; }
+
+        public double PO { get; set; }
+
+        public double Required => Math.Max(BOM - (Stock + PO), 0.0F);
+    }
     
     private class StockSummaryInfo
     {
@@ -445,10 +520,29 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         public double JobBOM { get; set; }
         public double JobStock { get; set; }
         public double JobPO { get; set; }
+
+        public Dictionary<Guid, StockSummaryJobInfo> JobInfo { get; private init; } = [];
+
+        public void AddJobBOM(Guid jobID, double quantity)
+        {
+            var item = JobInfo.GetValueOrAdd(jobID);
+            item.BOM += quantity;
+        }
+        public void AddJobPO(Guid jobID, double quantity)
+        {
+            var item = JobInfo.GetValueOrAdd(jobID);
+            item.PO += quantity;
+        }
+        public void AddJobStock(Guid jobID, double quantity)
+        {
+            var item = JobInfo.GetValueOrAdd(jobID);
+            item.Stock += quantity;
+        }
+        public double StockRequired => Math.Max(MinStock - (GenStock + GenPO), 0.0F);
         
         public double Required => Math.Max((MinStock + JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
         
-        public double Optimised => Math.Max(Math.Max(MinStock,JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
+        public double Optimised => Math.Max(Math.Max(MinStock, JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
     }
 
     private Dictionary<Guid, StockSummaryInfo> _summaryinfo = new Dictionary<Guid, StockSummaryInfo>();
@@ -572,25 +666,62 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
                 var info = new StockSummaryInfo();
                 info.MinStock = row.Get<ProductInstance, double>(x => x.MinimumStockLevel);
                 
-                var genstockrows = GetRows<StockHolding>(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, new Guid[] { Guid.Empty });
+                var genstockrows = GetRows(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, [Guid.Empty]);
                 info.GenStock = Aggregate(genstockrows, _stockHoldingCols, true, true, x=>x.Units);
                 
-                var genporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, new Guid[] { Guid.Empty });
+                var genporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, [Guid.Empty]);
                 info.GenPO = Aggregate(genporows, _poItemCols, true, true, x=>x.Qty);
-                
-                var bomrows = GetRows(_jobBOMs.Rows, _jobBOMColumns, _productid, _styleid, _unitsize, JobIDs);
-                var bom = Aggregate(bomrows, _jobBOMColumns, true, true, x=>x.Quantity);
+
+                // Job BOMs
+                {
+                    var bomrows = GetRows(_jobBOMs.Rows, _jobBOMColumns, _productid, _styleid, _unitsize, JobIDs);
+                    var bom = Aggregate(bomrows, _jobBOMColumns, true, true, x => x.Quantity);
         
-                var mvmtrows = GetRows(_stockMovements.Rows, _stockMovementCols, _productid, _styleid, _unitsize, JobIDs);
-                var mvmts = Aggregate(mvmtrows, _jobBOMColumns, true, true, x=>x.Quantity);
+                    var mvmtrows = GetRows(_stockMovements.Rows, _stockMovementCols, _productid, _styleid, _unitsize, JobIDs);
+                    var mvmts = Aggregate(mvmtrows, _stockMovementCols, true, true, x => x.Units);
         
-                info.JobBOM =  bom - mvmts;
-                
-                var jobstockrows = GetRows<StockHolding>(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, JobIDs);
-                info.JobStock = Aggregate(jobstockrows, _stockHoldingCols, true, true, x=>x.Units);
-                
-                var jobporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, JobIDs);
-                info.JobPO = Aggregate(jobporows, _poItemCols, true, true, x=>x.Qty);
+                    info.JobBOM =  bom - mvmts;
+
+                    var bomJobCol = _jobBOMColumns.IndexOf(x => x.Job.ID);
+                    var bomQtyCol = _jobBOMColumns.IndexOf(x => x.Quantity);
+                    foreach(var jobBOMRow in bomrows)
+                    {
+                        info.AddJobBOM(jobBOMRow.Get<Guid>(bomJobCol), jobBOMRow.Get<double>(bomQtyCol));
+                    }
+
+                    var mvtJobCol = _stockMovementCols.IndexOf(x => x.Job.ID);
+                    var mvtQtyCol = _stockMovementCols.IndexOf(x => x.Qty);
+                    foreach(var mvtRow in mvmtrows)
+                    {
+                        info.AddJobBOM(mvtRow.Get<Guid>(mvtJobCol), -mvtRow.Get<double>(mvtQtyCol));
+                    }
+                }
+
+                // Job Stock
+                {
+                    var jobstockrows = GetRows(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, JobIDs);
+                    info.JobStock = Aggregate(jobstockrows, _stockHoldingCols, true, true, x=>x.Units);
+
+                    var jobCol = _stockHoldingCols.IndexOf(x => x.Job.ID);
+                    var qtyCol = _stockHoldingCols.IndexOf(x => x.Units);
+                    foreach(var jobStockRow in jobstockrows)
+                    {
+                        info.AddJobStock(jobStockRow.Get<Guid>(jobCol), jobStockRow.Get<double>(qtyCol));
+                    }
+                }
+
+                // Job PO
+                {
+                    var jobporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, JobIDs);
+                    info.JobPO = Aggregate(jobporows, _poItemCols, true, true, x => x.Qty);
+
+                    var jobCol = _poItemCols.IndexOf(x => x.Job.ID);
+                    var qtyCol = _poItemCols.IndexOf(x => x.Qty);
+                    foreach(var jobPORow in jobporows)
+                    {
+                        info.AddJobPO(jobPORow.Get<Guid>(jobCol), jobPORow.Get<double>(qtyCol));
+                    }
+                }
 
                 _summaryinfo[_id] = info;
 
@@ -621,53 +752,51 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
         return result;
     }
 
-    private object GetMinimumStockLevel(CoreRow? row) => row?.Get<ProductInstance, double>(x => x.MinimumStockLevel) ?? 0.0F;
-    
-    private object GetGeneralStockLevel(CoreRow? row)
-    {
-        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
-            return info.GenStock;
-        return 0.0F;
-    }
-
-    private object GetGeneralPurchaseOrder(CoreRow? row)
-    {
-        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
-            return info.GenPO;
-        return 0.0F;
-    }
-    
-    private object GetBOMBalance(CoreRow? row)
-    {
-        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
-            return info.JobBOM;
-        return 0.0F;
-    }
+    #endregion
 
-    private object GetReservedStock(CoreRow? row)
-    {
-        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
-            return info.JobStock;
-        return 0.0F;
-    }
+    #region Ordering
 
-    private object GetReservedPurchaseOrder(CoreRow? row)
+    private bool SelectForOrder_Click(CoreRow? row)
     {
-        if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
-            return info.JobPO;
-        return 0.0F;
+        if (row is null)
+        {
+            var menu = new ContextMenu();
+            menu.AddItem("Deselect all", null, () =>
+            {
+                SelectedForOrder.Clear();
+                InvalidateGrid();
+                OrderButton.IsEnabled = false;
+            });
+            menu.IsOpen = true;
+            return false;
+        }
+        else
+        {
+            var id = row.Get<ProductInstance, Guid>(x => x.ID);
+            if (!SelectedForOrder.Remove(id))
+            {
+                SelectedForOrder.Add(id);
+            }
+            OrderButton.IsEnabled = SelectedForOrder.Count > 0;
+            InvalidateRow(row);
+            return false;
+        }
     }
 
-    private object GetBalanceRequired(CoreRow? row)
+    private BitmapImage? SelectForOrder_Image(CoreRow? row)
     {
-        if (row != null &&
-            _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
-            return Optimise
-                ? info.Optimised.IsEffectivelyEqual(0.0F) ? "" : $"{info.Optimised:F2}"
-                : info.Required.IsEffectivelyEqual(0.0F)
-                    ? ""
-                    : $"{info.Required:F2}";
-        return "";
+        if(row is null)
+        {
+            return _cart;
+        }
+        else if(SelectedForOrder.Contains(row.Get<ProductInstance, Guid>(x => x.ID)))
+        {
+            return _tick;
+        }
+        else
+        {
+            return null;
+        }
     }
     
     private bool OrderStock_Click(Button button, CoreRow[] rows)
@@ -677,61 +806,107 @@ public class StockSummaryGrid : DynamicDataGrid<ProductInstance>, IDataModelSour
             return false;
         }
 
-        var _instances = rows.ToObjects<ProductInstance>().ToArray();
+        rows = Data.Rows.Where(x => SelectedForOrder.Contains(x.Get<ProductInstance, Guid>(x => x.ID))).ToArray();
 
-        var order = new PurchaseOrder();
-        order.Description = "Purchase Order created from Stock Forecast Screen";
-        order.RaisedBy.ID = App.EmployeeID;
-        order.IssuedBy.ID = App.EmployeeID;
-        order.IssuedDate = DateTime.Today;
+        var items = new List<StockSummaryOrderingItem>();
+        foreach(var instance in rows.ToObjects<ProductInstance>())
+        {
+            var info = _summaryinfo.GetValueOrDefault(instance.ID);
+
+            var item = new StockSummaryOrderingItem();
+            item.Product.CopyFrom(instance.Product);
+            item.Style.CopyFrom(instance.Style);
+            item.Dimensions.CopyFrom(instance.Dimensions);
+            item.RequiredQuantity = (Optimise ? info?.Optimised : info?.Required) ?? default;
 
-        var orderItems = new List<PurchaseOrderItem>();
+            if(info is not null)
+            {
+                item.SetJobRequiredQuantity(Guid.Empty, info.StockRequired);
+                foreach(var (id, jobInfo) in info.JobInfo)
+                {
+                    item.SetJobRequiredQuantity(id, jobInfo.Required);
+                }
+            }
+            else
+            {
+                item.SetJobRequiredQuantity(Guid.Empty, 0.0);
+            }
+
+            items.Add(item);
+        }
 
-        foreach(var stockSummary in _instances)
+        var window = new StockSummaryOrderScreen(items);
+        if(window.ShowDialog() != true)
         {
-            var orderItem = new PurchaseOrderItem();
-            orderItem.Product.ID = stockSummary.Product.ID;
-            orderItem.Style.ID = stockSummary.Style.ID;
-            
-            // Need to Breakout BOMs into individual Lines
-            //orderItem.Job.ID = stockSummary.Job.ID;
-            
-            orderItem.Dimensions.CopyFrom(stockSummary.Dimensions);
- 
-            // Need to calculate Balance based on Supplier.OrderQty
-            //orderItem.Qty = Math.Max(-stockSummary.BalanceAvailable, 0);
-            
-            orderItems.Add(orderItem);
+            return false;
         }
-        LookupFactory.DoLookups<PurchaseOrderItem, Product, ProductLink>(
-            orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Product.ID)),
-            x => x.Product);
-        LookupFactory.DoLookups<PurchaseOrderItem, ProductStyle, ProductStyleLink>(
-            orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Style.ID)),
-            x => x.Style);
-        LookupFactory.DoLookups<PurchaseOrderItem, Job, JobLink>(
-            orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Job.ID)),
-            x => x.Job);
-
-        if (DynamicGridUtils.EditEntity(order, t =>
+
+        var orders = new List<Tuple<PurchaseOrder, List<PurchaseOrderItem>>>();
+        foreach(var perSupplier in window.Results.GroupBy(x => x.Supplier.ID))
         {
-            if (t == typeof(PurchaseOrderItem))
+            var order = new PurchaseOrder();
+            order.Description = "Purchase Order created from Stock Forecast Screen";
+            order.RaisedBy.ID = App.EmployeeID;
+
+            LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, perSupplier.Key);
+
+            var orderItems = new List<PurchaseOrderItem>();
+            var results = perSupplier.ToArray();
+            foreach(var item in results)
             {
-                var table = new CoreTable();
-                table.LoadColumns(typeof(PurchaseOrderItem));
-                table.LoadRows(orderItems);
-                return table;
+                var orderItem = new PurchaseOrderItem();
+                orderItem.Product.ID = item.Item.Product.ID;
+                orderItem.Style.ID = item.Item.Style.ID;
+                orderItem.Job.ID = item.Job?.ID ?? Guid.Empty;
+                
+                orderItems.Add(orderItem);
             }
-            return null;
-        }, preloadPages: true))
+
+            LookupFactory.DoLookups<PurchaseOrderItem, Product, ProductLink>(
+                orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Product.ID)),
+                x => x.Product);
+            LookupFactory.DoLookups<PurchaseOrderItem, ProductStyle, ProductStyleLink>(
+                orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Style.ID)),
+                x => x.Style);
+            LookupFactory.DoLookups<PurchaseOrderItem, Job, JobLink>(
+                orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Job.ID)),
+                x => x.Job);
+            LookupFactory.DoLookups<PurchaseOrderItem, TaxCode, TaxCodeLink>(
+                orderItems.WithIndex().Select(x => new Tuple<PurchaseOrderItem, Guid>(x.Value, results[x.Key].SupplierProduct.TaxCode.ID)),
+                x => x.TaxCode);
+
+            foreach(var (i, item) in results.WithIndex())
+            {
+                var orderItem = orderItems[i];
+                orderItem.Dimensions.CopyFrom(item.Item.Dimensions);
+
+                orderItem.Qty = item.Quantity;
+                orderItem.Cost = item.SupplierProduct.CostPrice;
+            }
+
+            orders.Add(new(order, orderItems));
+        }
+
+        Client.Save(orders.Select(x => x.Item1), "Created from Stock Forecast screen");
+        foreach(var (order, orderItems) in orders)
         {
-            MessageWindow.ShowMessage("Purchase order created.", "Success.");
+            foreach(var item in orderItems)
+            {
+                item.PurchaseOrderLink.ID = order.ID;
+            }
         }
-        return false;
+        Client.Save(orders.SelectMany(x => x.Item2), "Created from Stock Forecast screen");
+
+        SelectedForOrder.Clear();
+        OrderButton.IsEnabled = false;
+
+        return true;
     }
 
+    #endregion
+
     #region IDataModelSource
-    
+
     public event DataModelUpdateEvent? OnUpdateDataModel;
 
     public string SectionName => "Stock Summary";