Prechádzať zdrojové kódy

Added ability to create orders and picking lists from Reservation Managment Screen
Added ability to substitute sizes and styles on Reservation Management Screen
Added "Picking List Requested" Column to reservation Management Screen

frogsoftware 11 mesiacov pred
rodič
commit
7ee49a4552

+ 15 - 13
prs.classes/Entities/Job/Requisitions/JobRequisitionItem.cs

@@ -139,7 +139,22 @@ namespace Comal.Classes
         public FormulaOperator Operator => FormulaOperator.Subtract;
         public FormulaType Type => FormulaType.Virtual;
     }
+    
+    public class JobRequisitionItemPickRequestedAggregate : CoreAggregate<JobRequisitionItem, RequisitionItem, double>
+    {
+        public override Expression<Func<RequisitionItem, double>> Aggregate => x => x.Quantity;
 
+        public override Dictionary<Expression<Func<RequisitionItem, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => 
+            new Dictionary<Expression<Func<RequisitionItem, object?>>, Expression<Func<JobRequisitionItem, object?>>>
+            {
+                { x => x.SourceJRI.ID, x => x.ID },
+            };
+        
+        public override Filter<RequisitionItem>? Filter => new Filter<RequisitionItem>(x => x.RequisitionLink.StockUpdated).IsEqualTo(DateTime.MinValue);
+        
+        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
+    }
+    
     public class JobRequisitionItemIssuedAggregate : CoreAggregate<JobRequisitionItem, StockMovement, double>
     {
         public override Expression<Func<StockMovement, double>> Aggregate => x => x.Issued;
@@ -155,19 +170,6 @@ namespace Comal.Classes
         public override AggregateCalculation Calculation => AggregateCalculation.Sum;
     }
     
-    public class JobRequisitionItemPickRequestedAggregate : CoreAggregate<JobRequisitionItem, RequisitionItem, double>
-    {
-        public override Expression<Func<RequisitionItem, double>> Aggregate => x => x.Quantity;
-
-        public override Dictionary<Expression<Func<RequisitionItem, object?>>, Expression<Func<JobRequisitionItem, object?>>> Links => 
-            new Dictionary<Expression<Func<RequisitionItem, object?>>, Expression<Func<JobRequisitionItem, object?>>>
-            {
-                { x => x.SourceJRI.ID, x => x.ID },
-            };
-        
-        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
-    }
-
     public interface IJobRequisitionItem : IEntity
     {
 

+ 3 - 0
prs.classes/Entities/Requisition/RequisitionLink.cs

@@ -21,6 +21,9 @@ namespace Comal.Classes
 
         [TimestampEditor(Editable = Editable.Hidden)]
         public DateTime Filled { get; set; }
+        
+        [TimestampEditor(Editable = Editable.Hidden)]
+        public DateTime StockUpdated { get; set; }
 
         [TimestampEditor(Editable = Editable.Hidden)]
         public DateTime Archived { get; set; }

+ 76 - 0
prs.desktop/Panels/Jobs/Requisitions/JobRequisitionItemGrid.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Threading;
@@ -40,6 +41,7 @@ internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMa
             : new Filter<JobRequisitionItem>().None();
 
     private Button CreatePickingList;
+    private Button CreateOrder;
 
     public JobRequisitionItemGrid()
     {
@@ -63,6 +65,7 @@ internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMa
         
         HiddenColumns.Add(x=>x.PickRequested);
         
+        CreateOrder = AddButton("Create Order", PRSDesktop.Resources.purchase.AsBitmapImage(), DoCreatePurchaseOrder);
         CreatePickingList = AddButton("Create Picking List", PRSDesktop.Resources.trolley.AsBitmapImage(), DoCreatePickingList);
     }
 
@@ -71,7 +74,78 @@ internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMa
         base.SelectItems(rows);
         CreatePickingList.IsEnabled = rows?.Any() == true;
     }
