Explorar el Código

Added PurchaseOrderItems to Allocate Stock screen

Kenric Nugteren hace 1 semana
padre
commit
22df193e19

+ 45 - 3
prs.desktop/Panels/Reservation Management/Holdings/ReservationManagementHoldingsGrid.xaml

@@ -68,7 +68,7 @@
                     Padding="2">
                     <Button 
                         x:Name="currentStyle" 
-                        Tag="{Binding StockOfCurrentStyle}"
+                        Tag="{Binding BaseStockOfCurrentStyle}"
                         Click="Take_Click"
                         Visibility="{Binding Visibility}">
                         <Button.Style>
@@ -116,7 +116,7 @@
                     Padding="2">
                     <Button 
                         x:Name="noStyle" 
-                        Tag="{Binding StockOfNoStyle}" 
+                        Tag="{Binding BaseStockOfNoStyle}" 
                         Click="Take_Click"
                         Visibility="{Binding Visibility}">
                         <Button.Style>
@@ -166,7 +166,7 @@
                     <Button 
                         Grid.Column="4" 
                         x:Name="otherStyle" 
-                        Tag="{Binding StockOfOtherStyles}"
+                        Tag="{Binding BaseStockOfOtherStyles}"
                         Click="Take_Click"
                         Visibility="{Binding Visibility}">
                         <Button.Style>
@@ -261,6 +261,7 @@
                         <RowDefinition Height="auto"/>
                         <RowDefinition Height="auto"/>
                         <RowDefinition Height="auto"/>
+                        <RowDefinition Height="auto"/>
                     </Grid.RowDefinitions>
 
                     <Grid Grid.Row="0">
@@ -492,6 +493,47 @@
                         
                     </Grid>
                     
+                    <Grid
+                        Grid.Row="4" 
+                        x:Name="purchasedTab" 
+                        Background="Yellow"
+                        MinHeight="80" 
+                        Visibility="Collapsed">
+                        
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="25"/>
+                            <ColumnDefinition Width="*"/>
+                        </Grid.ColumnDefinitions>
+                        
+                        <Border 
+                            Grid.Column="0"
+                            BorderThickness="0,0,0.75,0.75"
+                            BorderBrush="Gray"
+                            Background="Transparent">
+                            <TextBlock 
+                                Text="Purchased" 
+                                HorizontalAlignment="Center" 
+                                VerticalAlignment="Center">
+                                <TextBlock.LayoutTransform>
+                                    <RotateTransform Angle="270"/>
+                                </TextBlock.LayoutTransform>
+                            </TextBlock>
+                        </Border>
+                        
+                        <ItemsControl 
+                            Grid.Column="1" 
+                            x:Name="listViewPurchased" 
+                            VerticalAlignment="Top"
+                            ItemTemplate="{StaticResource takePanel}">
+                            <ItemsControl.ItemsPanel>
+                                <ItemsPanelTemplate>
+                                    <VirtualizingStackPanel Orientation="Vertical"/>
+                                </ItemsPanelTemplate>
+                            </ItemsControl.ItemsPanel>
+                        </ItemsControl>
+                        
+                    </Grid>
+                    
                 </Grid>
 
             </ScrollViewer>

+ 149 - 44
prs.desktop/Panels/Reservation Management/Holdings/ReservationManagementHoldingsGrid.xaml.cs

@@ -1,11 +1,9 @@
-using com.sun.org.apache.xml.@internal.dtm.@ref;
-using Comal.Classes;
+using Comal.Classes;
 using InABox.Clients;
 using InABox.Core;
 using InABox.Wpf;
 using InABox.WPF;
-using java.sql;
-using org.omg.PortableInterceptor;
+using PRSDimensionUtils;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
@@ -86,34 +84,40 @@ public partial class ReservationManagementHoldingsGrid
         {
             requisitionedTab.Visibility = redList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
         };
+        purchasedList.CollectionChanged += (s, e) =>
+        {
+            purchasedTab.Visibility = purchasedList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+        };
     }
 
