Kaynağa Gözat

Reservation management screen improvements for splitting lines and general code improvemenyt

Kenric Nugteren 1 yıl önce
ebeveyn
işleme
fe6f4a4082

+ 13 - 95
prs.desktop/Panels/Products/Reservation Management/JobRequiHoldingsReviewModel.cs

@@ -11,112 +11,30 @@ public class JobRequiHoldingsReviewModel
     public Visibility Visibility { get; set; }
     public string JobNumber { get; set; }
     public string JobName { get; set; }
-    public double StockOfCurrentStyle { get; set; }
-    public double StockOfNoStyle { get; set; }
-    public double StockOfOtherStyles { get; set; }
-    public List<StockMovement> Movements { 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; }
+
+    public bool Empty => UnitsOfCurrentStyle <= 0 && UnitsOfNoStyle <= 0 && UnitsOfOtherStyles <= 0;
+
     public bool AlreadyAllocated { get; set; }
     public Guid JobID { get; set; }
-    public ProductStyleLink DefaultStyle { get; set; }
 
-    public JobRequiHoldingsReviewModel() : this(Guid.Empty, "","",new List<StockMovement>(), new ProductStyleLink())
+    public JobRequiHoldingsReviewModel() : this(Guid.Empty, "", "")
     {
         Visibility = Visibility.Collapsed;
     }
         
-    public JobRequiHoldingsReviewModel(Guid jobid, string jobnumber, string jobname, List<StockMovement> movements, ProductStyleLink style)
+    public JobRequiHoldingsReviewModel(Guid jobid, string jobnumber, string jobname)
     {
         JobNumber = jobnumber;
         JobID = jobid;
-        Movements = movements;
-        DefaultStyle = style;
         JobName = jobname;
         Visibility = Visibility.Visible;
     }
-
-    public void GetStock(Guid jobid, Guid styleid)
-    {
-        GetStockOfCurrentStyle(jobid, styleid);
-        GetStockOfNoStyle(jobid);
-        GetStockOfOtherStyles(jobid, styleid);
-    }
-    private void GetStockOfCurrentStyle(Guid jobid, Guid styleid)
-    {
-        if (styleid == Guid.Empty)
-            return;
-
-        if (!AlreadyAllocated)
-        {
-            StockOfCurrentStyle = CalculateTotal(Movements
-                                      .Where(x => x.Job.ID == jobid)
-                                      .Where(x => x.Style.ID == styleid)
-                                  )
-                                  -
-                                  CalculateTotal(Movements
-                                      .Where(x => x.Job.ID == jobid)
-                                      .Where(x => x.Style.ID == styleid)
-                                      .Where(x => x.JobRequisitionItem.ID != Guid.Empty));
-        }
-
-        else
-            StockOfCurrentStyle = CalculateTotal(Movements
-                .Where(x => x.Job.ID == jobid)
-                .Where(x => x.Style.ID == styleid)
-                .Where(x => x.JobRequisitionItem.ID != Guid.Empty));
-    }
-
-    private void GetStockOfOtherStyles(Guid jobid, Guid styleid)
-    {
-        if (!AlreadyAllocated)
-            StockOfOtherStyles = CalculateTotal(Movements
-                .Where(x => x.Job.ID == jobid)
-                .Where(x => x.Style.ID != Guid.Empty)
-                .Where(x => x.Style.ID != DefaultStyle.ID)
-                .Where(x => x.Style.ID != styleid)
-                .Where(x => x.JobRequisitionItem.ID == Guid.Empty));
-
-        else
-            StockOfOtherStyles = CalculateTotal(Movements
-                .Where(x => x.Job.ID == jobid)
-                .Where(x => x.Style.ID != Guid.Empty)
-                .Where(x => x.Style.ID != DefaultStyle.ID)
-                .Where(x => x.Style.ID != styleid)
-                .Where(x => x.JobRequisitionItem.ID != Guid.Empty));
-    }
-
-    private void GetStockOfNoStyle(Guid jobid)
-    {
-        if (!AlreadyAllocated)
-            StockOfNoStyle = CalculateTotal(Movements
-                .Where(x => x.Job.ID == jobid)
-                .Where(x => x.Style.ID == Guid.Empty || x.Style.ID == DefaultStyle.ID)
-                .Where(x => x.JobRequisitionItem.ID == Guid.Empty));
-
-        else
-            StockOfNoStyle = CalculateTotal(Movements
-                .Where(x => x.Job.ID == jobid)
-                .Where(x => x.Style.ID == Guid.Empty || x.Style.ID == DefaultStyle.ID)
-                .Where(x => x.JobRequisitionItem.ID != Guid.Empty));
-    }
-
-    private double CalculateTotal(IEnumerable<StockMovement> mvts)
-    {
-        double total = 0;
-
-        double rec = 0;
-        double issued = 0;
-
-        foreach (var sm in mvts)
-        {
-            if (sm.Received != 0)
-                rec = rec + sm.Received;
-            else if (sm.Issued != 0)
-                issued = issued + sm.Issued;
-        }
-
-        if (rec >= issued)
-            total = rec - issued;
-
-        return total;
-    }
 }

+ 9 - 6
prs.desktop/Panels/Products/Reservation Management/JobRequisitionHoldingsReview.xaml

@@ -68,14 +68,15 @@
                     Padding="2">
                     <Button 
                         x:Name="currentStyle" 
-                        Content="{Binding StockOfCurrentStyle}" 
+                        Content="{Binding UnitsOfCurrentStyle}"
+                        Tag="{Binding StockOfCurrentStyle}"
                         Background="WhiteSmoke"
                         Click="Take_Click">
                         <Button.Style>
                             <Style TargetType="Button">
                                 <Setter Property="Visibility" Value="{Binding Visibility}" />
                                 <Style.Triggers>
-                                    <DataTrigger Binding="{Binding StockOfCurrentStyle}" Value="0">
+                                    <DataTrigger Binding="{Binding UnitsOfCurrentStyle}" Value="0">
                                         <Setter Property="Visibility" Value="Collapsed" />
                                     </DataTrigger>
                                 </Style.Triggers>
@@ -92,14 +93,15 @@
                     Padding="2">
                     <Button 
                         x:Name="noStyle" 
-                        Content="{Binding StockOfNoStyle}"  
+                        Content="{Binding UnitsOfNoStyle}" 
+                        Tag="{Binding StockOfNoStyle}" 
                         Background="WhiteSmoke"
                         Click="Take_Click">
                         <Button.Style>
                             <Style TargetType="Button">
                                 <Setter Property="Visibility" Value="{Binding Visibility}" />
                                 <Style.Triggers>