+    
+    #region CreatePurchaseOrder
+    
+    private bool DoCreatePurchaseOrder(Button button, CoreRow[]? rows)
+    {
+        if (rows?.Any() != true)
+            return false;
+
+        MultiSelectDialog<Supplier> dlg = new MultiSelectDialog<Supplier>(
+            LookupFactory.DefineFilter<Supplier>(),
+            Columns.None<Supplier>()
+                .Add(x => x.ID)
+                .Add(x => x.Code)
+                .Add(x => x.Name),
+            false
+        );
+        
+        var _po = new PurchaseOrder();
+        if (dlg.ShowDialog())
+        {
+            Progress.ShowModal("Creating Purchase Order", progress =>
+            {
+                
+                _po.Description = "Created from Job Requisition Screen" + System.Environment.NewLine;
+                _po.RaisedBy.ID = App.EmployeeID;
+                _po.SupplierLink.ID = dlg.IDs().First();
+                Client.Save(_po, "Created From Requisition Screen");
+                
+                progress.Report("Creating Order Items");
+                Dictionary<Guid,PurchaseOrderItem> _pois = new Dictionary<Guid,PurchaseOrderItem>();
+                foreach (CoreRow row in SelectedRows)
+                {
+                    JobRequisitionItem _jri = row.ToObject<JobRequisitionItem>();
+                    PurchaseOrderItem _poi = new PurchaseOrderItem();
+                    _poi.PurchaseOrderLink.ID = _po.ID;
+                    _poi.Product.ID = _jri.Product.ID;
+                    _poi.Product.Code = _jri.Product.Code;
+                    _poi.Product.Name = _jri.Product.Name;               
+                    _poi.Qty = _jri.Qty;
+                    _poi.Dimensions.CopyFrom(_jri.Dimensions);
+                    _poi.Dimensions.Value = _jri.Dimensions.Value;
+                    _poi.Style.ID = _jri.Style.ID;
+                    _poi.Style.Code = _jri.Style.Code;
+                    _poi.Style.Description = _jri.Style.Description;
+                    _poi.Job.ID = _jri.Job.ID;
+                    _poi.Dimensions.UnitSize = _jri.Dimensions.UnitSize;
+                    _poi.Description = _jri.Product.Name + " (" + _jri.Dimensions.ToString() + ")";
+                    _pois[_jri.ID] = _poi;
+                }
+                Client.Save(_pois.Values, "Created From Requisition Screen");
 
+                List<JobRequisitionItemPurchaseOrderItem> _jripois = new();
+                foreach (var _poi in _pois)
+                {
+                    var _jripoi = new JobRequisitionItemPurchaseOrderItem();
+                    _jripoi.JobRequisitionItem.ID = _poi.Key;
+                    _jripoi.PurchaseOrderItem.ID = _poi.Value.ID;
+                    _jripois.Add(_jripoi);
+                }
+                Client.Save(_jripois, "Created From Requisition Screen");
+
+            });
+
+        }
+        new SupplierPurchaseOrders().EditItems(new[] { _po });
+        return true;
+    }
+    
+    #endregion
+
+    #region CreatePickingList
+    
     private bool DoCreatePickingList(Button button, CoreRow[]? rows)
     {
         if (rows?.Any() != true)
@@ -119,6 +193,8 @@ internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMa
         }
         return true;
     }
+    
+    #endregion
 
     // private void CheckVisibility()
     // {

+ 190 - 4
prs.desktop/Panels/Reservation Management/ReservationManagementItemGrid.cs

