Bläddra i källkod

Merge remote-tracking branch 'origin/kenric' into frank

frogsoftware 1 år sedan
förälder
incheckning
6a3cbf809e

+ 2 - 2
prs.classes/Entities/Employee/EmployeeProduct.cs

@@ -27,7 +27,7 @@ namespace Comal.Classes
         public DateTime Issued { get; set; }
 
         [NullEditor]
-        [EntityRelationship(DeleteAction.SetNull)]
+        [EntityRelationship(DeleteAction.Cascade)]
         public StockMovementLink IssuedMovement { get; set; }
 
         [TimestampEditor(Visible = Visible.Default)]
@@ -35,7 +35,7 @@ namespace Comal.Classes
         public DateTime Returned { get; set; }
 
         [NullEditor]
-        [EntityRelationship(DeleteAction.Cascade)]
+        [EntityRelationship(DeleteAction.SetNull)]
         public StockMovementLink ReturnedMovement { get; set; }
 
         [TimestampEditor(Visible = Visible.Default)]

+ 1 - 1
prs.classes/Entities/Job/Materials/JobMaterials.cs

@@ -42,7 +42,7 @@ namespace Comal.Classes
         
         public override Filter<JobRequisitionItem> Filter =>
             new Filter<JobRequisitionItem>(x => x.Requisition.Approved).IsNotEqualTo(DateTime.MinValue)
-                .And(x=>x.Status).IsNotEqualTo(JobRequisitionItemStatus.Cancelled);
+                .And(x => x.Cancelled).IsEqualTo(DateTime.MinValue);
 
     }
     

+ 41 - 14
prs.classes/Entities/PurchaseOrder/PurchaseOrder.cs

@@ -15,12 +15,26 @@ namespace Comal.Classes
     //
     //     public override Filter<PurchaseOrderItem>? Filter => new Filter<PurchaseOrderItem>(c => c.DueDate).IsNotEqualTo(DateTime.MinValue);
     //
-    //     public override Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>> Links =>
-    //         new Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>>()
+    //     public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+    //         new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
     //         {
     //             { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
     //         };
     // }