-    private readonly ObservableCollection<ReservationManagementHoldingsModel> greenList = new();
-    private readonly ObservableCollection<ReservationManagementHoldingsModel> yellowList = new();
-    private readonly ObservableCollection<ReservationManagementHoldingsModel> redList = new();
+    private readonly ObservableCollection<ReservationManagementHoldingsModel<StockMovement>> greenList = new();
+    private readonly ObservableCollection<ReservationManagementHoldingsModel<StockMovement>> yellowList = new();
+    private readonly ObservableCollection<ReservationManagementHoldingsModel<StockMovement>> redList = new();
+    private readonly ObservableCollection<ReservationManagementHoldingsModel<PurchaseOrderItem>> purchasedList = new();
 
-    private void DistributeMovements(Dictionary<Guid, List<StockMovement>>? movements, Guid selectedStyleID, ReservationManagementHoldingsModel model)
+    private void DistributeItems<T>(Dictionary<Guid, List<T>>? items, Guid selectedStyleID, ReservationManagementHoldingsModel<T> model,
+        Func<T, double> units)
     {
-        if (movements is null) return;
+        if (items is null) return;
 
-        foreach(var (id, mvts) in movements)
+        foreach(var (id, styleItems) in items)
         {
             if(id == Guid.Empty || id == CompanyDefaultStyle.ID)
             {
-                model.StockOfNoStyle.AddRange(mvts);
+                model.StockOfNoStyle.AddRange(styleItems);
             }
             else if (id == selectedStyleID)
             {
-                model.StockOfCurrentStyle.AddRange(mvts);
+                model.StockOfCurrentStyle.AddRange(styleItems);
             }
             else
             {
-                model.StockOfOtherStyles.AddRange(mvts);
+                model.StockOfOtherStyles.AddRange(styleItems);
             }
         }
-        model.UnitsOfCurrentStyle = Math.Round(model.StockOfCurrentStyle.Sum(x => x.Units));
-        model.UnitsOfNoStyle = Math.Round(model.StockOfNoStyle.Sum(x => x.Units));
-        model.UnitsOfOtherStyles = Math.Round(model.StockOfOtherStyles.Sum(x => x.Units));
+        model.UnitsOfCurrentStyle = Math.Round(model.StockOfCurrentStyle.Sum(units));
+        model.UnitsOfNoStyle = Math.Round(model.StockOfNoStyle.Sum(units));
+        model.UnitsOfOtherStyles = Math.Round(model.StockOfOtherStyles.Sum(units));
     }
 
     private void CalculateHoldings()
@@ -138,18 +142,67 @@ public partial class ReservationManagementHoldingsGrid
                     x => x.JobRequisitionItem.ID,
                     x => x.Cost)
                     .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data))