@@ -7,10 +7,12 @@ using InABox.Wpf;
 using InABox.WPF;
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Drawing;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Threading;
+using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media;
@@ -24,9 +26,12 @@ public delegate void GridRefresh();
 
 public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
 {
-    private readonly ReservationManagementUserSettings _userSettings = new ReservationManagementUserSettings();
 
+    public ReservationManagementUserSettings UserSettings { get; private set; }
+    
     private Button ArchiveButton;
+    private Button CreatePickingList;
+    private Button CreateOrder;
     
     public bool ShowColors { get; set; } 
     
@@ -107,12 +112,13 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
     private DynamicActionColumn TreatmentRequiredColumn;
     private DynamicActionColumn TreatmentOnOrderColumn;
     private DynamicActionColumn AllocatedColumn;
+    private DynamicActionColumn PickRequestedColumn;
     private DynamicActionColumn IssuedColumn;
 
     public ReservationManagementItemGrid()
     {
-        _userSettings = new UserConfiguration<ReservationManagementUserSettings>().Load();
-        FilterComponent.SetSettings(_userSettings.Filters, false);
+        UserSettings = new UserConfiguration<ReservationManagementUserSettings>().Load();
+        FilterComponent.SetSettings(UserSettings.Filters, false);
 
         HiddenColumns.Add(x => x.ID);
 
@@ -122,6 +128,7 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
         HiddenColumns.Add(x => x.TreatmentOnOrder);
         HiddenColumns.Add(x => x.TreatmentRequired);
         HiddenColumns.Add(x => x.Allocated);
+        HiddenColumns.Add(x => x.PickRequested);
         HiddenColumns.Add(x => x.Issued);
 
         HiddenColumns.Add(x => x.Product.ID);
@@ -166,6 +173,7 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
         TreatmentRequiredColumn = AddDoubleColumn(x => x.TreatmentRequired, "Req.");
         TreatmentOnOrderColumn = AddDoubleColumn(x => x.TreatmentOnOrder, "Ord.");
         AllocatedColumn = AddDoubleColumn(x => x.Allocated, "Stk.");
+        PickRequestedColumn = AddDoubleColumn(x => x.PickRequested, "P/L.");
         IssuedColumn = AddDoubleColumn(x => x.Issued, "Iss.");
 
         if (Security.CanEdit<JobRequisitionItem>())
@@ -174,10 +182,187 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
         ColumnsTag = "JobRequisitionReview";
 
         FilterComponent.OnFiltersSelected += GridOnFilterSelected;
+        
+        CreateOrder = AddButton("Create Order", PRSDesktop.Resources.purchase.AsBitmapImage(), DoCreatePurchaseOrder);
+        CreateOrder.IsEnabled = false;
+        
+        CreatePickingList = AddButton("Create Picking List", PRSDesktop.Resources.trolley.AsBitmapImage(), DoCreatePickingList);
+        CreatePickingList.IsEnabled = false;
 
         ArchiveButton = AddButton("Archive", PRSDesktop.Resources.archive.AsBitmapImage(), ArchiveButton_Clicked);
         ArchiveButton.IsEnabled = false;
     }
+    
+    #region CreatePurchaseOrder
+    
+    private bool DoCreatePurchaseOrder(Button button, CoreRow[]? rows)
+    {
+        if (rows?.Any() != true)
+            return false;
+
+        MultiSelectDialog<Supplier> dlg = new MultiSelectDialog<Supplier>(
+            LookupFactory.DefineFilter<Supplier>(),
+            Columns.None<Supplier>()
+                .Add(x => x.ID)
+                .Add(x => x.Code)
+                .Add(x => x.Name),
+            false
+        );
+        
+        var _po = new PurchaseOrder();
+        if (dlg.ShowDialog())
+        {
+            Progress.ShowModal("Creating Purchase Order", progress =>
+            {
+                
+                _po.Description = "Created from Job Requisition Screen" + System.Environment.NewLine;
+                _po.RaisedBy.ID = App.EmployeeID;
+                _po.SupplierLink.ID = dlg.IDs().First();
+                Client.Save(_po, "Created From Requisition Screen");
+                
+                progress.Report("Creating Order Items");
+                Dictionary<Guid,PurchaseOrderItem> _pois = new Dictionary<Guid,PurchaseOrderItem>();
+                foreach (CoreRow row in SelectedRows)
+                {
+                    JobRequisitionItem _jri = row.ToObject<JobRequisitionItem>();
+                    PurchaseOrderItem _poi = new PurchaseOrderItem();
+                    _poi.PurchaseOrderLink.ID = _po.ID;
+                    _poi.Product.ID = _jri.Product.ID;
+                    _poi.Product.Code = _jri.Product.Code;
+                    _poi.Product.Name = _jri.Product.Name;               
+                    _poi.Qty = _jri.Qty;
+                    _poi.Dimensions.CopyFrom(_jri.Dimensions);
+                    _poi.Dimensions.Value = _jri.Dimensions.Value;
+                    _poi.Style.ID = _jri.Style.ID;
+                    _poi.Style.Code = _jri.Style.Code;
+                    _poi.Style.Description = _jri.Style.Description;
+                    _poi.Job.ID = _jri.Job.ID;
+                    _poi.Dimensions.UnitSize = _jri.Dimensions.UnitSize;
+                    _poi.Description = _jri.Product.Name + " (" + _jri.Dimensions.ToString() + ")";
+                    _pois[_jri.ID] = _poi;
+                }
+                Client.Save(_pois.Values, "Created From Requisition Screen");
+
+                List<JobRequisitionItemPurchaseOrderItem> _jripois = new();
+                foreach (var _poi in _pois)
+                {
+                    var _jripoi = new JobRequisitionItemPurchaseOrderItem();
+                    _jripoi.JobRequisitionItem.ID = _poi.Key;
+                    _jripoi.PurchaseOrderItem.ID = _poi.Value.ID;
+                    _jripois.Add(_jripoi);
+                }
+                Client.Save(_jripois, "Created From Requisition Screen");
+
+            });
+
+        }
+        new SupplierPurchaseOrders().EditItems(new[] { _po });
+        return true;
+    }
+    
+    #endregion
+
+    #region CreatePickingList
+    
+    private bool DoCreatePickingList(Button button, CoreRow[]? rows)
+    {
+        if (rows?.Any() != true)
+            return false;
+        if (rows.All(r =>
+                r.Get<JobRequisitionItem, double>(x => x.PickRequested)
+                    .IsEffectivelyEqual(r.Get<JobRequisitionItem, double>(x => x.Qty))))
+        {
+            MessageWindow.ShowMessage("All Items have been picked!","Error");
+            return false;
+        }
+        
+        var _jris = rows.ToObjects<JobRequisitionItem>()
+            .GroupBy(x=>new Tuple<Guid,String,String>(x.Requisition.Job.ID, x.Requisition.Job.JobNumber, x.Requisition.Job.Name))
+            .ToArray();
+        
+        List<Requisition> _pls = new List<Requisition>();
+
+        Progress.ShowModal("Creating Picking Lists", progress =>
+        {
+            Task<StockMovement[]> _movementquery = Task.Run(
+                () => Client.Query(
+                    new Filter<StockMovement>(x => x.JobRequisitionItem.ID)
+                        .InList(rows.Select(r => r.Get<JobRequisitionItem,Guid>(c=>c.ID)).ToArray())
+                    ).ToObjects<StockMovement>()
+                    .ToArray()
+                );
+            
+            foreach (var _job in _jris)
+            {
+                var _pl = new Requisition();
+                _pl.JobLink.ID = _job.Key.Item1;
+                _pl.JobLink.JobNumber = _job.Key.Item2;
+                _pl.JobLink.Name = _job.Key.Item3;
+                _pl.RequestedBy.ID = App.EmployeeID;
+                _pl.Notes = new string[] { $"Items requested by {App.EmployeeName}" };
+                _pls.Add(_pl);
+            }
+            Client.Save(_pls, "Created from Reservation Management Screen");
+
+            progress.Report("Retrieving Allocations");
+            _movementquery.Wait();
+
+            progress.Report("Creating Picking List Items");
+            List<RequisitionItem> _plitems = new List<RequisitionItem>();
+            foreach (var _pl in _pls)
+            {
+                var _pljris = _jris.First(x => x.Key.Item1 == _pl.JobLink.ID);
+                foreach (var _pljri in _pljris)
+                {
+                    double qtyrequired = _pljri.Qty;
+                    var _locations = _movementquery.Result.Where(x => x.JobRequisitionItem.ID == _pljri.ID)
+                        .GroupBy(x => x.Location.ID);
+                    foreach (var _location in _locations)
+                    {
+                        if (!qtyrequired.IsEffectivelyEqual(0.0))
+                        {
+                            var _plitem = new RequisitionItem();
+                            _plitem.RequisitionLink.CopyFrom(_pl);
+                            _plitem.JobRequisitionItem.CopyFrom(_pljri);
+                            _plitem.SourceJRI.CopyFrom(_pljri);
+                            _plitem.Product.CopyFrom(_pljri.Product);
+                            _plitem.Style.CopyFrom(_pljri.Style);
+                            _plitem.Dimensions.CopyFrom(_pljri.Dimensions);
+                            _plitem.Location.ID = _location.Key;
+                            _plitem.Quantity = Math.Min(qtyrequired, _location.Sum(x => x.Units));
+                            _plitem.ActualQuantity = _plitem.Quantity;
+                            qtyrequired -= _plitem.Quantity;
+                            _plitems.Add(_plitem);
+                        }
+                    }
+
+                    if (!qtyrequired.IsEffectivelyEqual(0.0))
+                    {
+                        var _plitem = new RequisitionItem();
+                        _plitem.RequisitionLink.CopyFrom(_pl);
+                        _plitem.JobRequisitionItem.CopyFrom(_pljri);
+                        _plitem.Product.CopyFrom(_pljri.Product);
+                        _plitem.Style.CopyFrom(_pljri.Style);
+                        _plitem.Dimensions.CopyFrom(_pljri.Dimensions);
+                        _plitem.Quantity = qtyrequired;
+                        _plitems.Add(_plitem);
+                    }
+
+
+                }
+            }
+            Client.Save(_plitems,"Created from Reservation Management Screen");
+            
+        });
+        
+        MessageWindow.ShowMessage(
+            $"Created the following Picking Lists:\n- {string.Join("\n- ",_pls.Select(x=>$"{x.Number}: {x.JobLink.JobNumber} {x.JobLink.Name}"))}",
+            "Picking Lists Created");
+
+        return true;
+    }
+    
+    #endregion
 
     private DynamicActionColumn AddDoubleColumn(Expression<Func<JobRequisitionItem, object>> property, string header)
     {
@@ -238,10 +423,11 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
     protected override void SelectItems(CoreRow[]? rows)
     {
         base.SelectItems(rows);
-
         if(rows?.Any() == true)
         {
             ArchiveButton.IsEnabled = true;
+            CreateOrder.IsEnabled = true;
+            CreatePickingList.IsEnabled = true;
         }
     }
 

+ 39 - 2
prs.desktop/Panels/Reservation Management/ReservationManagementPanel.xaml

@@ -4,6 +4,7 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:PRSDesktop" xmlns:dynamicgrid="clr-namespace:InABox.DynamicGrid;assembly=InABox.Wpf"
+             xmlns:syncfusion="http://schemas.syncfusion.com/wpf"
              mc:Ignorable="d" 
              d:DesignHeight="450" d:DesignWidth="900"
              x:Name="Panel">
@@ -22,7 +23,7 @@
         </dynamicgrid:DynamicSplitPanel.Header>
        
         <dynamicgrid:DynamicSplitPanel.Master>
-            <local:ReservationManagementItemGrid  x:Name="JobRequiItems" Margin="0,2,0,0" ShowColors="True"/>
+            <local:ReservationManagementItemGrid  x:Name="JobRequiItems" Margin="0,2,0,0" ShowColors="True" OnSelectItem="JobRequiItems_OnOnSelectItem"/>
         </dynamicgrid:DynamicSplitPanel.Master>
 
         <dynamicgrid:DynamicSplitPanel.Detail>
@@ -56,11 +57,47 @@
                         <ColumnDefinition Width="*" x:Name="reserveCol"/>
                         <ColumnDefinition Width="0" x:Name="purchaseCol"/>
                     </Grid.ColumnDefinitions>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="*"/>
+                        <RowDefinition Height="0" x:Name="SubstitutionSplitterRow" />
+                        <RowDefinition Height="0" x:Name="SubstitutionRow" />
+                        <RowDefinition Height="0" x:Name="SubstitutionButtonRow" />
+                    </Grid.RowDefinitions>
                     <local:ReservationManagementHoldingsGrid Grid.Column="0" x:Name="holdings" Margin="0,2,0,0"/>
-                    <local:ReservationManagementPurchasing Grid.Column="1" x:Name="purchasing" Margin="0,2,0,0" AllowDrop="True" Drop="Purchasing_Drop"/>
+                    <syncfusion:SfGridSplitter Grid.Row="1" Grid.Column="0"
+                                               ResizeBehavior="PreviousAndNext" Height="4" HorizontalAlignment="Stretch"
+                                               Background="Transparent" Template="{StaticResource HorizontalSplitter}">
+
+                        <syncfusion:SfGridSplitter.PreviewStyle>
+                            <Style TargetType="Control">
+                                <Setter Property="Background" Value="Gray" />
+                                <Setter Property="Template">
+                                    <Setter.Value>
+                                        <ControlTemplate TargetType="Control">
+                                            <Grid x:Name="Root" Opacity="0.5">
+                                                <Rectangle Fill="{TemplateBinding Background}" />
+                                            </Grid>
+                                        </ControlTemplate>
+                                    </Setter.Value>
+                                </Setter>
+                            </Style>
+                        </syncfusion:SfGridSplitter.PreviewStyle>
+
+                    </syncfusion:SfGridSplitter>
+                    <local:ReservationManagementSubstitutionGrid Grid.Column="0" Grid.Row="2" x:Name="substitutions" OnSelectItem="Substitutions_OnOnSelectItem"/>
+                    <DockPanel
+                        Grid.Row="3"
+                        Margin="0,2,0,2">
+                        <CheckBox x:Name="SubstitutionFreeStock" DockPanel.Dock="Left" Content="General Stock Only?" VerticalAlignment="Center" Checked="SubstitutionFreeStock_OnChecked" Unchecked="SubstitutionFreeStock_OnChecked" />
+                        <Button DockPanel.Dock="Right" Padding="10,0" Height="30" Content="Select" x:Name="ChangeProduct" Click="ChangeProduct_OnClick" IsEnabled="False" />
+                        <CheckBox x:Name="SubstitutionUpdateStyle" DockPanel.Dock="Left" Content="Change Item Style?" VerticalAlignment="Center" HorizontalAlignment="Center" Checked="SubstitutionUpdateStyle_OnChecked" Unchecked="SubstitutionUpdateStyle_OnChecked"/>
+                    </DockPanel>
+                    <local:ReservationManagementPurchasing Grid.Column="1" Grid.RowSpan="4" x:Name="purchasing" Margin="0,2,0,0" AllowDrop="True" Drop="Purchasing_Drop"/>
                 </Grid>
             </Grid>
         </dynamicgrid:DynamicSplitPanel.Detail>
 
+        
+        
     </dynamicgrid:DynamicSplitPanel>
 </UserControl>

+ 107 - 0
prs.desktop/Panels/Reservation Management/ReservationManagementPanel.xaml.cs

@@ -210,6 +210,9 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
 
         _globalSettings = new GlobalConfiguration<ReservationManagementGlobalSettings>().Load();
 
+        substitutions.FreeStockOnly = JobRequiItems.UserSettings.SubstituteFreeStockOnly;
+        substitutions.UpdateStyle = JobRequiItems.UserSettings.SubstituteUpdateStyle;
+        
         JobRequiItems.DueDateAlert = _globalSettings.DueDateAlert;
         JobRequiItems.DueDateWarning = _globalSettings.DueDateWarning;
         
@@ -666,4 +669,108 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
             }
         }
     }
