فهرست منبع

Added "Stock Holdings" page to Projects Panel
Moved "Release Stock" function to JobSummaryGrid and JobStockGrid
Moved "Cancel Requisitions" to JobSummaryGrid

frogsoftware 1 سال پیش
والد
کامیت
32ee118410

+ 44 - 0
prs.classes/Entities/Stock/StockHolding/StockHolding.cs

@@ -209,6 +209,7 @@ namespace Comal.Classes
                         .Add(x => x.Job.JobNumber)
                         .Add(x => x.Requisition.Number)
                         .Add(x => x.Requisition.Description)
+                        .Add(x=>x.Status)
                         .Add(x => x.Qty))
                 .ToObjects<JobRequisitionItem>();
             if (holding.Available > 0 || alwaysshowunallocated)
@@ -220,6 +221,49 @@ namespace Comal.Classes
 
             return items;
         }
+
+    //     public static IEnumerable<Tuple<Guid,double>> GetAllocations(this StockHolding holding, bool alwaysshowunallocated)
+    //     {
+    //         var table = new Client<StockMovement>().Query(
+    //             StockHolding.GetFilter(holding),
+    //             new Columns<StockMovement>(x => x.Units)
+    //                 .Add(x => x.Location.ID)
+    //                 .Add(x => x.Product.ID)
+    //                 .Add(x => x.Style.ID)
+    //                 .AddDimensionsColumns(x => x.Dimensions)
+    //                 .Add(x => x.Cost)
+    //                 .Add(x => x.OrderItem.ID)
+    //                 .Add(x => x.JobRequisitionItem.ID)
+    //             );
+    //         
+    //         var movements = table
+    //             .ToObjects<StockMovement>();
+    //         
+    //         var groups = movements
+    //             .GroupBy(x => new
+    //             {
+    //                 Location = x.Location.ID,
+    //                 Product = x.Product.ID,
+    //                 Style = x.Style.ID,
+    //                 x.Dimensions,
+    //                 x.Cost,
+    //                 OrderItem = x.OrderItem.ID,
+    //                 JobRequisitionItem = x.JobRequisitionItem.ID
+    //             });
+    //
+    //         var result = groups
+    //             .Select(x => new Tuple<Guid, double>(
+    //                 x.Key.JobRequisitionItem,
+    //                 x.Sum(x => x.Units))
+    //             ).ToList();
+    //         
+    //         if (alwaysshowunallocated || !holding.Available.IsEffectivelyEqual(0))
+    //             result.Add(new Tuple<Guid, double>(Guid.Empty,holding.Available));
+    //         
+    //         return result;
+    //
+    //     }
+    
     }
     
 }

+ 121 - 120
prs.desktop/Panels/Jobs/ProjectsGrid.cs

@@ -63,133 +63,134 @@ public class ProjectsGrid : DynamicDataGrid<Job>
         HiddenColumns.Add(x => x.DefaultScope.ID);
 
         ActionColumns.Add(new DynamicMapColumn<Job>(this, x => x.SiteAddress.Location));
-        ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
+        //ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
     }
 
     protected override void DoReconfigure(FluentList<DynamicGridOption> options)
     {
         base.DoReconfigure(options);
-        options.AddRange(
-            DynamicGridOption.RecordCount,
-            DynamicGridOption.SelectColumns,
-            DynamicGridOption.FilterRows
-        );
+        options
+            .BeginUpdate()
+            .Add(DynamicGridOption.RecordCount)
+            .Add(DynamicGridOption.SelectColumns)
+            .Add(DynamicGridOption.FilterRows)
+            .EndUpdate();
     }
 