-                .ToArray<StockMovement>();
+                .ToObjects<StockMovement>();
             var stockMovements = new Dictionary<(Guid job, bool allocated), Dictionary<Guid, List<StockMovement>>>();
             foreach(var mvt in results)
             {
-                var key = (mvt.Job.ID, mvt.JobRequisitionItem.ID != Guid.Empty);
-                var lineDict = stockMovements.GetValueOrAdd(key);
-                var mvts = lineDict.GetValueOrAdd(mvt.Style.ID);
-                mvts.Add(mvt);
+                stockMovements.GetValueOrAdd((mvt.Job.ID, mvt.JobRequisitionItem.ID != Guid.Empty))
+                    .GetValueOrAdd(mvt.Style.ID)
+                    .Add(mvt);
+            }
+
+            Filter<PurchaseOrderItem> dimensionsFilter;
+            double jriDimensionsUnitValue;
+            if (item.Dimensions.Unit.Conversion.IsNullOrWhiteSpace())
+            {
+                dimensionsFilter = Filter<PurchaseOrderItem>.Where(x => x.Dimensions).DimensionEquals(item.Dimensions);
+                jriDimensionsUnitValue = 1;
+            }
+            else
+            {
+                dimensionsFilter = Filter<PurchaseOrderItem>.Where(x => x.Dimensions.Unit.ID).IsEqualTo(item.Dimensions.Unit.ID);
+                (jriDimensionsUnitValue, _) = DimensionUtils.ConvertDimensions(item.Dimensions.Copy(), 1, 0, Client<ProductDimensionUnit>.Provider);
+            }
+
+            var orderItemsResult = Client.Query(
+                Filter<PurchaseOrderItem>.Where(x => x.Product.ID).IsEqualTo(item.Product.ID)
+                    .And(dimensionsFilter)
+                    .And(x => x.ReceivedDate).IsEqualTo(DateTime.MinValue),
+                Columns.None<PurchaseOrderItem>()
+                    .Add(x => x.ID)
+                    .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.All)
+                    .Add(x => x.Dimensions.Unit.Conversion)
+                    .Add(x => x.Style.ID)
+                    .Add(x => x.Style.Description)
+                    .Add(x => x.Style.Code)
+                    .Add(x => x.Unallocated)
+                    .Add(x => x.PurchaseOrderLink.ID)
+                    .Add(x => x.PurchaseOrderLink.PONumber)
+                    .Add(x => x.Job.ID))
+                .ToObjects<PurchaseOrderItem>();
+            var purchaseOrderItems = new Dictionary<Guid, Dictionary<Guid, List<PurchaseOrderItem>>>();
+            var orderItemQuantities = new Dictionary<Guid, double>();
+            foreach (var orderItem in orderItemsResult)
+            {
+                if (!item.Dimensions.Unit.Conversion.IsNullOrWhiteSpace())
+                {
+                    // We need the unallocated quantity to be in terms of the dimensions of the JRI.
+                    var (qty, cost) = DimensionUtils.ConvertDimensions(orderItem.Dimensions, orderItem.Unallocated, orderItem.Cost, Client<ProductDimensionUnit>.Provider);
+                    orderItemQuantities[orderItem.ID] = qty / jriDimensionsUnitValue;
+                    orderItem.Unallocated = qty;
+                }
+                else
+                {
+                    orderItemQuantities[orderItem.ID] = orderItem.Unallocated;
+                }
+                purchaseOrderItems.GetValueOrAdd(orderItem.Job.ID)
+                    .GetValueOrAdd(orderItem.Style.ID)
+                    .Add(orderItem);
             }
 
             var jobs = Client.Query(
-                Filter<Job>.Where(x => x.ID).InList(stockMovements.Keys.Select(x => x.job).ToArray()),
+                Filter<Job>.Where(x => x.ID).InList(
+                    stockMovements.Keys.ToArray(x => x.job).Concatenate(purchaseOrderItems.Keys.ToArray())),
                 Columns.None<Job>().Add(x => x.ID).Add(x => x.JobNumber).Add(x => x.Name))
                 .ToObjects<Job>()
                 .ToDictionary(x => x.ID, x => x);
@@ -166,18 +219,20 @@ public partial class ReservationManagementHoldingsGrid
             greenList.Clear();
             yellowList.Clear();
             redList.Clear();