-                                    <DataTrigger Binding="{Binding StockOfNoStyle}" Value="0">
+                                    <DataTrigger Binding="{Binding UnitsOfNoStyle}" Value="0">
                                         <Setter Property="Visibility" Value="Collapsed" />
                                     </DataTrigger>
                                 </Style.Triggers>
@@ -117,14 +119,15 @@
                     <Button 
                         Grid.Column="4" 
                         x:Name="otherStyle" 
-                        Content="{Binding StockOfOtherStyles}"  
+                        Content="{Binding UnitsOfOtherStyles}"  
+                        Tag="{Binding StockOfOtherStyles}"
                         Background="WhiteSmoke"
                         Click="Take_Click">
                         <Button.Style>
                             <Style TargetType="Button">
                                 <Setter Property="Visibility" Value="{Binding Visibility}" />
                                 <Style.Triggers>
-                                    <DataTrigger Binding="{Binding StockOfOtherStyles}" Value="0">
+                                    <DataTrigger Binding="{Binding UnitsOfOtherStyles}" Value="0">
                                         <Setter Property="Visibility" Value="Collapsed" />
                                     </DataTrigger>
                                 </Style.Triggers>

+ 226 - 240
prs.desktop/Panels/Products/Reservation Management/JobRequisitionHoldingsReview.xaml.cs

@@ -1,6 +1,8 @@
 using Comal.Classes;
 using InABox.Clients;
 using InABox.Core;
+using InABox.Wpf;
+using org.omg.PortableInterceptor;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
@@ -8,308 +10,292 @@ using System.Linq;
 using System.Windows;
 using System.Windows.Controls;
 
-namespace PRSDesktop
+namespace PRSDesktop;
+
+public delegate void HoldingsReviewRefresh();
+/// <summary>
+/// Interaction logic for JobRequisitionHoldingsReview.xaml
+/// </summary>
+public partial class JobRequisitionHoldingsReview
 {
-    public delegate void HoldingsReviewRefresh();
-    /// <summary>
-    /// Interaction logic for JobRequisitionHoldingsReview.xaml
-    /// </summary>
-    public partial class JobRequisitionHoldingsReview
+    private ProductStyleLink companyDefaultStyle = new ProductStyleLink();
+    public ProductStyleLink CompanyDefaultStyle
     {
-        private ProductStyleLink companyDefaultStyle = new ProductStyleLink();
-        public ProductStyleLink CompanyDefaultStyle
+        get => companyDefaultStyle;
+        set
         {
-            get => companyDefaultStyle;
-            set
-            {
-                companyDefaultStyle = value;
-                DefaultOrNoStyleText.Text = companyDefaultStyle.Description + @" /None";
-            }
+            companyDefaultStyle = value;
+            DefaultOrNoStyleText.Text = companyDefaultStyle.Description + @" /None";
         }
+    }
 
-        public event HoldingsReviewRefresh? OnHoldingsReviewRefresh;
-        List<Job> jobs = new List<Job>();
+    public event HoldingsReviewRefresh? OnHoldingsReviewRefresh;
 
-        private JobRequisitionItem? item;
-        public JobRequisitionItem? Item
+    private JobRequisitionItem? item;
+    public JobRequisitionItem? Item
+    {
+        get
         {
-            get
-            {
-                return item;
-            }
-            set
+            return item;
+        }
+        set
+        {
+            if(item?.ID != value?.ID)
             {
-                if(item?.ID != value?.ID)
-                {
-                    item = value;
-                    CalculateHoldings();
-                    SetStyle();
-                    SetProductName();
-                }
+                item = value;
+                CalculateHoldings();
+                SetStyle();
+                SetProductName();
             }
         }
+    }
 
-        private void SetProductName()
-        {
-            productLbl.Text = Item != null
-                ? Item.Product.Name + " (" + Item.Product.Code + ")"
-                : "No Product Selected";
-        }
+    private void SetProductName()
+    {
+        productLbl.Text = Item != null
+            ? Item.Product.Name + " (" + Item.Product.Code + ")"
+            : "No Product Selected";
+    }
+
+    private void SetStyle()
+    {
+        if ((Item != null) &&  (Item.Style.ID != Guid.Empty))
+            styleLbl.Text = Item.Style.Description + " (" + Item.Style.Code + ")";
+        else
+            styleLbl.Text = "No Style Selected";
+    }
+
+    public JobRequisitionHoldingsReview()
+    {
+        InitializeComponent();
 
-        private void SetStyle()
+        greenList.CollectionChanged += (s, e) =>
         {
-            if ((Item != null) &&  (Item.Style.ID != Guid.Empty))
-                styleLbl.Text = Item.Style.Description + " (" + Item.Style.Code + ")";
-            else
-                styleLbl.Text = "No Style Selected";
-        }
+            availableTab.Visibility = greenList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+        };
+        yellowList.CollectionChanged += (s, e) =>
+        {
+            reservedTab.Visibility = yellowList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+        };
+        redList.CollectionChanged += (s, e) =>
+        {
+            requisitionedTab.Visibility = redList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+        };
+    }
+
+    private readonly ObservableCollection<JobRequiHoldingsReviewModel> greenList = new();
+    private readonly ObservableCollection<JobRequiHoldingsReviewModel> yellowList = new();
+    private readonly ObservableCollection<JobRequiHoldingsReviewModel> redList = new();
 
-        public JobRequisitionHoldingsReview()
+    private void DistributeMovements(Dictionary<Guid, List<StockMovement>>? movements, Guid selectedStyleID, JobRequiHoldingsReviewModel model)
+    {
+        if (movements is null) return;
+
+        foreach(var (id, mvts) in movements)
         {
-            InitializeComponent();
-            CoreTable table = new Client<Job>().Query(null, new Columns<Job>(x => x.ID, x => x.JobNumber, x => x.Name));
-            foreach (var row in table.Rows)
+            if(id == Guid.Empty || id == CompanyDefaultStyle.ID)
             {
-                jobs.Add(row.ToObject<Job>());
+                model.StockOfNoStyle.AddRange(mvts);
             }
-            greenList.CollectionChanged += (s, e) =>
-            {
-                availableTab.Visibility = greenList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
-            };
-            yellowList.CollectionChanged += (s, e) =>
+            else if (id == selectedStyleID)
             {
-                reservedTab.Visibility = yellowList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
-            };
-            redList.CollectionChanged += (s, e) =>
+                model.StockOfCurrentStyle.AddRange(mvts);
+            }
+            else
             {
-                requisitionedTab.Visibility = redList.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
-            };
+                model.StockOfOtherStyles.AddRange(mvts);
+            }
         }