+    public class PurchaseOrderUnreceivedQtyAggregate : CoreAggregate<PurchaseOrder, PurchaseOrderItem, double>
+    {
+        public override Expression<Func<PurchaseOrderItem, double>> Aggregate => x => x.Qty;
+
+        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
+
+        public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+            new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
+            {
+                { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
+            };
+
+        public override Filter<PurchaseOrderItem>? Filter => new Filter<PurchaseOrderItem>(x => x.ReceivedDate).IsEqualTo(DateTime.MinValue);
+    }    
     
     public class PurchaseOrderExTax : CoreAggregate<PurchaseOrder, PurchaseOrderItem, double>
     {
@@ -28,8 +42,8 @@ namespace Comal.Classes
 
         public override AggregateCalculation Calculation => AggregateCalculation.Sum;
 
-        public override Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>> Links =>
-            new Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>>()
+        public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+            new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
             {
                 { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
             };
@@ -41,8 +55,8 @@ namespace Comal.Classes
 
         public override AggregateCalculation Calculation => AggregateCalculation.Sum;
 
-        public override Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>> Links =>
-            new Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>>()
+        public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+            new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
             {
                 { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
             };
@@ -54,8 +68,8 @@ namespace Comal.Classes
 
         public override AggregateCalculation Calculation => AggregateCalculation.Sum;
 
-        public override Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>> Links =>
-            new Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>>()
+        public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+            new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
             {
                 { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
             };
@@ -67,8 +81,8 @@ namespace Comal.Classes
 
         public override AggregateCalculation Calculation => AggregateCalculation.Sum;
 
-        public override Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>> Links =>
-            new Dictionary<Expression<Func<PurchaseOrderItem, object>>, Expression<Func<PurchaseOrder, object>>>()
+        public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+            new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
             {
                 { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
             };
@@ -120,6 +134,10 @@ namespace Comal.Classes
         [TimestampEditor]
         [EditorSequence("Additional",4)]
         public DateTime ClosedDate { get; set; }
+        
+        [TimestampEditor]
+        [EditorSequence("Additional",5)]
+        public DateTime CancelledDate { get; set; }
 
         [EditorSequence(10)]
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
@@ -141,6 +159,11 @@ namespace Comal.Classes
         [Aggregate(typeof(PurchaseOrderBalance))]
         public double Balance { get; set; }
 
+        [EditorSequence(13)]
+        [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
+        [Aggregate(typeof(PurchaseOrderUnreceivedQtyAggregate))]
+        public double Unreceived { get; set; }
+
         [EditorSequence(14)]
         [EnumLookupEditor(typeof(PurchaseOrderStatus), Editable = Editable.Hidden)]
         public PurchaseOrderStatus Status { get; set; }
@@ -201,6 +224,10 @@ namespace Comal.Classes
                     if (IssuedDate == DateTime.MinValue)
                         IssuedDate = DateTime.Now;
                 }
+                else if(status == PurchaseOrderStatus.Cancelled)
+                {
+                    CancelledDate = DateTime.Now;
+                }
                 else
                 {
                     if (ClosedDate != DateTime.MinValue)
@@ -211,12 +238,12 @@ namespace Comal.Classes
             }
             else
             {
-                var closed = name.Equals("ClosedDate") ? (DateTime)after : ClosedDate;
-                var issued = name.Equals("IssuedDate") ? (DateTime)after : IssuedDate;
                 var status = PurchaseOrderStatus.Closed;
-                if (closed != DateTime.MinValue)
+                if (CancelledDate != DateTime.MinValue)
+                    status = PurchaseOrderStatus.Cancelled;
+                else if (ClosedDate != DateTime.MinValue)
                     status = PurchaseOrderStatus.Closed;
-                else if (issued != DateTime.MinValue)
+                else if (IssuedDate != DateTime.MinValue)
                     status = PurchaseOrderStatus.Issued;
                 else
                     status = PurchaseOrderStatus.Draft;

+ 2 - 1
prs.classes/Entities/PurchaseOrder/PurchaseOrderStatus.cs

@@ -4,6 +4,7 @@
     {
         Draft,
         Issued,
-        Closed
+        Closed,
+        Cancelled
     }
 }

+ 2 - 2
prs.classes/Gantt/GanttRelationship.cs

@@ -9,10 +9,10 @@ namespace Comal.Classes
         [EntityRelationship(DeleteAction.Cascade)]
         public TEntityLink Parent { get; set; }
 
-        [EntityRelationship(DeleteAction.Cascade)]
+        [EntityRelationship(DeleteAction.SetNull)]
         public TStageLink Predecessor { get; set; }
 
-        [EntityRelationship(DeleteAction.Cascade)]
+        [EntityRelationship(DeleteAction.SetNull)]
         public TStageLink Successor { get; set; }
 
         public GanttRelationshipType Type { get; set; }

+ 81 - 30
prs.desktop/Panels/DataEntry/DataEntryGrid.cs

@@ -4,6 +4,7 @@ using InABox.Configuration;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.WPF;
+using PRSDesktop.Panels.DataEntry;
 using Syncfusion.Pdf;
 using System;
 using System.Collections.Generic;
@@ -158,6 +159,7 @@ public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
         HiddenColumns.Add(x => x.Tag.ID);
         HiddenColumns.Add(x => x.Tag.AppliesTo);
         HiddenColumns.Add(x => x.Document.ID);
+        HiddenColumns.Add(x => x.Document.FileName);
         HiddenColumns.Add(x => x.EntityID);
         HiddenColumns.Add(x => x.Archived);
         HiddenColumns.Add(x => x.Note);
@@ -231,10 +233,14 @@ public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
         _tags ??= GetVisibleTagList();
         return _tags;
     }
-    
-    public void DoExplode()
+
+    /// <summary>
+    /// Gets the currently selected tag ID, if all selected rows belong to the same tag.
+    /// </summary>
+    /// <returns></returns>
+    private Guid GetSelectedTagID()
     {
-        Guid tagID = Guid.Empty;
+        var tagID = Guid.Empty;
         foreach (var row in SelectedRows)
         {
             var rowTag = row.Get<DataEntryDocument, Guid>(x => x.Tag.ID);
@@ -244,35 +250,75 @@ public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
             }
             else if (rowTag != tagID)
             {
-                tagID = Guid.Empty;
-                break;
+                return Guid.Empty;
             }
         }
+        return tagID;
+    }
 
-        var docIDs = SelectedRows.Select(r => r.Get<DataEntryDocument, Guid>(c => c.Document.ID)).ToArray();
+    private IEnumerable<Tuple<DataEntryDocument, List<DataEntryReGroupWindow.Page>>> ExplodeDocuments()
+    {
+        var dataEntryDocs = SelectedRows.ToArray<DataEntryDocument>();
+        var docIDs = dataEntryDocs.Select(r => r.Document.ID).ToArray();
         var docs = new Client<Document>()
             .Query(
                 new Filter<Document>(x => x.ID).InList(docIDs),
                 Columns.None<Document>().Add(x => x.ID).Add(x => x.Data).Add(x => x.FileName))
             .ToObjects<Document>().ToDictionary(x => x.ID, x => x);
 
-        var pages = new List<DataEntryReGroupWindow.Page>();
-        string filename = "";
-        foreach (var docID in docIDs)
+        foreach (var dataEntryDoc in dataEntryDocs)
         {
-            if (docs.TryGetValue(docID, out var doc))
+            if (docs.TryGetValue(dataEntryDoc.Document.ID, out var doc))
             {
-                filename = doc.FileName;
                 var ms = new MemoryStream(doc.Data);
                 var pdfDoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, ms);
-                foreach (var page in DataEntryReGroupWindow.SplitIntoPages(doc.FileName, pdfDoc))
+                yield return new(dataEntryDoc, DataEntryReGroupWindow.SplitIntoPages(doc.FileName, pdfDoc).ToList());
+            }
+        }
+    }
+
+    public void DoExplodeAll()
+    {
+        var pages = ExplodeDocuments();
+
+        var groups = new List<DocumentGroup>();
+        foreach(var (doc, docPages) in pages)
+        {
+            if(docPages.Count == 1)
+            {
+                groups.Add(new DocumentGroup(doc.Document.FileName, docPages, doc.Tag.ID));
+            }
+            else
+            {
+                var extension = Path.GetExtension(doc.Document.FileName) ?? "";
+                var stem = Path.GetFileNameWithoutExtension(doc.Document.FileName);
+
+                foreach(var (i, page) in docPages.WithIndex())
                 {
-                    pages.Add(page);
+                    var group = new DocumentGroup($"{stem} - {i + 1}{extension}", [page], doc.Tag.ID);
+                    groups.Add(group);
                 }
             }
         }
+        SavePageGroups(groups);
 
-        if (ShowDocumentWindow(pages, filename, tagID))
+        DeleteItems(SelectedRows);
+        Refresh(false,true);
+    }
+    
+    public void DoExplode()
+    {
+        var tagID = GetSelectedTagID();
+        var pages = ExplodeDocuments();
+        var filename = "";
+        var allPages = new List<DataEntryReGroupWindow.Page>();
+        foreach(var (doc, docPages) in pages)
+        {
+            filename = doc.Document.FileName;
+            allPages.AddRange(docPages);
+        }
+
+        if (ShowDocumentWindow(allPages, filename, tagID))
         {
             // ShowDocumentWindow already saves new scans, so we just need to get rid of the old ones.
             DeleteItems(SelectedRows);
@@ -391,28 +437,33 @@ public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
         return document;
     }
 
-    public bool ShowDocumentWindow(List<DataEntryReGroupWindow.Page> pages, string filename, Guid tagID)
+    private void SavePageGroups(IEnumerable<DocumentGroup> groups)
     {
-        var window = new DataEntryReGroupWindow(pages, filename, tagID);
-        if (window.ShowDialog() == true)
+        Progress.ShowModal("Uploading Files", (progress) =>
         {
-            Progress.ShowModal("Uploading Files", (progress) =>
+            foreach (var group in groups)
             {
-                foreach (var group in window.Groups)
+                progress.Report($"Uploading '{group.FileName}'");
+                var doc = CombinePages(group.Pages);
+
+                byte[] data;
+                using (var ms = new MemoryStream())
                 {
-                    progress.Report($"Uploading '{group.FileName}'");
-                    var doc = CombinePages(group.Pages);
+                    doc.Save(ms);
+                    data = ms.ToArray();
+                }
 
-                    byte[] data;
-                    using (var ms = new MemoryStream())
-                    {
-                        doc.Save(ms);
-                        data = ms.ToArray();
-                    }
+                UploadDocument(group.FileName, data, group.TagID);
+            }
+        });
+    }
 
-                    UploadDocument(group.FileName, data, group.TagID);
-                }
-            });
+    public bool ShowDocumentWindow(List<DataEntryReGroupWindow.Page> pages, string filename, Guid tagID)
+    {
+        var window = new DataEntryReGroupWindow(pages, filename, tagID);
+        if (window.ShowDialog() == true)
+        {
+            SavePageGroups(window.Groups);
             return true;
         }
         return false;

+ 1 - 0
prs.desktop/Panels/DataEntry/DataEntryList.xaml

@@ -64,6 +64,7 @@
                                         x:Name="_contextMenu">
                                         <ContextMenu.Items>
                                             <MenuItem x:Name="_Explode" Header="Regroup Pages" Click="_Explode_OnClick"/>
+                                            <MenuItem x:Name="_ExplodeAll" Header="Explode All Pages" Click="_ExplodeAll_OnClick"/>
                                             <MenuItem x:Name="_ShowImage" Header="View Image" Click="_ShowImage_Click"
                                                       ToolTip="Show this image in a separate window." Tag="{Binding}"/>
                                             <MenuItem x:Name="_RotateImage" Header="Rotate Document" Click="_RotateImage_Click"

+ 4 - 0
prs.desktop/Panels/DataEntry/DataEntryList.xaml.cs

@@ -461,6 +461,10 @@ public partial class DataEntryList : UserControl, ICorePanel, IDockPanel
     {
        _dataEntryGrid.DoExplode();
     }
+    private void _ExplodeAll_OnClick(object sender, RoutedEventArgs e)
+    {
+       _dataEntryGrid.DoExplodeAll();
+    }
 
     private List<DataEntryTag>? _tags;
 

+ 203 - 0
prs.desktop/Panels/Jobs/Summary/JobRequisitionItemSummaryGrid.cs

@@ -0,0 +1,203 @@
+using Comal.Classes;
+using InABox.Configuration;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+using Syncfusion.Data;
+using Syncfusion.UI.Xaml.Grid;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Media;
+
+namespace PRSDesktop.Panels.Jobs.Summary;
+
+public class JobRequisitionItemSummaryGrid : DynamicDataGrid<JobRequisitionItem>, ISpecificGrid
+{
+    public int DueDateAlert { get; set; }
+    public int DueDateWarning { get; set; }
+
+    protected override bool ShowSequenceButtons => false;
+
+    private class UIComponent : DynamicGridGridUIComponent<JobRequisitionItem>
+    {
+        private JobRequisitionItemSummaryGrid Grid;
+
+        public UIComponent(JobRequisitionItemSummaryGrid grid)
+        {
+            Grid = grid;
+            Parent = grid;
+        }
+
+        protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
+        {
+            if (column is DynamicGridColumn col)
+            {
+                if (String.Equals(col.ColumnName,
+                        CoreUtils.GetFullPropertyName<JobRequisitionItem, DateTime>(x => x.Requisition.DueDate, ".")))
+                {
+                    var due = row.Get<JobRequisitionItem, DateTime>(x => x.Requisition.DueDate);
+                    if (!due.IsEmpty())
+                    {
+                        var background = DateTime.Today > due.Date
+                            ? Colors.LightSalmon
+                            : DateTime.Today.AddDays(Grid.DueDateWarning) >= due.Date
+                                ? Colors.Orange
+                                : DateTime.Today.AddDays(Grid.DueDateAlert) >= due.Date
+                                    ? Colors.LightYellow
+                                    : Colors.LightGreen;
+                        return new SolidColorBrush(background) { Opacity = 0.5 };
+                    }
+                }
+            }
+            else if(column is DynamicActionColumn dac)
+            {
+                if (dac == Grid.InStockColumn
+                    || dac == Grid.OnOrderColumn)
+                {
+                    return Colors.LightBlue.ToBrush(0.5);
+                }
+                else if (dac == Grid.TreatmentRequiredColumn
+                    || dac == Grid.TreatmentOnOrderColumn)
+                {
+                    return Colors.Plum.ToBrush(0.5);
+                }
+            }
+
+            var qty = row.Get<JobRequisitionItem, double>(x => x.Qty);
+            if(row.Get<JobRequisitionItem, double>(x => x.Issued) >= qty)
+            {
+                return Colors.Silver.ToBrush(0.5);
+            }
+            else if(row.Get<JobRequisitionItem, double>(x => x.Allocated) + row.Get<JobRequisitionItem, double>(x => x.Issued) >= qty)
+            {
+                return Colors.LightGreen.ToBrush(0.5);
+            }
+            else if(row.Get<JobRequisitionItem, double>(x => x.InStock) + row.Get<JobRequisitionItem, double>(x => x.Issued) >= qty)
+            {
+                return Colors.Orange.ToBrush(0.5);
+            }
+            else
+            {
+                return Colors.LightSalmon.ToBrush(0.5);
+            }
+        }
+    }
+
+    private DynamicActionColumn InStockColumn;
+    private DynamicActionColumn OnOrderColumn;
+    private DynamicActionColumn TreatmentRequiredColumn;
+    private DynamicActionColumn TreatmentOnOrderColumn;
+    private DynamicActionColumn AllocatedColumn;
+    private DynamicActionColumn IssuedColumn;
+
+    public JobRequisitionItemSummaryGrid()
+    {
+        HiddenColumns.Add(x => x.Qty);
+        HiddenColumns.Add(x => x.InStock);
+        HiddenColumns.Add(x => x.OnOrder);
+        HiddenColumns.Add(x => x.TreatmentOnOrder);
+        HiddenColumns.Add(x => x.TreatmentRequired);
+        HiddenColumns.Add(x => x.Allocated);
+        HiddenColumns.Add(x => x.Issued);
+
+        var qtyColumn = AddDoubleColumn(x => x.Qty, "Qty.");
+        qtyColumn.GetSummary = () =>
+        {
+            return new GridSummaryColumn
+            {
+                Name = "Qty",
+                Format = "{Sum:F2}",
+                MappingName = "Qty",
+                SummaryType = Syncfusion.Data.SummaryType.Custom,
+                CustomAggregate = new QtyAggregate()
+            };
+        };
+
+        InStockColumn = AddDoubleColumn(x => x.InStock, "Stk.");
+        OnOrderColumn = AddDoubleColumn(x => x.OnOrder, "Ord.");
+        TreatmentRequiredColumn = AddDoubleColumn(x => x.TreatmentRequired, "Req.");
+        TreatmentOnOrderColumn = AddDoubleColumn(x => x.TreatmentOnOrder, "Ord.");
+        AllocatedColumn = AddDoubleColumn(x => x.Allocated, "Stk.");
+        IssuedColumn = AddDoubleColumn(x => x.Issued, "Iss.");
+
+        ColumnsTag = "JobRequisitionReview";
+    }
+
+    private class QtyAggregate : ISummaryAggregate
+    {
+        public double Sum { get; private set; }
+
+        public Action<IEnumerable, string, PropertyDescriptor> CalculateAggregateFunc()
+        {
+            return AggregateFunc;
+        }
+
+        private void AggregateFunc(IEnumerable items, string property, PropertyDescriptor args)
+        {
+            if (items is IEnumerable<DataRowView> rows)
+            {
+                Sum = 0;
+                foreach (var dataRow in rows)
+                {
+                    Sum += (double)dataRow["Qty"];
+                }
+            }
+            else
+            {
+                Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'.");
+            }
+        }
+    }
+
+    private DynamicActionColumn AddDoubleColumn(Expression<Func<JobRequisitionItem, object>> property, string header)
+    {
+        var col = new DynamicTextColumn<JobRequisitionItem>(property) { Format = "F2", HeaderText = header, Width = 50 };
+        ActionColumns.Add(col);
+        return col;
+    }
+
+    protected override IDynamicGridUIComponent<JobRequisitionItem> CreateUIComponent()
+    {
+        return new UIComponent(this);
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+
+        options.Clear();
+        options.FilterRows = true;
+        options.SelectColumns = true;
+        options.RecordCount = true;
+    }
+
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var columns = new DynamicGridColumns();
+        columns.Add<JobRequisitionItem, DateTime>(x => x.Requisition.DueDate, 80, "Due", "", Alignment.MiddleCenter);
+        columns.Add<JobRequisitionItem, string>(x => x.Requisition.Job.JobNumber, 70, "Job", "", Alignment.MiddleCenter);
+        columns.Add<JobRequisitionItem, int>(x => x.Requisition.Number, 50, "Requi", "", Alignment.MiddleCenter);
+        columns.Add<JobRequisitionItem, string>(x => x.Product.Code, 100, "Product Code", "", Alignment.MiddleLeft);
+        columns.Add<JobRequisitionItem, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
+        columns.Add<JobRequisitionItem, string>(x => x.Style.Code, 100, "Style", "", Alignment.MiddleLeft);
+        columns.Add<JobRequisitionItem, string>(x => x.Dimensions.UnitSize, 70, "Size", "", Alignment.MiddleLeft);
+        columns.Add<JobRequisitionItem, JobRequisitionItemStatus>(x => x.Status, 90, "Status", "", Alignment.MiddleCenter);
+
+        return columns;
+    }
+
+    protected override void ConfigureColumnGroups()
+    {
+        GetColumnGrouping()
+            .AddGroup("Stock", InStockColumn, OnOrderColumn)
+            .AddGroup("Treatment", TreatmentRequiredColumn, TreatmentOnOrderColumn)
+            .AddGroup("Allocated", AllocatedColumn, IssuedColumn);
+    }
+}

+ 78 - 36
prs.desktop/Panels/Jobs/Summary/JobSummaryGrid.cs

@@ -13,6 +13,7 @@ using System.Linq.Expressions;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media;
+using PRSDesktop.Panels.Jobs.Summary;
 
 namespace PRSDesktop;
 
@@ -66,10 +67,13 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         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).DimensionEquals(material.Dimensions);
+            var filter = new Filter<StockHolding>(x => x.Product.ID).IsEqualTo(material.Product.ID);
             if (StyleColumnVisible())
                 filter = filter.And(x => x.Style.ID).IsEqualTo(material.Style.ID);
+
+            if(DimensionsColumnVisible())
+                filter = filter.And(x => x.Dimensions).DimensionEquals(material.Dimensions);
+
             var columns = Columns.None<StockHolding>().Add(x => x.Location.ID)
                 .Add(x => x.Product.ID)
                 .Add(x => x.Style.ID)
@@ -248,6 +252,13 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         return VisibleColumns.Any(x => x.ColumnName.StartsWith(styleColumn));
     }
 