-    private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
-    {
-        if (Security.IsAllowed<CanCancelAllJobRequisitions>())
-        {
-            column.AddItem("Cancel all active requisitions", null, CancelJobRequisitions_Click);
-        }
-        if (Security.IsAllowed<CanReleaseJobReserves>())
-        {
-            column.AddItem("Release Job Reserves", null, ReleaseJobReserves_Click);
-        }
-    }
-
-    private void CancelJobRequisitions_Click(CoreRow? obj)
-    {
-        var job = obj?.ToObject<Job>();
-        if (job is null)
-        {
-            MessageWindow.ShowMessage("Please select a job.", "No job selected");
-            return;
-        }
-
-        if(MessageWindow.ShowYesNoCancel("Are you sure you wish to do this? This will cancel all requisitions for this job.", "Confirm") != MessageWindowResult.Yes)
-        {
-            MessageWindow.ShowMessage("No action taken.", "Process aborted");
-            return;
-        }
-
-        var requisitionItems = Client.Query(
-            new Filter<JobRequisitionItem>(x => x.Requisition.Job.ID).IsEqualTo(job.ID)
-                .And(x => x.Status).IsNotEqualTo(JobRequisitionItemStatus.Cancelled)
-                .And(x => x.Status).IsNotEqualTo(JobRequisitionItemStatus.Issued)
-                .And(x => x.Status).IsNotEqualTo(JobRequisitionItemStatus.Archived),
-            new Columns<JobRequisitionItem>(x => x.ID)
-                .Add(x => x.Cancelled))
-            .ToList<JobRequisitionItem>();
-        foreach(var jri in requisitionItems)
-        {
-            jri.Cancelled = DateTime.Now;
-        }
-        Client.Save(requisitionItems, "Cancelled all job requisitions for job");
-        MessageWindow.ShowMessage("All job requisitions cancelled.", "Success");
-    }
-
-    private void ReleaseJobReserves_Click(CoreRow? obj)
-    {
-        var job = obj?.ToObject<Job>();
-        if (job is null)
-        {
-            MessageWindow.ShowMessage("Please select a job.", "No job selected");
-            return;
-        }
-
-        if(MessageWindow.ShowYesNoCancel("Are you sure you wish to do this? This will release all reserves for this job.", "Confirm") != MessageWindowResult.Yes)
-        {
-            MessageWindow.ShowMessage("No action taken.", "Process aborted");
-            return;
-        }
-
-        var movements = Client.Query<StockMovement>(
-            new Filter<StockMovement>(x => x.Job.ID).IsEqualTo(job.ID),
-            new Columns<StockMovement>(x => x.Units)
-                .Add(x => x.Location.ID)
-                .Add(x => x.Product.ID)
-                .Add(x => x.Style.ID)
-                .AddDimensionsColumns(x => x.Dimensions)
-                .Add(x => x.Cost)
-                .Add(x => x.OrderItem.ID)
-                .Add(x => x.JobRequisitionItem.ID))
-            .ToObjects<StockMovement>()
-            .GroupBy(x => new
-            {
-                Location = x.Location.ID,
-                Product = x.Product.ID,
-                Style = x.Style.ID,
-                x.Dimensions,
-                x.Cost,
-                OrderItem = x.OrderItem.ID,
-                JobRequisitionItem = x.JobRequisitionItem.ID
-            })
-            .Select(x => new { x.Key, Units = x.Sum(x => x.Units) });
-
-        var toSave = new List<StockMovement>();
-        foreach(var group in movements)
-        {
-            var from = new StockMovement();
-            from.Location.ID = group.Key.Location;
-            from.Style.ID = group.Key.Style;
-            from.Product.ID = group.Key.Product;
-            from.Dimensions.CopyFrom(group.Key.Dimensions);
-
-            var to = from.CreateMovement();
-
-            from.Job.ID = job.ID;
-            
-            from.Cost = group.Key.Cost;
-            to.Cost = group.Key.Cost;
-            from.OrderItem.ID = group.Key.OrderItem;
-            to.OrderItem.ID = group.Key.OrderItem;
-            from.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
-            to.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
-
-            from.Type = StockMovementType.TransferOut;
-            to.Type = StockMovementType.TransferIn;
-            to.Transaction = from.Transaction;
-
-            from.Units = -group.Units;
-            to.Units = group.Units;
-
-            toSave.Add(from);
-            toSave.Add(to);
-        }
-        Client.Save(toSave, "Released all job stock.");
-        MessageWindow.ShowMessage("All job stock reserves released.", "Success");
-    }
+    // private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
+    // {
+    //     if (Security.IsAllowed<CanCancelAllJobRequisitions>())
+    //     {
+    //         column.AddItem("Cancel all active requisitions", null, CancelJobRequisitions_Click);
+    //     }
+    //     if (Security.IsAllowed<CanReleaseJobReserves>())
+    //     {
+    //         column.AddItem("Release Job Reserves", null, ReleaseJobReserves_Click);
+    //     }
+    // }
+    //
+    // private void CancelJobRequisitions_Click(CoreRow? obj)
+    // {
+    //     var job = obj?.ToObject<Job>();
+    //     if (job is null)
+    //     {
+    //         MessageWindow.ShowMessage("Please select a job.", "No job selected");
+    //         return;
+    //     }
+    //
+    //     if(MessageWindow.ShowYesNoCancel("Are you sure you wish to do this? This will cancel all requisitions for this job.", "Confirm") != MessageWindowResult.Yes)
+    //     {
+    //         MessageWindow.ShowMessage("No action taken.", "Process aborted");
+    //         return;
+    //     }
+    //
+    //     var requisitionItems = Client.Query(
+    //         new Filter<JobRequisitionItem>(x => x.Requisition.Job.ID).IsEqualTo(job.ID)
+    //             .And(x => x.Status).IsNotEqualTo(JobRequisitionItemStatus.Cancelled)
+    //             .And(x => x.Status).IsNotEqualTo(JobRequisitionItemStatus.Issued)
+    //             .And(x => x.Status).IsNotEqualTo(JobRequisitionItemStatus.Archived),
+    //         new Columns<JobRequisitionItem>(x => x.ID)
+    //             .Add(x => x.Cancelled))
+    //         .ToList<JobRequisitionItem>();
+    //     foreach(var jri in requisitionItems)
+    //     {
+    //         jri.Cancelled = DateTime.Now;
+    //     }
+    //     Client.Save(requisitionItems, "Cancelled all job requisitions for job");
+    //     MessageWindow.ShowMessage("All job requisitions cancelled.", "Success");
+    // }
+    //
+    // private void ReleaseJobReserves_Click(CoreRow? obj)
+    // {
+    //     var job = obj?.ToObject<Job>();
+    //     if (job is null)
+    //     {
+    //         MessageWindow.ShowMessage("Please select a job.", "No job selected");
+    //         return;
+    //     }
+    //
+    //     if(MessageWindow.ShowYesNoCancel("Are you sure you wish to do this? This will release all reserves for this job.", "Confirm") != MessageWindowResult.Yes)
+    //     {
+    //         MessageWindow.ShowMessage("No action taken.", "Process aborted");
+    //         return;
+    //     }
+    //
+    //     var movements = Client.Query<StockMovement>(
+    //         new Filter<StockMovement>(x => x.Job.ID).IsEqualTo(job.ID),
+    //         new Columns<StockMovement>(x => x.Units)
+    //             .Add(x => x.Location.ID)
+    //             .Add(x => x.Product.ID)
+    //             .Add(x => x.Style.ID)
+    //             .AddDimensionsColumns(x => x.Dimensions)
+    //             .Add(x => x.Cost)
+    //             .Add(x => x.OrderItem.ID)
+    //             .Add(x => x.JobRequisitionItem.ID))
+    //         .ToObjects<StockMovement>()
+    //         .GroupBy(x => new
+    //         {
+    //             Location = x.Location.ID,
+    //             Product = x.Product.ID,
+    //             Style = x.Style.ID,
+    //             x.Dimensions,
+    //             x.Cost,
+    //             OrderItem = x.OrderItem.ID,
+    //             JobRequisitionItem = x.JobRequisitionItem.ID
+    //         })
+    //         .Select(x => new { x.Key, Units = x.Sum(x => x.Units) });
+    //
+    //     var toSave = new List<StockMovement>();
+    //     foreach(var group in movements)
+    //     {
+    //         var from = new StockMovement();
+    //         from.Location.ID = group.Key.Location;
+    //         from.Style.ID = group.Key.Style;
+    //         from.Product.ID = group.Key.Product;
+    //         from.Dimensions.CopyFrom(group.Key.Dimensions);
+    //
+    //         var to = from.CreateMovement();
+    //
+    //         from.Job.ID = job.ID;
+    //         
+    //         from.Cost = group.Key.Cost;
+    //         to.Cost = group.Key.Cost;
+    //         from.OrderItem.ID = group.Key.OrderItem;
+    //         to.OrderItem.ID = group.Key.OrderItem;
+    //         from.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
+    //         to.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
+    //
+    //         from.Type = StockMovementType.TransferOut;
+    //         to.Type = StockMovementType.TransferIn;
+    //         to.Transaction = from.Transaction;
+    //
+    //         from.Units = -group.Units;
+    //         to.Units = group.Units;
+    //
+    //         toSave.Add(from);
+    //         toSave.Add(to);
+    //     }
+    //     Client.Save(toSave, "Released all job stock.");
+    //     MessageWindow.ShowMessage("All job stock reserves released.", "Success");
+    // }
 
     public Guid StatusID
     {

+ 5 - 0
prs.desktop/Panels/Jobs/ProjectsPanel.cs

@@ -4,6 +4,7 @@ using InABox.Configuration;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.Wpf;
+using PRSDesktop.Panels.Jobs;
 
 namespace PRSDesktop;
 
@@ -38,6 +39,7 @@ public class ProjectsPanel : MasterDetailPanel<Job,ProjectsGrid,ProjectsPanelSet
         CreatePage<JobDetailGrid<JobProductStylesGrid, JobStyle>>(Security.CanView<JobStyle>, "Product Styles");
         CreatePage<JobDetailPanel<JobBillOfMaterialsPanel>>(Security.CanView<JobBillOfMaterials>, "BOM");
         CreatePage<JobDetailPanel<JobRequisitionPanel>>(Security.CanView<JobRequisition>, "Requisitions");
+        CreatePage<JobDetailPanel<JobStockGrid>>(Security.CanView<StockHolding>, "Stock Holdings");
         CreatePage<JobDetailPanel<JobPickingListPanel>>(Security.CanView<Requisition>, "Picking Lists");
         CreatePage<JobDetailGrid<JobOrderGrid, PurchaseOrderItem>>(Security.CanView<PurchaseOrderItem>, "Orders");
         CreatePage<JobDetailPanel<JobDesignPanel>>(Security.CanView<Setout>, "Designs");
@@ -78,6 +80,9 @@ public class ProjectsPanel : MasterDetailPanel<Job,ProjectsGrid,ProjectsPanelSet
         host.CreateSetupSeparator();
         ProjectSetupActions.SetoutGroups(host);
 
+        if (SelectedPage is IMasterDetailPanelPage { Panel: not null } subpanel)
+            subpanel.Panel.CreateToolbarButtons(host);
+        
         // host.CreateSetupAction(new PanelAction()
         // {
         //     Caption = "Job Settings", 

+ 1 - 1
prs.desktop/Panels/Jobs/Requisitions/JobRequisitionGrid.cs

@@ -20,7 +20,7 @@ namespace PRSDesktop
         public Job? Master { get; set; }
 
         public Filter<JobRequisition> MasterDetailFilter => (Master?.ID ?? Guid.Empty) != Guid.Empty
-            ? new Filter<JobRequisition>(x => x.Job.ID).IsEqualTo(Master.ID)
+            ? new Filter<JobRequisition>(x => x.Job.ID).IsEqualTo(Master?.ID ?? Guid.Empty)
             : new Filter<JobRequisition>().None();
         
         private readonly Button _approve;

+ 162 - 4
prs.desktop/Panels/Jobs/Stock Holdings/JobStockGrid.cs

@@ -1,15 +1,128 @@
 using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows.Controls;
+using com.sun.xml.@internal.ws.api.config.management;
 using Comal.Classes;
+using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.Wpf;
+using InABox.WPF;
+using net.sf.mpxj.MpxjUtilities;
 
 namespace PRSDesktop.Panels.Jobs
 {
-    public class JobStockGrid : DynamicDataGrid<StockHolding>, IMasterDetailControl<Job,StockHolding>
+    public class JobStockGrid : DynamicDataGrid<StockHolding>, IMasterDetailControl<Job,StockHolding>, IBasePanel
     {
-        
-        public Job? Master { get; set; }
+        public Job? Master
+        {
+            get => _master;
+            set
+            {
+                _master = value; 
+                _releaseStock.IsEnabled = value?.JobStatus.Active == false && SelectedRows?.Any() == true;
+            }
+        }
+
+        private Button _releaseStock;
+        private Job? _master;
+
+        public JobStockGrid()
+        {
+            _releaseStock = AddButton("Release Stock", PRSDesktop.Resources.archive.AsBitmapImage(), ReleaseStock);
+            HiddenColumns.Add(x => x.Location.ID);
+            HiddenColumns.Add(x => x.Product.ID);
+            HiddenColumns.Add(x => x.Style.ID);
+            HiddenColumns.Add(x=>x.Dimensions.Unit.ID);
+            HiddenColumns.Add(x=>x.Dimensions.Height);
+            HiddenColumns.Add(x=>x.Dimensions.Length);
+            HiddenColumns.Add(x=>x.Dimensions.Width);
+            HiddenColumns.Add(x=>x.Dimensions.Quantity);
+            HiddenColumns.Add(x=>x.Dimensions.Weight);
+            HiddenColumns.Add(x => x.Job.ID);
+        }
+
+        protected override void Init()
+        {
+            base.Init();
+        }
+
+        protected override void SelectItems(CoreRow[]? rows)
+        {
+            base.SelectItems(rows);
+            _releaseStock.IsEnabled = Master?.JobStatus.Active == false && SelectedRows?.Any() == true;
+        }
+
+
+        private bool ReleaseStock(object sender, CoreRow[] rows)
+        {
+            if(MessageWindow.ShowYesNoCancel("Are you sure you wish to do this? This will release the selected holdings from this job.", "Confirm") != MessageWindowResult.Yes)
+                return false;
+
+            var toSave = new List<StockMovement>();
+            foreach (var row in SelectedRows)
+            {
+                StockHolding holding = row.ToObject<StockHolding>();
+                var movements = new Client<StockMovement>().Query(
+                        StockHolding.GetFilter(holding),
+                        new Columns<StockMovement>(x => x.Units)
+                            .Add(x => x.Location.ID)
+                            .Add(x => x.Product.ID)
+                            .Add(x => x.Style.ID)
+                            .AddDimensionsColumns(x => x.Dimensions)
+                            .Add(x => x.Cost)
+                            .Add(x => x.OrderItem.ID)
+                            .Add(x => x.JobRequisitionItem.ID))
+                    .ToObjects<StockMovement>()
+                    .GroupBy(x => new
+                    {
+                        Location = x.Location.ID,
+                        Product = x.Product.ID,
+                        Style = x.Style.ID,
+                        x.Dimensions,
+                        x.Cost,
+                        OrderItem = x.OrderItem.ID,
+                        JobRequisitionItem = x.JobRequisitionItem.ID
+                    })
+                    .Select(x => new
+                        {
+                            x.Key,
+                            Units = x.Sum(x => x.Units)
+                        }
+                    );
+
+                foreach (var group in movements)
+                {
+
+                    var from = holding.CreateMovement();
+                    from.OrderItem.ID = group.Key.OrderItem;
+                    from.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
+                    from.Cost = group.Key.Cost;
+                    from.Type = StockMovementType.TransferOut;
+                    from.Job.ID = Master.ID;
+                    from.Issued = group.Units;
+
+                    var to = holding.CreateMovement();
+                    to.Cost = group.Key.Cost;
+                    to.OrderItem.ID = group.Key.OrderItem;
+                    to.JobRequisitionItem.ID = group.Key.JobRequisitionItem;
+                    to.Type = StockMovementType.TransferIn;
+                    to.Transaction = from.Transaction;
+                    to.Units = group.Units;
+
+                    toSave.Add(from);
+                    toSave.Add(to);
+                }
+            }
+
+            new Client<StockMovement>().Save(toSave, "Released job stock.");
+            Refresh(false,true);
+            MessageWindow.ShowMessage("All job stock reserves released.", "Success");
+
+            return true;
+        }
 
         public Filter<StockHolding> MasterDetailFilter => (Master?.ID ?? Guid.Empty) != Guid.Empty
             ? new Filter<StockHolding>(x => x.Job.ID).IsEqualTo(Master.ID)
@@ -18,7 +131,14 @@ namespace PRSDesktop.Panels.Jobs
         protected override void DoReconfigure(FluentList<DynamicGridOption> options)
         {
             base.DoReconfigure(options);
-            options.AddRange(DynamicGridOption.RecordCount, DynamicGridOption.SelectColumns);
+            options
+                .BeginUpdate()
+                .Clear()
+                .Add(DynamicGridOption.FilterRows)
+                .Add(DynamicGridOption.RecordCount)
+                .Add(DynamicGridOption.SelectColumns)
+                .Add(DynamicGridOption.ExportData)
+                .EndUpdate();
         }
         
         protected override void Reload(Filters<StockHolding> criteria, Columns<StockHolding> columns, ref SortOrder<StockHolding>? sort, Action<CoreTable?, Exception?> action)
@@ -26,5 +146,43 @@ namespace PRSDesktop.Panels.Jobs
             criteria.Add(MasterDetailFilter);
             base.Reload(criteria, columns, ref sort, action);
         }
+
+        public void Setup()
+        {
+            Refresh(true,false);
+        }
+
+        public void Shutdown(CancelEventArgs? cancel)
+        {
+            
+        }
+
+        public void Refresh()
+        {
+            Refresh(false,true);
+        }
+
+        public string SectionName => "Stock Holdings";
+        public DataModel DataModel(Selection selection)
+        {
+            return new AutoDataModel<StockHolding>(
+                new Filter<StockHolding>(x => x.Job.ID).IsEqualTo(Master?.ID ?? CoreUtils.FullGuid));
+        }
+
+        public event DataModelUpdateEvent? OnUpdateDataModel;
+        public bool IsReady { get; set; }
+        public void CreateToolbarButtons(IPanelHost host)
+        {
+        }
+
+        public Dictionary<string, object[]> Selected()
+        {
+            return new Dictionary<string, object[]>();
+        }
+
+        public void Heartbeat(TimeSpan time)
+        {
+            
+        }
     }
 }