+        model.UnitsOfCurrentStyle = Math.Max(model.StockOfCurrentStyle.Sum(x => x.Units), 0);
+        model.UnitsOfNoStyle = Math.Max(model.StockOfNoStyle.Sum(x => x.Units), 0);
+        model.UnitsOfOtherStyles = Math.Max(model.StockOfOtherStyles.Sum(x => x.Units), 0);
+    }
 
-        private readonly ObservableCollection<JobRequiHoldingsReviewModel> greenList = new();
-        private readonly ObservableCollection<JobRequiHoldingsReviewModel> yellowList = new();
-        private readonly ObservableCollection<JobRequiHoldingsReviewModel> redList = new();
-
-        private void CalculateHoldings()
+    private void CalculateHoldings()
+    {
+        if(item is not null)
         {
-            CoreTable table = new Client<StockMovement>().Query(
-                new Filter<StockMovement>(x => x.Product.ID).IsEqualTo(item?.Product.ID ?? Guid.Empty).
-                And(x => x.Dimensions.UnitSize).IsEqualTo(item?.Dimensions.UnitSize ?? "")
-                .And(x => x.Location.ID).IsNotEqualTo(Guid.Empty),
+            var stockMovements = Client.Query(
+                new Filter<StockMovement>(x => x.Product.ID).IsEqualTo(item.Product.ID)
+                    .And(x => x.Dimensions.UnitSize).IsEqualTo(item.Dimensions.UnitSize)
+                    .And(x => x.Location.ID).IsNotEqualTo(Guid.Empty),
                 new Columns<StockMovement>(
                     x => x.ID,
                     x => x.Style.ID,
                     x => x.Style.Description,
                     x => x.Style.Code,
-                    x => x.Received,
-                    x => x.Issued,
+                    x => x.Units,
                     x => x.Location.ID,
                     x => x.Location.Code,
                     x => x.Location.Description,
                     x => x.Location.Area.Description,
                     x => x.Job.ID,
-                    x => x.Job.JobNumber,
-                    x => x.JobRequisitionItem.ID,
-                    x => x.Dimensions.Unit.ID,
-                    x => x.Dimensions.Unit.Formula,
-                    x => x.Dimensions.Unit.Format,
-                    x => x.Dimensions.Unit.HasHeight,
-                    x => x.Dimensions.Unit.HasWeight,
-                    x => x.Dimensions.Unit.HasLength,
-                    x => x.Dimensions.Unit.HasWidth,
-                    x => x.Dimensions.Unit.HasQuantity,
-                    x => x.Dimensions.Quantity,
-                    x => x.Dimensions.Height,
-                    x => x.Dimensions.Weight,
-                    x => x.Dimensions.Width,
-                    x => x.Dimensions.Length,
-                    x => x.Dimensions.UnitSize,
-                    x => x.Dimensions.Value
-                    ));
-
-            List<StockMovement> stockMovements = new List<StockMovement>();
-
-            foreach (CoreRow row in table.Rows)
-                stockMovements.Add(row.ToObject<StockMovement>());
-
-            CalculateHoldingsForJob(stockMovements);
-            CalculateHoldingsForFreeStock(stockMovements);
-            listViewGreen.ItemsSource = greenList;
-
-            CalculateNonRequiHoldingsForOtherJobs(stockMovements);
-            listViewYellow.ItemsSource = yellowList;
-
-            CalculateAlreadyRequiedHoldings(stockMovements);
-            listViewRed.ItemsSource = redList;
-        }
-
-        private void CalculateHoldingsForJob(List<StockMovement> mvts)
-        {
-            greenList.Clear();
-            if (item != null)
+                    x => x.JobRequisitionItem.ID)
+                    .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data))
+                .ToObjects<StockMovement>()
+                .GroupBy(x => new { JobID = x.Job.ID, Allocated = x.JobRequisitionItem.ID != Guid.Empty })
+                .ToDictionary(
+                    x => x.Key,
+                    x => x.GroupBy(x => x.Style.ID)
+                        .Select(x =>
+                        {
+                            var mvts = x.ToList();
+                            return new
+                            {
+                                Style = x.Key,
+                                Movements = mvts
+                            };
+                        })
+                        .ToDictionary(
+                            x => x.Style,
+                            x => x.Movements));
+
+            var jobs = Client.Query(
+                new Filter<Job>(x => x.ID).InList(stockMovements.Keys.Select(x => x.JobID).ToArray()),
+                new Columns<Job>(x => x.ID).Add(x => x.JobNumber).Add(x => x.Name))
+                .ToObjects<Job>()
+                .ToDictionary(x => x.ID, x => x);
+
+            if(!stockMovements.ContainsKey(new { JobID = item.Job.ID, Allocated = false }))
             {
-                var currentJobHoldings = new JobRequiHoldingsReviewModel(item.Job.ID,
-                    item.Job.JobNumber, item.Job.Name, mvts, CompanyDefaultStyle);
-                currentJobHoldings.GetStock(item.Job.ID, item.Style.ID);
-                greenList.Add(currentJobHoldings);
+                stockMovements.Add(new { JobID = item.Job.ID, Allocated = false }, new Dictionary<Guid, List<StockMovement>>());
             }
-        }
-
-        private void CalculateHoldingsForFreeStock(List<StockMovement> mvts)
-        {
-            if (item != null)
+            if (!stockMovements.ContainsKey(new { JobID = Guid.Empty, Allocated = false }))
             {
-                var freeStock = new JobRequiHoldingsReviewModel(Guid.Empty, "Free Stock", "Free Stock", mvts,
-                    CompanyDefaultStyle);
-                freeStock.GetStock(Guid.Empty, item.Style.ID);
-                greenList.Add(freeStock);
+                stockMovements.Add(new { JobID = Guid.Empty, Allocated = false }, new Dictionary<Guid, List<StockMovement>>());
             }
-        }
 