+    private bool DimensionsColumnVisible()
+    {
+        var dimColumn = CoreUtils.GetFullPropertyName<JobMaterial, StockDimensions>(x => x.Dimensions, ".");
+
+        return VisibleColumns.Any(x => x.ColumnName.StartsWith(dimColumn));
+    }
+
     public override DynamicGridColumns GenerateColumns()
     {
         var columns = new DynamicGridColumns();
@@ -285,7 +296,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
 
         var productID = item.Product.ID;
         Guid? styleID = StyleColumnVisible() ? item.Style.ID : null;
-        var dimensions = item.Dimensions;
+        var dimensions = DimensionsColumnVisible() ? item.Dimensions : null;
 
         /*ShowDetailGrid<StockMovement>(
             args.Column.ColumnName,
@@ -324,8 +335,9 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
 
         grid.OnDefineFilter += t =>
         {
-            var filter = new Filter<StockMovement>(x => x.Product.ID).IsEqualTo(productID)
-                .And(x => x.Dimensions).DimensionEquals(dimensions);
+            var filter = new Filter<StockMovement>(x => x.Product.ID).IsEqualTo(productID);
+            if(dimensions is not null)
+                filter = filter.And(x => x.Dimensions).DimensionEquals(dimensions);
             if (styleID.HasValue)
                 filter = filter.And(x => x.Style.ID).IsEqualTo(styleID);
             filter = filter.And(x => x.Job.ID).IsEqualTo(Master?.ID ?? Guid.Empty);
@@ -343,7 +355,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         Expression<Func<TEntity, object?>> stylecol,
         Guid? styleid,
         Expression<Func<TEntity, IDimensions>> dimcol,
-        IDimensions dimensions,
+        IDimensions? dimensions,
         Expression<Func<TEntity, object?>>? jobcol,
         Filter<TEntity>? extrafilter,
         Func<CoreRow, bool>? rowfilter
@@ -364,8 +376,9 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         });
         grid.OnDefineFilter += t =>
         {
-            var filter = new Filter<TEntity>(productcol).IsEqualTo(productid)
-                .And(CoreUtils.GetFullPropertyName(dimcol, ".")).DimensionEquals(dimensions);
+            var filter = new Filter<TEntity>(productcol).IsEqualTo(productid);
+            if(dimensions is not null)
+                filter = filter.And(CoreUtils.GetFullPropertyName(dimcol, ".")).DimensionEquals(dimensions);
             if (styleid.HasValue)
                 filter = filter.And(stylecol).IsEqualTo(styleid);
 
@@ -400,7 +413,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             x => x.Job.ID,
             new Filter<JobBillOfMaterialsItem>(x => x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue),
             null
@@ -409,18 +422,31 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
     private void ViewRequisitions(CoreRow row)
     {
         var item = row.ToObject<JobMaterial>();
-        ShowDetailGrid<JobRequisitionItem>(
-            RequisitionsColumn.Property,
-            x => x.Product.ID,
-            item.Product.ID,
-            x => x.Style.ID,
-            StyleColumnVisible() ? item.Style.ID : null,
-            x => x.Dimensions,
-            item.Dimensions,
-            x => x.Job.ID,
-            new Filter<JobRequisitionItem>(x => x.Requisition.Approved).IsNotEqualTo(DateTime.MinValue),
-            null
-        );
+
+        Guid? styleID = StyleColumnVisible() ? item.Style.ID : null;
+        var dimensions = DimensionsColumnVisible() ? item.Dimensions : null;
+
+        var grid = new JobRequisitionItemSummaryGrid();
+        grid.OnDefineFilter += t =>
+        {
+            var filter = new Filter<JobRequisitionItem>(x => x.Product.ID).IsEqualTo(item.Product.ID)
+                .And(x => x.Requisition.Approved).IsNotEqualTo(DateTime.MinValue)
+                .And(x => x.Cancelled).IsEqualTo(DateTime.MinValue);
+
+            if(dimensions is not null)
+            {
+                filter = filter.And(x => x.Dimensions).DimensionEquals(item.Dimensions);
+            }
+            if (styleID.HasValue)
+            {
+                filter = filter.And(x => x.Style.ID).IsEqualTo(styleID.Value);
+            }
+            filter = filter.And(x => x.Requisition.Job.ID).IsEqualTo(Master?.ID ?? Guid.Empty);
+
+            return filter;
+        };
+        var window = DynamicGridUtils.CreateGridWindow($"Viewing Requisition Calculation", grid);
+        window.ShowDialog();
     }
     private void ViewPickingLists(CoreRow row)
     {
@@ -432,7 +458,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             x => x.JobLink.ID,
             new Filter<RequisitionItem>(x => x.RequisitionLink.Filled).IsEqualTo(DateTime.MinValue),
             null
@@ -448,7 +474,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             x => x.Job.ID,
             new Filter<StockMovement>(x => x.Type).IsEqualTo(StockMovementType.Issue),
             null
@@ -464,7 +490,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             x => x.Job.ID,
             new Filter<StockHolding>(x => x.Units).IsGreaterThan(0.1),
             null
@@ -480,7 +506,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             x => x.Job.ID,
             null,
             null
@@ -496,7 +522,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             null,
             new Filter<StockHolding>(x => x.Units).IsNotEqualTo(0.0F)
                 .And(
@@ -517,7 +543,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
             x => x.Style.ID,
             StyleColumnVisible() ? item.Style.ID : null,
             x => x.Dimensions,
-            item.Dimensions,
+            DimensionsColumnVisible() ? item.Dimensions : null,
             null,
 
             IncludeReserves
@@ -595,7 +621,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         return result;
     }
 
-    private class Key(Guid jobID, Guid productID, Guid? styleID, IDimensions dimensions)
+    private class Key(Guid jobID, Guid productID, Guid? styleID, IDimensions? dimensions)
     {
         public Guid JobID { get; set; } = jobID;
 
@@ -603,21 +629,35 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
 
         public Guid? StyleID { get; set; } = styleID;
 
-        public IDimensions Dimensions { get; set; } = dimensions;
+        public IDimensions? Dimensions { get; set; } = dimensions;
+
+        public override bool Equals(object? obj)
+        {
+            return obj is Key key
+                && JobID == key.JobID
+                && ProductID == key.ProductID
+                && StyleID == key.StyleID
+                && Equals(Dimensions, key.Dimensions);
+        }
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(
+                JobID, ProductID, StyleID, Dimensions);
+        }
     }
 
-    private Key[] GetKeys(IEnumerable<CoreRow> rows, Columns<JobMaterial> columns, bool hasstyle)
+    private Key[] GetKeys(IEnumerable<CoreRow> rows, Columns<JobMaterial> columns, bool hasstyle, bool hasDimensions)
     {
         int jobcol = columns.IndexOf(x => x.Job.ID);
         int productcol = columns.IndexOf(x => x.Product.ID);
         int stylecol = hasstyle ? columns.IndexOf(x => x.Style.ID) : -1;
-        var dimCols = Dimensions.GetFilterColumnIndices<JobMaterial>(columns, x => x.Dimensions);
+        var dimCols = hasDimensions ? Dimensions.GetFilterColumnIndices<JobMaterial>(columns, x => x.Dimensions) : null;
 
         var result = rows.Select(r => new Key(
             (Guid)(r.Values[jobcol] ?? Guid.Empty),
             (Guid)(r.Values[productcol] ?? Guid.Empty),
             (stylecol != -1) ? (Guid)(r.Values[stylecol] ?? Guid.Empty) : null,
-            r.ToDimensions<StockDimensions>(dimCols))
+            dimCols is not null ? r.ToDimensions<StockDimensions>(dimCols) : null)
         ).Distinct().ToArray();
 
         return result;
@@ -637,7 +677,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
                 (!jobID.HasValue || Guid.Equals(jobID, r.Values[jobcol]))
                 && Guid.Equals(key.ProductID, r.Values[productcol])
                 && (!key.StyleID.HasValue || Guid.Equals(key.StyleID, r.Values[stylecol]))
-                && key.Dimensions.Equals(r.ToDimensions<StockDimensions>(dimCols))
+                && (key.Dimensions is null || key.Dimensions.Equals(r.ToDimensions<StockDimensions>(dimCols)))
                 && ((extrafilter == null) || extrafilter(r))
             );
 
@@ -661,7 +701,8 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
         Action<CoreTable?, Exception?> action)
     {
 
-        var filter = MasterDetailFilter;
+        criteria.Add(MasterDetailFilter);
+        criteria.Add(FilterComponent.GetFilter());
 
         var orderby = sort;
 
@@ -671,7 +712,7 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
                 var table = new CoreTable();
                 table.LoadColumns(columns);
 
-                var data = new Client<JobMaterial>().Query(filter, columns, orderby);
+                var data = new Client<JobMaterial>().Query(criteria.Combine(), columns, orderby);
                 var pids = data.ExtractValues<JobMaterial, Guid>(x => x.Product.ID).ToArray();
 
                 if (pids.Any())
@@ -705,7 +746,8 @@ internal class JobSummaryGrid : DynamicDataGrid<JobMaterial>, IMasterDetailContr
                     var freeordercolumns = Columns.None<PurchaseOrderItem>().Add(freeorders.Columns.Select(x => x.ColumnName));
 
                     var hasStyle = StyleColumnVisible();
-                    var keys = GetKeys(data.Rows, columns, hasStyle);
+                    var hasDimensions = DimensionsColumnVisible();
+                    var keys = GetKeys(data.Rows, columns, hasStyle, hasDimensions);
 
                     foreach (var key in keys)
                     {