+ 128 - 2
prs.desktop/Panels/Jobs/Summary/JobSummaryGrid.cs

@@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Windows;
+using System.Windows.Controls;
 using System.Windows.Media;
 
 namespace PRSDesktop;
@@ -19,8 +20,21 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
 {
     Guid empID = Guid.Empty;
     string empName = "";
+    private List<DetailsColumn> DetailsColumns;
+    private Job? _master;
     
-    public Job? Master { get; set; }
+    private Button _cancelRequisitions;
+    private Button _releaseStock;
+
+    public Job? Master
+    {
+        get => _master;
+        set
+        {
+            _master = value;
+            _cancelRequisitions.IsEnabled = Master?.JobStatus.Active == false;
+        }
+    }
 
     public Filter<JobMaterial> MasterDetailFilter => (Master?.ID ?? Guid.Empty) != Guid.Empty
         ? new Filter<JobMaterial>(x => x.Job.ID).IsEqualTo(Master.ID)
@@ -38,8 +52,120 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         OnCellDoubleClick += JobSummaryGrid_OnCellDoubleClick;
 
         SetupDetailsColumns();
+        
+        _cancelRequisitions = AddButton("Cancel Requisitions", PRSDesktop.Resources.archive.AsBitmapImage(),
+            CancelRequisitions);
+        
+        _releaseStock = AddButton("Release Stock", PRSDesktop.Resources.archive.AsBitmapImage(),
+            ReleaseStock);
+    }
+
+    public Dictionary<StockHolding,IEnumerable<JobRequisitionItem>> GetHoldings(CoreRow[] rows)
+    {
+        var result = new Dictionary<StockHolding,IEnumerable<JobRequisitionItem>>();
+        foreach (var row in rows)
+        {
+            var material = row.ToObject<JobMaterial>();
+            var filter = new Filter<StockHolding>(x => x.Product.ID).IsEqualTo(material.Product.ID)
+                .And(x => x.Dimensions.Unit.ID).IsEqualTo(material.Dimensions.Unit.ID)
+                .And(x => x.Dimensions.Height).IsEqualTo(material.Dimensions.Height)
+                .And(x => x.Dimensions.Width).IsEqualTo(material.Dimensions.Width)
+                .And(x => x.Dimensions.Length).IsEqualTo(material.Dimensions.Length)
+                .And(x => x.Dimensions.Quantity).IsEqualTo(material.Dimensions.Quantity)
+                .And(x => x.Dimensions.Weight).IsEqualTo(material.Dimensions.Weight);
+            if (StyleColumnVisible())
+                filter = filter.And(x => x.Style.ID).IsEqualTo(material.Style.ID);
+            var columns = new Columns<StockHolding>(x => x.Location.ID)
+                .Add(x => x.Product.ID)
+                .Add(x => x.Style.ID)
+                .AddDimensionsColumns(x=>x.Dimensions);
+            foreach (var holding in new Client<StockHolding>().Query(filter, columns).ToObjects<StockHolding>())
+                result[holding] = holding.LoadRequisitionItems(true);
+        }
+        return result;
     }
 