-        private void CalculateNonRequiHoldingsForOtherJobs(List<StockMovement> mvts)
-        {
+            greenList.Clear();
             yellowList.Clear();
-            if (item != null)
+            redList.Clear();
+            foreach (var (key, movements) in stockMovements)
             {
-                foreach (var job in jobs)
-                {
-                    if (job.ID == item.Job.ID)
-                        continue;
-
-                    var holdings =
-                        new JobRequiHoldingsReviewModel(job.ID, job.JobNumber, job.Name, mvts, CompanyDefaultStyle);
-                    holdings.GetStock(job.ID, item.Style.ID);
+                var job = jobs.GetValueOrDefault(key.JobID);
 
-                    if (holdings.StockOfCurrentStyle == 0 && holdings.StockOfNoStyle == 0 &&
-                        holdings.StockOfOtherStyles == 0)
-                        continue;
-                    yellowList.Add(holdings);
+                JobRequiHoldingsReviewModel holding;
+                if (key.Allocated)
+                {
+                    holding = new JobRequiHoldingsReviewModel(key.JobID, job?.JobNumber ?? "", job?.Name ?? "")
+                    {
+                        AlreadyAllocated = true
+                    };
+                    DistributeMovements(movements, item.Style.ID, holding);
+                    if (!holding.Empty)
+                    {
+                        redList.Add(holding);
+                    }
+                }
+                else if (key.JobID == Guid.Empty)
+                {
+                    holding = new JobRequiHoldingsReviewModel(Guid.Empty, "Free Stock", "Free Stock");
+                    DistributeMovements(movements, item.Style.ID, holding);
+                    greenList.Add(holding);
+                }
+                else if(key.JobID == item.Job.ID)
+                {
+                    holding = new JobRequiHoldingsReviewModel(item.Job.ID, item.Job.JobNumber, item.Job.Name);
+                    DistributeMovements(movements, item.Style.ID, holding);
+                    greenList.Add(holding);
+                }
+                else
+                {
+                    holding = new JobRequiHoldingsReviewModel(key.JobID, job?.JobNumber ?? "", job?.Name ?? "");
+                    DistributeMovements(movements, item.Style.ID, holding);
+                    if (!holding.Empty)
+                    {
+                        yellowList.Add(holding);
+                    }
                 }
-
-                if (yellowList.Count == 1)
-                    yellowList.Add(new JobRequiHoldingsReviewModel());
             }
 
-        }
+            if (greenList.Count == 1)
+                greenList.Add(new JobRequiHoldingsReviewModel());
 
-        private void CalculateAlreadyRequiedHoldings(List<StockMovement> mvts)
+            if (redList.Count == 1)
+                redList.Add(new JobRequiHoldingsReviewModel());
+
+            if (yellowList.Count == 1)
+                yellowList.Add(new JobRequiHoldingsReviewModel());
+        }
+        else
         {
+            greenList.Clear();
+            yellowList.Clear();
             redList.Clear();
-            if (item != null)
-            {
-                foreach (var job in jobs)
-                {
-                    var holdings =
-                        new JobRequiHoldingsReviewModel(job.ID, job.JobNumber, job.Name, mvts, CompanyDefaultStyle);
-                    holdings.AlreadyAllocated = true;
-                    holdings.GetStock(job.ID, item.Style.ID);
-
-                    if (holdings.StockOfCurrentStyle == 0 && holdings.StockOfNoStyle == 0 &&
-                        holdings.StockOfOtherStyles == 0)
-                        continue;
+        }
 
-                    redList.Add(holdings);
+        listViewGreen.ItemsSource = greenList;
+        listViewYellow.ItemsSource = yellowList;
+        listViewRed.ItemsSource = redList;
+    }
 
-                }
+    private void Take_Click(object sender, RoutedEventArgs e)
+    {
+        if (sender is not FrameworkElement element
+            || element.DataContext is not JobRequiHoldingsReviewModel model
+            || element.Tag is not List<StockMovement> mvts) return;
+        LaunchStockSelectionPage(model, mvts);
+    }
 