+
+    private bool _updatingSubstitution = false;
+    
+    private void JobRequiItems_OnOnSelectItem(object sender, DynamicGridSelectionEventArgs e)
+    {
+        _updatingSubstitution = true;
+        try
+        {
+            var row = e.Rows?.SingleOrDefault();
+            var visible =
+                row != null
+                && row.Get<JobRequisitionItem, double>(x => x.InStock).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.TotalOrders).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.OnOrder).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.TreatmentRequired).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.TreatmentOnOrder).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.Allocated).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.PickRequested).IsEffectivelyEqual(0.0)
+                && row.Get<JobRequisitionItem, double>(x => x.Issued).IsEffectivelyEqual(0.0);
+        
+            substitutions.SizeChanged -= Substitutions_OnSizeChanged;
+            SubstitutionSplitterRow.Height = visible ? GridLength.Auto : new GridLength(0.0);
+            SubstitutionRow.Height = visible ? new GridLength(JobRequiItems.UserSettings.SubstitutionGridHeight) : new GridLength(0.0);
+            SubstitutionButtonRow.Height = visible ? GridLength.Auto : new GridLength(0.0);
+            substitutions.SizeChanged += Substitutions_OnSizeChanged;
+            if (visible)
+                substitutions.JRI = row?.ToObject<JobRequisitionItem>() ?? new JobRequisitionItem();
+            SubstitutionFreeStock.IsChecked = JobRequiItems.UserSettings.SubstituteFreeStockOnly;
+            SubstitutionUpdateStyle.IsChecked = JobRequiItems.UserSettings.SubstituteUpdateStyle;
+        }
+        finally
+        {
+            _updatingSubstitution = false;
+        }
+    }
+
+    private void SubstitutionFreeStock_OnChecked(object sender, RoutedEventArgs e)
+    {
+        if (_updatingSubstitution)
+            return;
+        substitutions.FreeStockOnly = SubstitutionFreeStock.IsChecked == true;
+        JobRequiItems.UserSettings.SubstituteFreeStockOnly = substitutions.FreeStockOnly;
+        new UserConfiguration<ReservationManagementUserSettings>().Save(JobRequiItems.UserSettings);
+    }
+
+    private void SubstitutionUpdateStyle_OnChecked(object sender, RoutedEventArgs e)
+    {
+        if (_updatingSubstitution)
+            return;
+        substitutions.UpdateStyle = SubstitutionUpdateStyle.IsChecked == true;
+        JobRequiItems.UserSettings.SubstituteUpdateStyle = substitutions.UpdateStyle;
+        new UserConfiguration<ReservationManagementUserSettings>().Save(JobRequiItems.UserSettings);
+    }
+
+    private bool _sizeChanging = false;
+    
+    private void Substitutions_OnSizeChanged(object sender, SizeChangedEventArgs e)
+    {
+        _sizeChanging = System.Windows.Input.Mouse.LeftButton == MouseButtonState.Pressed;
+    }
+
+    protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
+    {
+        base.OnPreviewMouseUp(e);
+        if (_sizeChanging)
+        {
+            _sizeChanging = false;
+            JobRequiItems.UserSettings.SubstitutionGridHeight = substitutions.ActualHeight;
+            new UserConfiguration<ReservationManagementUserSettings>().Save(JobRequiItems.UserSettings);
+        }
+    }
+
+    private void ChangeProduct_OnClick(object sender, RoutedEventArgs e)
+    {
+        var _holding = substitutions.SelectedRows?.FirstOrDefault()?.ToObject<StockHolding>();
+        var _jrirow = JobRequiItems.SelectedRows.FirstOrDefault();
+        if (_holding == null || _jrirow == null)
+            return;
+        var _jri = _jrirow.ToObject<JobRequisitionItem>();
+        _jri.Dimensions.CopyFrom(_holding.Dimensions, true);
+        if (substitutions.UpdateStyle)
+            _jri.Style.CopyFrom(_holding.Style);
+        Client.Save(_jri,"Substituted by Requisition Management Screen");
+        var _xferout = _holding.CreateMovement();
+        _xferout.Issued = Math.Min(_holding.Available, _jri.Qty);
+        _xferout.Notes = "Substituted by Requisition Management Screen";
+        _xferout.Transaction = Guid.NewGuid();
+        _xferout.Type = StockMovementType.TransferOut;
+        var _xferin = _holding.CreateMovement();
+        _xferin.JobRequisitionItem.CopyFrom(_jri);
+        _xferin.Job.CopyFrom(_jri.Job);
+        _xferin.Received = Math.Min(_holding.Available, _jri.Qty);
+        _xferin.Notes = "Substituted by Requisition Management Screen";
+        _xferin.Transaction = _xferout.Transaction;
+        _xferin.Type = StockMovementType.TransferIn;
+        Client.Save(new[] { _xferout, _xferin}, "Substituted by Requisition Management Screen");
+        JobRequiItems.Refresh(false,true);
+        // Allocate Stock (transfer if required)
+    }
+
+    private void Substitutions_OnOnSelectItem(object sender, DynamicGridSelectionEventArgs e)
+    {
+        ChangeProduct.IsEnabled = e.Rows?.SingleOrDefault() != null;
+    }
 }