+            purchasedList.Clear();
+
             foreach (var (key, movements) in stockMovements)
             {
                 var job = jobs.GetValueOrDefault(key.job);
 
-                ReservationManagementHoldingsModel holding;
+                ReservationManagementHoldingsModel<StockMovement> holding;
                 if (key.allocated)
                 {
-                    holding = new ReservationManagementHoldingsModel(key.job, job?.JobNumber ?? "", job?.Name ?? "")
+                    holding = new(key.job, job?.JobNumber ?? "", job?.Name ?? "")
                     {
                         AlreadyAllocated = true
                     };
-                    DistributeMovements(movements, item.Style.ID, holding);
+                    DistributeItems(movements, item.Style.ID, holding, x => x.Units);
                     if (!holding.Empty)
                     {
                         redList.Add(holding);
@@ -185,20 +240,20 @@ public partial class ReservationManagementHoldingsGrid
                 }
                 else if (key.job == Guid.Empty)
                 {
-                    holding = new ReservationManagementHoldingsModel(Guid.Empty, "Free Stock", "Free Stock");
-                    DistributeMovements(movements, item.Style.ID, holding);
+                    holding = new(Guid.Empty, "Free Stock", "Free Stock");
+                    DistributeItems(movements, item.Style.ID, holding, x => x.Units);
                     greenList.Add(holding);
                 }
                 else if(key.job == item.Job.ID)
                 {
-                    holding = new ReservationManagementHoldingsModel(item.Job.ID, item.Job.JobNumber, item.Job.Name);
-                    DistributeMovements(movements, item.Style.ID, holding);
+                    holding = new(item.Job.ID, item.Job.JobNumber, item.Job.Name);
+                    DistributeItems(movements, item.Style.ID, holding, x => x.Units);
                     greenList.Add(holding);
                 }
                 else
                 {
-                    holding = new ReservationManagementHoldingsModel(key.job, job?.JobNumber ?? "", job?.Name ?? "");
-                    DistributeMovements(movements, item.Style.ID, holding);
+                    holding = new(key.job, job?.JobNumber ?? "", job?.Name ?? "");
+                    DistributeItems(movements, item.Style.ID, holding, x => x.Units);
                     if (!holding.Empty)
                     {
                         yellowList.Add(holding);
@@ -206,25 +261,50 @@ public partial class ReservationManagementHoldingsGrid
                 }
             }
 
+            foreach (var (key, orderItems) in purchaseOrderItems)
+            {
+                var job = jobs.GetValueOrDefault(key);
+
+                ReservationManagementHoldingsModel<PurchaseOrderItem> holding;
+                if(key == Guid.Empty)
+                {
+                    holding = new(Guid.Empty, "Free Stock", "Free Stock");
+                }
+                else
+                {
+                    holding = new ReservationManagementHoldingsModel<PurchaseOrderItem>(key, job?.JobNumber ?? "", job?.Name ?? "");
+                }
+                DistributeItems(orderItems, item.Style.ID, holding, x => orderItemQuantities.GetValueOrDefault(x.ID));
+                if (!holding.Empty)
+                {
+                    purchasedList.Add(holding);
+                }
+            }
+
             if (greenList.Count == 1)
-                greenList.Add(new ReservationManagementHoldingsModel());
+                greenList.Add(new());
 
             if (redList.Count == 1)
-                redList.Add(new ReservationManagementHoldingsModel());
+                redList.Add(new());
 
             if (yellowList.Count == 1)
-                yellowList.Add(new ReservationManagementHoldingsModel());
+                yellowList.Add(new());
+
+            if (purchasedList.Count == 1)
+                purchasedList.Add(new());
         }
         else
         {
             greenList.Clear();
             yellowList.Clear();
             redList.Clear();
+            purchasedList.Clear();
         }
 
         listViewGreen.ItemsSource = greenList;
         listViewYellow.ItemsSource = yellowList;
         listViewRed.ItemsSource = redList;
+        listViewPurchased.ItemsSource = purchasedList;
     }
 
     //private void Take_RightClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
@@ -249,28 +329,53 @@ public partial class ReservationManagementHoldingsGrid
     private void Take_Click(object sender, RoutedEventArgs e)
     {
         if (sender is not FrameworkElement element
-            || element.DataContext is not ReservationManagementHoldingsModel model
-            || element.Tag is not List<StockMovement> mvts) return;
+            || element.DataContext is not ReservationManagementHoldingsModel model) return;
+        var thisList = element.Tag;
 
-        if (mvts == model.StockOfCurrentStyle && model.UnitsOfCurrentStyle.IsEffectivelyEqual(0.0)
-            || mvts == model.StockOfNoStyle && model.UnitsOfNoStyle.IsEffectivelyEqual(0.0)
-            || mvts == model.StockOfOtherStyles && model.UnitsOfOtherStyles.IsEffectivelyEqual(0.0))
+        if (thisList == model.BaseStockOfCurrentStyle && model.UnitsOfCurrentStyle.IsEffectivelyEqual(0.0)
+            || thisList == model.BaseStockOfNoStyle && model.UnitsOfNoStyle.IsEffectivelyEqual(0.0)
+            || thisList == model.BaseStockOfOtherStyles && model.UnitsOfOtherStyles.IsEffectivelyEqual(0.0))
         {
             return;
         }
 