-                if (redList.Count == 1)
-                    redList.Add(new JobRequiHoldingsReviewModel());
-            }
-        }
+    private void LaunchStockSelectionPage(JobRequiHoldingsReviewModel model, List<StockMovement> mvts)
+    {
+        if (Item is null) return;
 
-        private void Take_Click(object sender, RoutedEventArgs e)
-        {
-            LaunchStockSelectionPage((sender as Button).Name, (sender as Button).DataContext as JobRequiHoldingsReviewModel);
-        }
+        var locationIDs = new List<Guid>();
+        var holdings = new List<StockHolding>();
 
-        private void LaunchStockSelectionPage(string buttonName, JobRequiHoldingsReviewModel model)
+        foreach (var mvt in mvts)
         {
-            if (Item is null) return;
-
-            var mvts = model.Movements.Where(x => x.Job.ID == model.JobID);
-
-            switch (buttonName)
+            if (!locationIDs.Contains(mvt.Location.ID))
             {
-                case "currentStyle":
-                    if (model.StockOfCurrentStyle == 0)
-                        return;
-                    mvts = mvts
-                        .Where(x => x.Style.ID == Item.Style.ID);
-                    break;
-                case "noStyle":
-                    if (model.StockOfNoStyle == 0)
-                        return;
-                    mvts = mvts
-                        .Where(x => x.Style.ID == Guid.Empty || x.Style.ID == CompanyDefaultStyle.ID);
-                    break;
-                case "otherStyle":
-                    if (model.StockOfOtherStyles == 0)
-                        return;
-                    mvts = mvts
-                        .Where(x => x.Style.ID != Guid.Empty && x.Style.ID != Item.Style.ID && x.Style.ID != CompanyDefaultStyle.ID);
-                    break;
+                var holding = new StockHolding();
+                holding.Location.ID = mvt.Location.ID;
+                holding.Location.Description = mvt.Location.Description;
+                holding.Location.Area.Description = mvt.Location.Area.Description;
+                holding.Style.ID = mvt.Style.ID;
+                holding.Style.Description = mvt.Style.Description;
+                holding.Style.Code = mvt.Style.Code;
+                holding.Dimensions.CopyFrom(mvt.Dimensions);
+                if (mvt.JobRequisitionItem.ID != Guid.Empty && model.AlreadyAllocated)
+                {
+                    holding.Units = mvt.Units;
+                    holdings.Add(holding);
+                    locationIDs.Add(mvt.Location.ID);
+                }
+                else if (mvt.JobRequisitionItem.ID == Guid.Empty && !model.AlreadyAllocated)
+                {
+                    holding.Units = mvt.Units;
+                    holdings.Add(holding);
+                    locationIDs.Add(mvt.Location.ID);
+                }
             }
-
-            List<Guid> locationIDs = new List<Guid>();
-            List<StockHolding> holdings = new List<StockHolding>();
-            List<StockMovement> movements = new List<StockMovement>();
-            foreach (var mvt in mvts)
-                movements.Add(mvt);
-
-            foreach (var mvt in movements)
+            else
             {
-                if (!locationIDs.Contains(mvt.Location.ID))
+                if (mvt.JobRequisitionItem.ID != Guid.Empty && model.AlreadyAllocated)
                 {
-                    var holding = new StockHolding();
-                    holding.Location.ID = mvt.Location.ID;
-                    holding.Location.Description = mvt.Location.Description;
-                    holding.Location.Area.Description = mvt.Location.Area.Description;
-                    holding.Style.ID = mvt.Style.ID;
-                    holding.Style.Description = mvt.Style.Description;
-                    holding.Style.Code = mvt.Style.Code;
-                    holding.Dimensions.CopyFrom(mvt.Dimensions);
-                    if (mvt.JobRequisitionItem.ID != Guid.Empty && model.AlreadyAllocated)
-                    {
-                        holding.Units = mvt.Received - mvt.Issued;
-                        holdings.Add(holding);
-                        locationIDs.Add(mvt.Location.ID);
-                    }
-                    else if (mvt.JobRequisitionItem.ID == Guid.Empty && !model.AlreadyAllocated)
-                    {
-                        holding.Units = mvt.Received - mvt.Issued;
-                        holdings.Add(holding);
-                        locationIDs.Add(mvt.Location.ID);
-                    }
+                    var holding = holdings.First(x => x.Location.ID == mvt.Location.ID);
+                    holding.Units += mvt.Units;
                 }
-                else
+                else if (mvt.JobRequisitionItem.ID == Guid.Empty && !model.AlreadyAllocated)
                 {
-                    if (mvt.JobRequisitionItem.ID != Guid.Empty && model.AlreadyAllocated)
-                    {
-                        var holding = holdings.First(x => x.Location.ID == mvt.Location.ID);
-                        holding.Units = holding.Units + mvt.Received - mvt.Issued;
-                    }
-                    else if (mvt.JobRequisitionItem.ID == Guid.Empty && !model.AlreadyAllocated)
-                    {
-                        var holding = holdings.First(x => x.Location.ID == mvt.Location.ID);
-                        holding.Units = holding.Units + mvt.Received - mvt.Issued;
-                    }
-
+                    var holding = holdings.First(x => x.Location.ID == mvt.Location.ID);
+                    holding.Units += mvt.Units;
                 }
+
             }
-            var filteredHoldings = holdings.Where(x => x.Units > 0);
+        }
+        var filteredHoldings = holdings.Where(x => x.Units > 0);
 
-            var page = new StockSelectionPage(
-                filteredHoldings,
-                Item,
-                new Job { ID = model.JobID, Name = model.JobName, JobNumber = model.JobNumber },
-                model.AlreadyAllocated);
+        var page = new StockSelectionPage(
+            filteredHoldings,
+            Item,
+            new Job { ID = model.JobID, Name = model.JobName, JobNumber = model.JobNumber },
+            model.AlreadyAllocated);
 
-            if (page.ShowDialog() == true)
-            {
-                MessageBox.Show("Success - stock allocated to requisition line");
-                OnHoldingsReviewRefresh?.Invoke();
-            }
-                
+        if (page.ShowDialog() == true)
+        {
+            MessageWindow.ShowMessage("Success - stock allocated to requisition line", "Success");
+            OnHoldingsReviewRefresh?.Invoke();
         }
     }
 }

+ 42 - 0
prs.desktop/Panels/Products/Reservation Management/JobRequisitionItemPurchaseOrderItemGrid.cs

@@ -0,0 +1,42 @@
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.DynamicGrid;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRSDesktop;
+
+public class JobRequisitionItemPurchaseOrderItemGrid : DynamicOneToManyGrid<JobRequisitionItem, JobRequisitionItemPurchaseOrderItem>
+{
+    public override void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    {
+        Reconfigure();
+        Refresh(true, false);
+        base.Load(item, type =>
+        {
+            var data = PageDataHandler?.Invoke(type);
+            if (data is null && type == typeof(JobRequisitionItemPurchaseOrderItem))
+            {
+                Filter<JobRequisitionItemPurchaseOrderItem> filter;
+                if (Item.ID == Guid.Empty)
+                {
+                    filter = new Filter<JobRequisitionItemPurchaseOrderItem>().None();
+                }
+                else
+                {
+                    filter = new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.JobRequisitionItem.ID).IsEqualTo(Item.ID);
+                }
+
+                data = new Client<JobRequisitionItemPurchaseOrderItem>().Query(
+                    filter,
+                    DynamicGridUtils.LoadEditorColumns(DataColumns()),
+                    LookupFactory.DefineSort<JobRequisitionItemPurchaseOrderItem>());
+            }
+            return data;
+        });
+    }
+}

+ 15 - 11
prs.desktop/Panels/Products/Reservation Management/JobRequisitionReviewGrid.cs

@@ -49,6 +49,7 @@ public class JobRequisitionReviewGrid : DynamicDataGrid<JobRequisitionItem>
         FilterComponent.SetSettings(FilterSettings.Filters, false);
 
         HiddenColumns.Add(x => x.ID);
+        HiddenColumns.Add(x => x.InStock);
         HiddenColumns.Add(x => x.Product.ID);
         HiddenColumns.Add(x => x.Product.Code);
         HiddenColumns.Add(x => x.Product.Group.ID);
@@ -175,26 +176,28 @@ public class JobRequisitionReviewGrid : DynamicDataGrid<JobRequisitionItem>
             MessageWindow.ShowMessage("Item is already on order!", "Error", image: MessageWindow.WarningImage);
             return false;
         }