+ 113 - 0
prs.desktop/Panels/Reservation Management/ReservationManagementSubstitutionGrid.cs

@@ -0,0 +1,113 @@
+using System;
+using System.Threading;
+using System.Windows.Controls;
+using Comal.Classes;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+
+namespace PRSDesktop;
+
+public class ReservationManagementSubstitutionGrid : DynamicDataGrid<StockHolding>
+{
+    private bool initialized;
+    private JobRequisitionItem? jri;
+    private bool freeStockOnly;
+
+    public JobRequisitionItem? JRI
+    {
+        get => jri;
+        set
+        {
+            jri = value;
+            Refresh(!initialized,true);
+        }
+    }
+
+    public bool FreeStockOnly
+    {
+        get => freeStockOnly;
+        set
+        {
+            freeStockOnly = value;
+            Refresh(!initialized, true);
+        }
+    }
+
+    public bool UpdateStyle { get; set; }
+    
+    private bool SubstituteProduct(CoreRow? row)
+    {
+        return true;
+    }
+    
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        options.Clear();
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        var result = new DynamicGridColumns();
+        result.Add<StockHolding,string>(x => x.Dimensions.UnitSize, 100, "Substitutes", "", Alignment.MiddleCenter);
+        result.Add<StockHolding,string>(x => x.Style.Description, 0, "Style", "", Alignment.MiddleCenter);
+        result.Add<StockHolding,string>(x => x.Location.Code, 100, "Location", "", Alignment.MiddleCenter);
+        result.Add<StockHolding,string>(x => x.Job.JobNumber, 70, "Job", "", Alignment.MiddleCenter);
+        result.Add<StockHolding,double>(x => x.Available, 60, "Avail.", "F2", Alignment.MiddleCenter);
+        return result;
+    }
+
+    protected override void Init()
+    {
+        base.Init();
+        HiddenColumns.Add(x=>x.Job.ID);
+        HiddenColumns.Add(x=>x.Job.JobNumber);
+        HiddenColumns.Add(x=>x.Job.Name);
+        HiddenColumns.Add(x=>x.Style.ID);
+        HiddenColumns.Add(x=>x.Style.Code);
+        HiddenColumns.Add(x=>x.Style.Description);
+        HiddenColumns.Add(x => x.Dimensions.UnitSize);
+        HiddenColumns.Add(x => x.Dimensions.Length);
+        HiddenColumns.Add(x => x.Dimensions.Width);
+        HiddenColumns.Add(x => x.Dimensions.Height);
+        HiddenColumns.Add(x => x.Dimensions.Weight);
+        HiddenColumns.Add(x => x.Dimensions.Quantity);
+        HiddenColumns.Add(x => x.Dimensions.Value);
+        HiddenColumns.Add(x => x.Dimensions.Unit.ID);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasLength);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasHeight);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasWidth);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasWeight);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasQuantity);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Formula);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Format);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Code);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Description);
+        HiddenColumns.Add(x => x.Available);
+    }
+
+    protected override void Reload(Filters<StockHolding> criteria, Columns<StockHolding> columns, ref SortOrder<StockHolding>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        initialized = true;
+        if (jri != null)
+        {
+            criteria.Add(new Filter<StockHolding>(x => x.Product.ID).IsEqualTo(jri.Product.ID)
+                .And(
+                    new Filter<StockHolding>(x => x.Style.ID).IsNotEqualTo(jri.Style.ID)
+                        .Or(x => x.Dimensions.Unit.ID).IsNotEqualTo(jri.Dimensions.Unit.ID)
+                        .Or(x => x.Dimensions.Height).IsNotEqualTo(jri.Dimensions.Height)
+                        .Or(x => x.Dimensions.Width).IsNotEqualTo(jri.Dimensions.Width)
+                        .Or(x => x.Dimensions.Length).IsNotEqualTo(jri.Dimensions.Length)
+                        .Or(x => x.Dimensions.Quantity).IsNotEqualTo(jri.Dimensions.Quantity)
+                        .Or(x => x.Dimensions.Weight).IsNotEqualTo(jri.Dimensions.Weight)
+                )
+            );
+            if (FreeStockOnly)
+                criteria.Add(new Filter<StockHolding>(x => x.Job.ID).IsEqualTo(Guid.Empty));
+        }
+        else
+            criteria.Add(new Filter<StockHolding>().None());
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
+    
+}

+ 10 - 0
prs.desktop/Panels/Reservation Management/ReservationManagementUserSettings.cs

@@ -11,6 +11,8 @@ public class ReservationManagementUserSettings : IUserConfigurationSettings
     [Obsolete]
     private CoreFilterDefinition? _currentFilter;
 
+    private double substitutionGridHeight = 200.0;
+
     [Obsolete]
     public CoreFilterDefinition? CurrentFilter
     {
@@ -26,4 +28,12 @@ public class ReservationManagementUserSettings : IUserConfigurationSettings
 
     public DynamicGridSelectedFilterSettings Filters { get; set; } = new();
     
+    public bool SubstituteFreeStockOnly { get; set; } = false;
+    public bool SubstituteUpdateStyle { get; set; } = false;
+
+    public double SubstitutionGridHeight
+    {
+        get => double.IsNaN(substitutionGridHeight) ? 200.0 : substitutionGridHeight;
+        set => substitutionGridHeight = value;
+    }
 }