-        LaunchStockSelectionPage(model, mvts);
+        if(model is ReservationManagementHoldingsModel<StockMovement> movementModel
+            && thisList is List<StockMovement> mvts)
+        {
+            LaunchStockSelectionPage(movementModel, mvts);
+        }
+        else if(model is ReservationManagementHoldingsModel<PurchaseOrderItem> orderItemModel
+            && thisList is List<PurchaseOrderItem> orderItems)
+        {
+            LaunchPurchaseOrderSelectionPage(orderItemModel, orderItems);
+        }
     }
 
-    private void LaunchStockSelectionPage(ReservationManagementHoldingsModel model, List<StockMovement> mvts)
+    private void LaunchPurchaseOrderSelectionPage(ReservationManagementHoldingsModel<PurchaseOrderItem> model, List<PurchaseOrderItem> mvts)
+    {
+        if (Item is null) return;
+
+        var page = new PurchaseOrderItemSelectionPage(
+            mvts.Where(x => !x.Unallocated.IsEffectivelyEqual(0.0)),
+            Item,
+            new Job { ID = model.JobID, Name = model.JobName, JobNumber = model.JobNumber });
+
+        if (page.ShowDialog() == true)
+        {
+            //MessageWindow.ShowMessage("Success - stock allocated to requisition line", "Success");
+            CalculateHoldings();
+            OnHoldingsReviewRefresh?.Invoke();
+        }
+    }
+    private void LaunchStockSelectionPage(ReservationManagementHoldingsModel<StockMovement> model, List<StockMovement> mvts)
     {
         if (Item is null) return;
 
-        var holdings = new Dictionary<(Guid locationID, Guid requiItemID), StockHolding>();
+        var holdings = new Dictionary<(Guid locationID, Guid styleID, Guid requiItemID), StockHolding>();
 
         foreach (var mvt in mvts)
         {
-            if(!holdings.TryGetValue(new(mvt.Location.ID, mvt.JobRequisitionItem.ID), out var holding))
+            if(!holdings.TryGetValue(new(mvt.Location.ID, mvt.Style.ID, mvt.JobRequisitionItem.ID), out var holding))
             {
                 holding = new StockHolding();
 
@@ -278,7 +383,7 @@ public partial class ReservationManagementHoldingsGrid
                 holding.Style.CopyFrom(mvt.Style);
                 holding.Dimensions.CopyFrom(mvt.Dimensions);
                 
-                holdings.Add(new(mvt.Location.ID, mvt.JobRequisitionItem.ID), holding);
+                holdings.Add(new(mvt.Location.ID, mvt.Style.ID, mvt.JobRequisitionItem.ID), holding);
             }
             if (mvt.JobRequisitionItem.ID != Guid.Empty && model.AlreadyAllocated)
             {

+ 22 - 5
prs.desktop/Panels/Reservation Management/Holdings/ReservationManagementHoldingsModel.cs

@@ -7,16 +7,12 @@ using InABox.Core;
 
 namespace PRSDesktop;
 
-public class ReservationManagementHoldingsModel
+public abstract class ReservationManagementHoldingsModel
 {
     public Visibility Visibility { get; set; }
     public string JobNumber { get; set; }
     public string JobName { get; set; }
 
-    public List<StockMovement> StockOfCurrentStyle { get; set; } = new List<StockMovement>();
-    public List<StockMovement> StockOfNoStyle { get; set; } = new List<StockMovement>();
-    public List<StockMovement> StockOfOtherStyles { get; set; } = new List<StockMovement>();
-
     public double UnitsOfCurrentStyle { get; set; }
     public double UnitsOfNoStyle { get; set; }
     public double UnitsOfOtherStyles { get; set; }
@@ -30,6 +26,10 @@ public class ReservationManagementHoldingsModel
     public bool AlreadyAllocated { get; set; }
     public Guid JobID { get; set; }
 
+    public abstract object? BaseStockOfCurrentStyle { get; }
+    public abstract object? BaseStockOfNoStyle { get; }
+    public abstract object? BaseStockOfOtherStyles { get; }
+
     public ReservationManagementHoldingsModel() : this(Guid.Empty, "", "")
     {
         Visibility = Visibility.Collapsed;
@@ -42,4 +42,21 @@ public class ReservationManagementHoldingsModel
         JobName = jobname;
         Visibility = Visibility.Visible;
     }
+}
+
+public class ReservationManagementHoldingsModel<T> : ReservationManagementHoldingsModel
+{
+    public List<T> StockOfCurrentStyle { get; set; } = new List<T>();
+    public List<T> StockOfNoStyle { get; set; } = new List<T>();
+    public List<T> StockOfOtherStyles { get; set; } = new List<T>();
+
+    public override object? BaseStockOfCurrentStyle => StockOfCurrentStyle;
+
+    public override object? BaseStockOfNoStyle => StockOfNoStyle;
+
+    public override object? BaseStockOfOtherStyles => StockOfOtherStyles;
+
+    public ReservationManagementHoldingsModel() : base() { }
+        
+    public ReservationManagementHoldingsModel(Guid jobid, string jobnumber, string jobname): base(jobid, jobnumber, jobname) { }
 }

+ 45 - 0
prs.desktop/Panels/Reservation Management/PurchaseOrderItemSelectionPage.xaml

@@ -0,0 +1,45 @@
+<wpf:ThemableWindow x:Class="PRSDesktop.PurchaseOrderItemSelectionPage"
+                    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"
+                    xmlns:wpf="clr-namespace:InABox.Wpf;assembly=InABox.Wpf"
+                    xmlns:WPF="clr-namespace:InABox.WPF;assembly=InABox.Wpf"
+                    mc:Ignorable="d" Title="Allocate Purchase Order Items for Line"
+                    Width="1000" WindowStartupLocation="CenterScreen"
+                    SizeToContent="Height"
+                    MaxHeight="600"
+                    x:Name="Window">
+    <wpf:ThemableWindow.Resources>
+        <WPF:BooleanToVisibilityConverter x:Key="boolToVisibilityConverter" TrueValue="Visible" FalseValue="Collapsed"/>
+    </wpf:ThemableWindow.Resources>
+
+    <Grid DataContext="{Binding ElementName=Window}">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="auto"/>
+            <RowDefinition Height="*"/>
+            <RowDefinition Height="auto"/>
+        </Grid.RowDefinitions>
+
+        <Border Grid.Column="0" BorderThickness="2" BorderBrush="Gray" Background="WhiteSmoke" Margin="5" Padding="15">
+            <TextBlock x:Name="jobLbl"  Text="Job" HorizontalAlignment="Center" FontSize="14" VerticalAlignment="Center" FontWeight="DemiBold"/>
+        </Border>
+
+        <local:PurchaseOrderItemSelectionGrid x:Name="Grid" Grid.Row="1" Margin="5"
+                                              PropertyChanged="Grid_PropertyChanged"
+                                              AfterRefresh="Grid_OnAfterRefresh"/>
+
+        <DockPanel Grid.Row="2" LastChildFill="False">
+            <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>
+</wpf:ThemableWindow>

+ 161 - 0
prs.desktop/Panels/Reservation Management/PurchaseOrderItemSelectionPage.xaml.cs

@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using com.sun.tools.javac.util;
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.Wpf;
+using javax.swing;
+using PRSDimensionUtils;
+
+namespace PRSDesktop;
+
+public class PurchaseOrderItemSelectionItem : JobRequisitionItemSelectionItem
+{
+    public PurchaseOrderItem OrderItem { get; set; }
+
+    public string PONumber => OrderItem.PurchaseOrderLink.PONumber;
+
+    public string Style => OrderItem.Style.Code;
+
+    public string UnitSize => OrderItem.Dimensions.UnitSize;
+}
+
+public class PurchaseOrderItemSelectionGrid : JobRequisitionItemSelectionGrid<PurchaseOrderItemSelectionItem>
+{
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var columns = new DynamicGridColumns();
+        columns.Add<PurchaseOrderItemSelectionItem>(x => x.PONumber, 0, "Purchase Order", "", Alignment.MiddleCenter);
+        columns.Add<PurchaseOrderItemSelectionItem>(x => x.Style, 0, "Style", "", Alignment.MiddleCenter);
+        columns.Add<PurchaseOrderItemSelectionItem>(x => x.UnitSize, 0, "Size", "", Alignment.MiddleCenter);
+        return columns;
+    }
+
+    protected override void Reload(
+    	Filters<PurchaseOrderItemSelectionItem> criteria, Columns<PurchaseOrderItemSelectionItem> columns, ref SortOrder<PurchaseOrderItemSelectionItem>? sort,
+    	CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        foreach(var item in Items)
+        {
+            item.Issued = 0;
+            item.Quantity = item.OrderItem.Unallocated;
+            item.MaxValue = item.Quantity;
+        }
+
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
+}
+
+/// <summary>
+/// Interaction logic for JobRequisitionStockSelectionPage.xaml
+/// </summary>
+public partial class PurchaseOrderItemSelectionPage : ThemableWindow, INotifyPropertyChanged
+{
+    public class Holding(StockHolding holding, Guid jobRequisitionItemID)
+    {
+        public StockHolding StockHolding { get; set; } = holding;
+
+        public Guid JobRequisitionItemID { get; set; } = jobRequisitionItemID;
+    }
+
+    public bool CanSave => Grid.TotalIssued > 0;
+
+    public JobRequisitionItem Item { get; set; }
+
+    public Guid IssuingJobID { get; set; }
+
+    private bool _canEdit = true;
+    public bool CanEdit
+    {
+        get => _canEdit;
+        private set
+        {
+            _canEdit = value;
+            UpdateItems();
+        }
+    }
+
+    private void UpdateItems()
+    {
+        foreach(var item in Grid.Items)
+        {
+            item.Editable = _canEdit && item.Quantity.IsEffectivelyGreaterThan(0.0);
+        }
+    }
+
+    public PurchaseOrderItemSelectionPage(IEnumerable<PurchaseOrderItem> orderItems, JobRequisitionItem item, Job issuingJob)
+    {
+        InitializeComponent();
+
+        Item = item;
+
+        Grid.Items = orderItems.Select(x => new PurchaseOrderItemSelectionItem
+        {
+            OrderItem = x
+        }).ToList();
+
+        IssuingJobID = issuingJob.ID;
+        jobLbl.Text = $"Taking stock from orders for Job: {issuingJob.Name} ({issuingJob.JobNumber})";
+
+        Grid.Refresh(true, true);
+    }
+
+    private void Grid_PropertyChanged(object sender, PropertyChangedEventArgs e)
+    {
+        if(e.PropertyName == nameof(Grid.TotalIssued))
+        {
+            OnPropertyChanged(nameof(CanSave));
+        }
+    }
+
+    private void OKButton_Click(object sender, RoutedEventArgs e)
+    {
+        if(Grid.TotalIssued <= 0)
+        {
+            MessageWindow.ShowMessage("Please select from at least one purchase order item to reserve.", "Error", image: MessageWindow.WarningImage);
+            return;
+        }
+
+        var allocations = new List<PurchaseOrderItemAllocation>();
+        foreach (var item in Grid.Items.Where(x => x.Issued > 0))
+        {
+            var allocation = new PurchaseOrderItemAllocation();
+            allocation.Item.CopyFrom(item.OrderItem);
+            allocation.Job.CopyFrom(Item.Job);
+            allocation.JobRequisitionItem.CopyFrom(Item);
+            allocation.Quantity = item.Issued;
+
+            allocations.Add(allocation);
+        }
+        Client.Save(allocations, "Allocated to Job Requisition Item");
+
+        DialogResult = true;
+    }
+
+    private void CancelButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = false;
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+
+    private void Grid_OnAfterRefresh(object sender, AfterRefreshEventArgs args)
+    {
+        UpdateItems();
+    }
+}