+        else if (item.InStock >= item.Qty)
+        {
+            MessageWindow.ShowMessage("Item is already in stock!", "Error", image: MessageWindow.WarningImage);
+            return false;
+        }
         return valid;
     }
 
     private void SplitLine(JobRequisitionItem item, double oldItemQty, double newItemQty, string notes)
     {
         var items = new List<JobRequisitionItem>();
+
         var newItem = new JobRequisitionItem();
+
         newItem.Requisition.ID = item.Requisition.ID;
         newItem.Requisition.Job.ID = item.Requisition.Job.ID;
-        newItem.Requisition.Job.JobNumber = item.Requisition.Job.JobNumber;
-        newItem.Requisition.Job.Name = item.Requisition.Job.Name;
+
         newItem.Product.ID = item.Product.ID;
-        newItem.Product.Name = item.Product.Name;
-        newItem.Product.Code = item.Product.Code;
-        newItem.Product.Group.ID = item.Product.Group.ID;
-        newItem.Product.Group.Description = item.Product.Group.Description;
+
         newItem.Dimensions.CopyFrom(item.Dimensions);
         newItem.Style.ID = item.Style.ID;
-        newItem.Style.Description = item.Style.Description;
-        newItem.Style.Code = item.Style.Code;
+
         newItem.Notes = item.Notes + Environment.NewLine + notes;
         item.Notes = newItem.Notes;
 
@@ -203,8 +206,9 @@ public class JobRequisitionReviewGrid : DynamicDataGrid<JobRequisitionItem>
 
         items.Add(newItem);
         items.Add(item);
+
         Client.Save(items, "Split lines from Job Requi Item Review Dashboard");
-        MessageWindow.ShowMessage("Line split - original line Qty is now " + item.Qty + ". New line Qty is " + newItem.Qty, "Success");
+        MessageWindow.ShowMessage($"Line split - original line Qty is now {item.Qty}. New line Qty is {newItem.Qty}", "Lines split");
 
         Refresh(false, true);
     }
@@ -216,8 +220,8 @@ public class JobRequisitionReviewGrid : DynamicDataGrid<JobRequisitionItem>
         var item = row.ToObject<JobRequisitionItem>();
         if (CheckValidAction(item))
         {
-            int units = Convert.ToInt32(item.Qty);
-            if (NumberEdit.Execute("Enter amount to split", 1, units, ref units))
+            var units = item.Qty - item.InStock;
+            if(DoubleEdit.Execute("Enter amount to split", 1, units, ref units))
             {
                 SplitLine(item, item.Qty - units, units, "Line split");
             }

+ 21 - 10
prs.desktop/Panels/Products/Reservation Management/StockSelectionPage.xaml

@@ -6,9 +6,10 @@
         xmlns:local="clr-namespace:PRSDesktop"
         xmlns:wpf="clr-namespace:InABox.Wpf;assembly=InABox.Wpf"
         mc:Ignorable="d" Title="Requisition Stock for Line"
-        Height="500" Width="1400">
+        Height="500" Width="1400"
+                    x:Name="Window">
 
-    <Grid>
+    <Grid DataContext="{Binding ElementName=Window}">
         <Grid.RowDefinitions>
             <RowDefinition Height="auto"/>
             <RowDefinition Height="auto"/>
@@ -41,9 +42,11 @@
                        VerticalAlignment="Center" FontWeight="DemiBold" FontSize="14" TextWrapping="Wrap"/>
         </Grid>
 
-        <ListView Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" x:Name="listView" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Center" Margin="0">
+        <ListView x:Name="listView" ItemsSource="{Binding ViewModels}"
+                  Grid.Row="2" Margin="0"
+                  HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Center">
             <ListView.ItemTemplate>
-                <DataTemplate>
+                <DataTemplate DataType="local:StockSelectionViewModel">
                     <Grid>
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="0.4*"/>
@@ -74,11 +77,19 @@
                         </Border>
 
 
-                        <Button Grid.Column="4" Content="-" Width="60"  Height="25" Click="Minus_Click" FontWeight="Bold"/>
-                        <TextBox Grid.Column="5" Width="60"  Height="25" TextChanged="TextBox_TextChanged" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
-                                 HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding ChosenUnits}"/>
-                        <Button Grid.Column="6" Content="+"  Width="60"  Height="25" Click="Plus_Click" FontWeight="Bold"/>
-                        <Button Grid.Column="7" Content="All" Width="60"  Height="25" Click="All_Clicked"/>
+                        <Button Grid.Column="4" Content="-" Width="60"  Height="25" Click="Minus_Click" FontWeight="Bold"
+                                Tag="{Binding}"/>
+                        <TextBox Grid.Column="5" Width="60" Height="25"
+                                 TextChanged="TextBox_TextChanged"
+                                 HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
+                                 HorizontalAlignment="Center" VerticalAlignment="Center"
+                                 Text="{Binding ChosenUnits}"
+                                 Tag="{Binding}"/>
+                        
+                        <Button Grid.Column="6" Content="+"  Width="60"  Height="25" Click="Plus_Click" FontWeight="Bold"
+                                Tag="{Binding}"/>
+                        <Button Grid.Column="7" Content="All" Width="60"  Height="25" Click="All_Clicked"
+                                Tag="{Binding}"/>
 
                     </Grid>
                 </DataTemplate>
@@ -89,7 +100,7 @@
         <StackPanel Grid.Row="3"  Orientation="Horizontal" HorizontalAlignment="Right">
 
             <Button Name="okButton" Click="SaveButton_Click" Margin="5" FontSize="13" Width="90" Height="30"
-                    IsDefault="True" Content="Save"/>
+                    IsDefault="True" Content="Save" IsEnabled="False"/>
 
             <Button Name="cancelButton" IsCancel="True" Margin="5" FontSize="13" Width="90" Height="30"
                     Click="Cancel_Click" Content="Cancel"/>

+ 152 - 190
prs.desktop/Panels/Products/Reservation Management/StockSelectionPage.xaml.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Linq;
+using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
@@ -13,232 +14,193 @@ using InABox.Core;
 using InABox.Wpf;
 using javax.swing;
 