+    private bool ReleaseStock(System.Windows.Controls.Button btn, CoreRow[] rows)
+    {
+        if (rows?.Any() != true)
+            return false;
+        
+        if(MessageWindow.ShowYesNoCancel("This will release any stock holdings for the selected items!\n\nAre you sure you wish to do this? ", "Confirm") != MessageWindowResult.Yes)
+            return false;
+        
+        var updates = new List<StockMovement>();
+        Progress.ShowModal("Loading Holdings", progress =>
+        {
+            var holdings = GetHoldings(rows);
+            foreach (var holding in holdings.Keys)
+            {
+                foreach (var item in holdings[holding])
+                {
+                    var from = holding.CreateMovement();
+                    //from.OrderItem.ID = group.Key.OrderItem;
+                    from.JobRequisitionItem.ID = item.ID;
+                    from.Cost = holding.AverageValue;
+                    from.Type = StockMovementType.TransferOut;
+                    from.Job.ID = Master.ID;
+                    from.Issued = item.Qty;
+
+                    var to = holding.CreateMovement();
+                    to.Cost = holding.AverageValue;
+                    //to.OrderItem.ID = group.Key.OrderItem;
+                    //to.JobRequisitionItem.ID = item.ID;
+                    to.Job.ID = Guid.Empty;
+                    to.Type = StockMovementType.TransferIn;
+                    to.Transaction = from.Transaction;
+                    to.Received = item.Qty;
+
+                    updates.Add(from);
+                    updates.Add(to);
+                }
+            }
+            progress.Report("Saving Movements");
+            if (updates.Any())
+                Client.Save(updates, "Stock Released from Job Summary Screen");
+        });
+        MessageWindow.ShowMessage("The selected stock holdings have been cancelled.","Done");
+
+        return updates.Any();
+    }
+    
+    private bool CancelRequisitions(System.Windows.Controls.Button btn, CoreRow[] rows)
+    {
+        if (rows?.Any() != true)
+            return false;
+        
+        if(MessageWindow.ShowYesNoCancel("This will cancel all outstanding requisitions for the selected items!\n\nAre you sure you wish to do this? ", "Confirm") != MessageWindowResult.Yes)
+            return false;
+            
+        var updates = new List<JobRequisitionItem>();
+        Progress.ShowModal("Loading Holdings", progress =>
+        {
+            var holdings = GetHoldings(rows);
+            foreach (var holding in holdings)
+            {
+                var items = holding.Value.Where(
+                    x => x.ID != Guid.Empty
+                         && x.Status != JobRequisitionItemStatus.Cancelled
+                         && x.Status != JobRequisitionItemStatus.Issued
+                         && x.Status != JobRequisitionItemStatus.Archived
+                );
+                foreach (var item in items)
+                {
+                    item.Cancelled = DateTime.Now;
+                    updates.Add(item);
+                }
+
+                progress.Report("Cancelling Requisitions");
+                Client.Save(updates, "Requsition cancelled from Job Summary Screen");
+            }
+        });
+        MessageWindow.ShowMessage("The selected requisitions have been cancelled.","Done");
+        return updates.Any();
+    }
+
+
     private class UIComponent : DynamicGridGridUIComponent<JobMaterial>
     {
         private Column<JobMaterial> _jobshortage = new Column<JobMaterial>(x => x.JobShortage);
@@ -389,7 +515,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         );
     }
 
-    private List<DetailsColumn> DetailsColumns;
+
 
     [MemberNotNull(nameof(DetailsColumns))]
     private void SetupDetailsColumns()