-namespace PRSDesktop
+namespace PRSDesktop;
+
+/// <summary>
+/// Interaction logic for JobRequisitionStockSelectionPage.xaml
+/// </summary>
+public partial class StockSelectionPage : ThemableWindow, INotifyPropertyChanged
 {
-    /// <summary>
-    /// Interaction logic for JobRequisitionStockSelectionPage.xaml
-    /// </summary>
-    public partial class StockSelectionPage : ThemableWindow
-    {
-        ObservableCollection<StockSelectionViewModel> ViewModels = new ObservableCollection<StockSelectionViewModel>();
-        JobRequisitionItem Item = new JobRequisitionItem();
-        public Guid EmpID { get; set; }
-        public Guid IssuingJobID { get; set; }
-        public bool Allocated = false;
-        PageMode _mode;
-
-        enum PageMode 
-        {
-            Reservation,
-            Picking
-        }
-        public StockSelectionPage(IEnumerable<StockHolding> holdings, JobRequisitionItem item, Job issuingJob, bool requisitioned = false)
-        {
-            InitializeComponent();
-
-            _mode = PageMode.Reservation;
-
-            Item = item;
-            IssuingJobID = issuingJob.ID;
-            jobLbl.Text = "Taking stock from Job: " + issuingJob.Name + " (" + issuingJob.JobNumber + ")";
-            foreach (var holding in holdings)
-            {
-                ViewModels.Add(new StockSelectionViewModel
-                {
-                    Location = holding.Location.Description,
-                    Area = holding.Location.Area.Description,
-                    Units = holding.Units,
-                    Style = holding.Style.Description,
-                    Holding = holding,
-                }) ;
-            }
-            listView.ItemsSource = ViewModels;
-
-            Task.Run(() =>
-            {
-                EmpID = new Client<Employee>().Query(new Filter<Employee>(x => x.UserLink.ID).IsEqualTo(ClientFactory.UserGuid), new Columns<Employee>(x => x.ID)).Rows.FirstOrDefault().Get<Employee, Guid>(x => x.ID);
-            });
-
-            if (requisitioned)
-            {
-                availableUnitsLbl.Text = "Allocated";
-                listView.IsEnabled = false;
-                okButton.IsEnabled = false;
-            }
-        }
+    public ObservableCollection<StockSelectionViewModel> ViewModels { get; } = new ObservableCollection<StockSelectionViewModel>();
 
-        private void SaveButton_Click(object sender, RoutedEventArgs e)
-        {
-            var models = ViewModels.Where(x => x.ChosenUnits > 0);
+    private readonly JobRequisitionItem Item;
 
-            double total = 0;
-            foreach (var model in models)
-            {
-                CreateStockMovements(model);
-                total = total + model.ChosenUnits;
-            }
+    public Guid IssuingJobID { get; set; }
 
-            if (Item.Qty > total)
-            {
-                SplitLine(Item, total, Item.Qty - total);
-            }
+    public bool Allocated = false;
 
-            DialogResult = true;
+    public double TotalChosen => ViewModels.Sum(x => x.ChosenUnits);
+
+    public StockSelectionPage(IEnumerable<StockHolding> holdings, JobRequisitionItem item, Job issuingJob, bool requisitioned = false)
+    {
+        Item = item;
+        foreach (var holding in holdings)
+        {
+            ViewModels.Add(new StockSelectionViewModel(holding));
         }
 
-        private void CreateStockMovements(StockSelectionViewModel model)
+        InitializeComponent();
+
+        IssuingJobID = issuingJob.ID;
+        jobLbl.Text = $"Taking stock from Job: {issuingJob.Name} ({issuingJob.JobNumber})";
+
+        if (requisitioned)
         {
-            var batch = new StockMovementBatch
-            {
-                TimeStamp = DateTime.Now,
-                Type = StockMovementBatchType.Transfer,
-                Notes = "Allocated to Job " + Item.Job.JobNumber
-            };
-            new Client<StockMovementBatch>().Save(batch, "Created for requisitioning stock");
-
-            var issuing = CreateBaseMovement(model, batch.ID);
-            issuing.Job.ID = IssuingJobID;
-            issuing.Issued = model.ChosenUnits;
-            issuing.Type = StockMovementType.TransferOut;
-
-            var receiving = CreateBaseMovement(model, batch.ID);
-            receiving.Job.ID = Item.Job.ID;
-            receiving.Received = model.ChosenUnits;
-            receiving.JobRequisitionItem.ID = Item.ID;
-            receiving.Type = StockMovementType.TransferIn;
-            receiving.Transaction = issuing.Transaction;
-
-            Client.Save(new StockMovement[] { issuing, receiving }, "Created from Reservation Management Screen");
+            availableUnitsLbl.Text = "Allocated";
+            listView.IsEnabled = false;
+            okButton.IsEnabled = false;
         }
+    }
 
-        private StockMovement CreateBaseMovement(StockSelectionViewModel model, Guid batchid)
+    private void SaveButton_Click(object sender, RoutedEventArgs e)
+    {
+        if(TotalChosen <= 0)
         {
-            var mvt = new StockMovement();
-            mvt.Style.ID = model.Holding.Style.ID;
-            mvt.Location.ID = model.Holding.Location.ID;
-            mvt.Dimensions.CopyFrom(model.Holding.Dimensions);
-            mvt.Batch.ID = batchid;
-            mvt.Employee.ID = EmpID;
-            mvt.IsTransfer = true;
-            mvt.Date = DateTime.Now;
-            mvt.Product.ID = Item.Product.ID;
-            mvt.Notes = "Reservation Management Screen - allocating to Job" + Item.Job.JobNumber + " for Requisition Line";
-
-            return mvt;
+            MessageWindow.ShowMessage("Please select at least from at least one holding to reserve.", "Error", image: MessageWindow.WarningImage);
+            return;
         }
 
-        private void Cancel_Click(object sender, RoutedEventArgs e)
+        var models = ViewModels.Where(x => x.ChosenUnits > 0);
+
+        foreach (var model in models)
         {
-            DialogResult = false;
+            CreateStockMovements(model);
         }
 
-        private void TextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
+        DialogResult = true;
+    }
+
+    private void CreateStockMovements(StockSelectionViewModel model)
+    {
+        var batch = new StockMovementBatch
         {
-            var box = sender as TextBox;
-            if (!string.IsNullOrWhiteSpace(box.Text))
-            {
-                if (double.TryParse(box.Text, out double value) == false)
-                    box.Text = "0";
+            TimeStamp = DateTime.Now,
+            Type = StockMovementBatchType.Transfer,
+            Notes = $"Allocated to Job {Item.Job.JobNumber}"
+        };
+        Client.Save(batch, "Created for requisitioning stock");
+
+        var issuing = CreateBaseMovement(model, batch.ID);
+        issuing.Job.ID = IssuingJobID;
+        issuing.Issued = model.ChosenUnits;
+        issuing.Type = StockMovementType.TransferOut;
+
+        var receiving = CreateBaseMovement(model, batch.ID);
+        receiving.Job.ID = Item.Job.ID;
+        receiving.Received = model.ChosenUnits;
+        receiving.JobRequisitionItem.ID = Item.ID;
+        receiving.Type = StockMovementType.TransferIn;
+        receiving.Transaction = issuing.Transaction;
+
+        Client.Save(new StockMovement[] { issuing, receiving }, "Created from Reservation Management Screen");
+    }
 
-                else
-                {
-                    var holdingUnits = (box.DataContext as StockSelectionViewModel).Holding.Units;
+    private StockMovement CreateBaseMovement(StockSelectionViewModel model, Guid batchid)
+    {
+        var mvt = new StockMovement();
+
+        mvt.Style.ID = model.Holding.Style.ID;
+        mvt.Location.ID = model.Holding.Location.ID;
+        mvt.Dimensions.CopyFrom(model.Holding.Dimensions);
+        mvt.Batch.ID = batchid;
+        mvt.Employee.ID = App.EmployeeID;
+        mvt.IsTransfer = true;
+        mvt.Date = DateTime.Now;
+        mvt.Product.ID = Item.Product.ID;
+        mvt.Notes = $"Reservation Management Screen - allocating to Job {Item.Job.JobNumber} for Requisition Line";
+
+        return mvt;
+    }
 
-                    if (holdingUnits >= value && value >= 0)
-                        (box.DataContext as StockSelectionViewModel).ChosenUnits = value;
-                    else
-                        (box.DataContext as StockSelectionViewModel).ChosenUnits = holdingUnits;
+    private void Cancel_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = false;
+    }
 
-                }
+    private void TextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
+    {
+        if (sender is not TextBox box || box.Tag is not StockSelectionViewModel model) return;
 
-            }
-        }
+        if (box.Text.IsNullOrWhiteSpace()) return;
 
-        private void All_Clicked(object sender, RoutedEventArgs e)
+        if (double.TryParse(box.Text, out double value))
         {
-            var holdingUnits = ((sender as Button).DataContext as StockSelectionViewModel).Holding.Units;
-            ((sender as Button).DataContext as StockSelectionViewModel).ChosenUnits = holdingUnits;
+            model.ChosenUnits = value;
+            OnPropertyChanged(nameof(TotalChosen));
         }
-
-        private void Minus_Click(object sender, RoutedEventArgs e)
+        else
         {
-            var model = (sender as Button).DataContext as StockSelectionViewModel;
-
-            if (model.ChosenUnits > 0)
-                model.ChosenUnits--;
+            // This'll call the event handler again.
+            box.Text = "0";
         }
+    }
 
-        private void Plus_Click(object sender, RoutedEventArgs e)
-        {
-            var holdingUnits = ((sender as Button).DataContext as StockSelectionViewModel).Holding.Units;
-            var model = (sender as Button).DataContext as StockSelectionViewModel;
-            if (model.ChosenUnits < holdingUnits)
-                model.ChosenUnits++;
-        }
+    private void All_Clicked(object sender, RoutedEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not StockSelectionViewModel model) return;
 
-        private void SplitLine(JobRequisitionItem item, double oldItemQty, double newItemQty)
-        {
-            List<JobRequisitionItem> items = new List<JobRequisitionItem>();
-            JobRequisitionItem newItem = new JobRequisitionItem();
-            newItem.Requisition.ID = item.Requisition.ID;
-            newItem.Requisition.Job.ID = item.Requisition.Job.ID;
-            newItem.Requisition.Job.JobNumber = item.Requisition.Job.JobNumber;
-            newItem.Requisition.Job.Name = item.Requisition.Job.Name;
-            newItem.Job.ID = item.Job.ID;
-            newItem.Product.ID = item.Product.ID;
-            newItem.Product.Name = item.Product.Name;
-            newItem.Product.Code = item.Product.Code;
-            newItem.Product.Group.ID = item.Product.Group.ID;
-            newItem.Product.Group.Description = item.Product.Group.Description;
-            newItem.Dimensions.CopyFrom(item.Dimensions);
-            newItem.Style.ID = item.Style.ID;
-            newItem.Style.Description = item.Style.Description;
-            newItem.Style.Code = item.Style.Code;
-            newItem.Notes = item.Notes + Environment.NewLine + "Line split from original line when reserving";
-            item.Notes = newItem.Notes;
-
-            item.Qty = oldItemQty;
-            newItem.Qty = newItemQty;
-
-            items.Add(newItem);
-            items.Add(item);
-            new Client<JobRequisitionItem>().Save(items, "Split lines from Job Requi Item Review Dashboard");
-            MessageBox.Show("Requisition Line split due to " + oldItemQty + " stock selected of " + (oldItemQty + newItemQty) + " required.");           
-        }
+        model.ChosenUnits = model.Holding.Units;
+        OnPropertyChanged(nameof(TotalChosen));
     }
 
-    public class StockSelectionViewModel : INotifyPropertyChanged
+    private void Minus_Click(object sender, RoutedEventArgs e)
     {
-        public event PropertyChangedEventHandler? PropertyChanged;
-        public string Location { get; set; }
-        public string Area { get; set; }
-        public string Style { get; set; }
-        public double Units { get; set; }
-
-        private double chosenUnits;
-        public double ChosenUnits
-        {
-            get => chosenUnits;
-            set
-            {
-                chosenUnits = value;
-                OnPropertyChanged("ChosenUnits");
-            }
-        }
-        public StockHolding Holding { get; set; }
+        if (sender is not FrameworkElement element || element.Tag is not StockSelectionViewModel model) return;
+        
+        model.ChosenUnits--;
+        OnPropertyChanged(nameof(TotalChosen));
+    }
+
+    private void Plus_Click(object sender, RoutedEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not StockSelectionViewModel model) return;
+
+        model.ChosenUnits++;
+        OnPropertyChanged(nameof(TotalChosen));
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
 
-        public StockSelectionViewModel()
+    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+        if(propertyName == nameof(TotalChosen))
         {
-            Location = "";
-            Area = "";
-            Style = "";
-            Units = 0;
-            ChosenUnits = 0;
+            okButton.IsEnabled = TotalChosen > 0;
         }
+    }
+}
 
-        protected virtual void OnPropertyChanged(string propertyName)
+public class StockSelectionViewModel : INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler? PropertyChanged;
+    public string Location => Holding.Location.Description;
+    public string Area => Holding.Location.Area.Description;
+    public string Style => Holding.Style.Description;
+    public double Units => Holding.Units;
+
+    private double _chosenUnits;
+    public double ChosenUnits
+    {
+        get => _chosenUnits;
+        set
         {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+            _chosenUnits = Math.Clamp(value, 0, Holding.Units);
+            OnPropertyChanged();
         }
     }
+
+    public StockHolding Holding { get; set; }
+
+    public StockSelectionViewModel(StockHolding holding)
+    {
+        Holding = holding;
+        ChosenUnits = 0;
+    }
+
+    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
 }