Browse Source

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

frankvandenbos 5 months ago
parent
commit
f7c23db775

+ 2 - 1
PRS.Avalonia/PRS.Avalonia/Components/SelectionView/SelectionView.axaml

@@ -13,7 +13,8 @@
 			<RowDefinition Height="*"/>
 			<RowDefinition Height="Auto"/>
 		</Grid.RowDefinitions>
-		<components:ButtonStrip Name="Filters" Grid.Row="0" Items="{Binding FilterButtons}" SelectionChanged="Filters_OnSelectionChanged"/>
+		<components:ButtonStrip Name="Filters" Grid.Row="0" ItemsSource="{Binding FilterButtons}" SelectionChanged="Filters_OnSelectionChanged"
+								SelectedItem="{Binding SelectedFilter}"/>
 		<components:AvaloniaDataGrid Name="Grid" Grid.Row="1"
 									 ItemsSource="{Binding ItemsSource}"
 									 RefreshRequested="Grid_OnRefreshRequested"

+ 3 - 3
PRS.Avalonia/PRS.Avalonia/Components/SelectionView/SelectionView.axaml.cs

@@ -36,7 +36,7 @@ public partial class SelectionView : UserControl
         Columns_Changed(Model.Columns);
         Model.WhenPropertyChanged(x => x.SelectionPageMode).Subscribe(value =>
         {
-            Grid.SelectionMode = value.Value == SelectionPageMode.MultiSelect ? SelectionMode.Multiple : SelectionMode.Single;
+            Grid.SelectionMode = value.Value == SelectionPageMode.MultiSelect ? AvaloniaDataGridSelectionMode.Multiple : AvaloniaDataGridSelectionMode.Single;
         });
         Model.FilterButtons.CollectionChanged += FilterButtons_CollectionChanged;
     }
@@ -53,13 +53,13 @@ public partial class SelectionView : UserControl
 
     private void Filters_OnSelectionChanged(object sender, EventArgs e)
     {
-        var args = new SelectionViewRefreshArgs(true, Filters.SelectedItem?.Text);
+        var args = new SelectionViewRefreshArgs(true, Filters.SelectedItem as string);
         Model.RefreshCommand.Execute(args);
     }
 
     private void Grid_OnRefreshRequested(object sender, AvaloniaDataGridRefreshRequestedEventArgs e)
     {
-        var args = new SelectionViewRefreshArgs(true, Filters.SelectedItem?.Text);
+        var args = new SelectionViewRefreshArgs(true, Filters.SelectedItem as string);
         Model.RefreshCommand.Execute(args);
     }
     private void Grid_OnSelectionChanged(object sender, AvaloniaDataGridSelectionChangedEventArgs e)

+ 7 - 6
PRS.Avalonia/PRS.Avalonia/Components/SelectionView/SelectionViewModel.cs

@@ -42,7 +42,7 @@ public partial class SelectionViewModel : PopupViewModel<object?[]>, IModuleView
     [ObservableProperty]
     private SelectionPageMode _selectionPageMode = SelectionPageMode.Immediate;
 
-    public ObservableCollection<ButtonStripItem> FilterButtons { get; } = new();
+    public ObservableCollection<string> FilterButtons { get; } = new();
 
     public ObservableCollection<SelectionViewButton> Buttons { get; } = new();
 
@@ -67,6 +67,9 @@ public partial class SelectionViewModel : PopupViewModel<object?[]>, IModuleView
     [ObservableProperty]
     private Action<object?[]>? _selected;
 
+    [ObservableProperty]
+    private object? _selectedFilter;
+
     private object?[] _selectedItems = [];
 
     public SelectionViewModel()
@@ -78,10 +81,7 @@ public partial class SelectionViewModel : PopupViewModel<object?[]>, IModuleView
     {
         foreach(var filter in filters)
         {
-            FilterButtons.Add(new()
-            {
-                Text = filter
-            });
+            FilterButtons.Add(filter);
         }
     }
     public void AddFilters(IEnumerable<string> filters) => DoAddFilters(filters);
@@ -98,7 +98,7 @@ public partial class SelectionViewModel : PopupViewModel<object?[]>, IModuleView
     {
         return Task.Run(() =>
         {
-            var result = Refresh?.Invoke(new(false, FilterButtons.FirstOrDefault(x => x.Selected)?.Text ?? ""));
+            var result = Refresh?.Invoke(new(false, SelectedFilter as string));
             if (result is not null)
             {
                 if (result is ICoreRepository repository)
@@ -120,6 +120,7 @@ public partial class SelectionViewModel : PopupViewModel<object?[]>, IModuleView
         {
             LastUpdated = repository.LastUpdated;
         }
+        ItemsSource = null;
         ItemsSource = result;
     }
 

+ 1 - 0
PRS.Avalonia/PRS.Avalonia/Images/Images.cs

@@ -34,6 +34,7 @@ public static class Images
     public static SvgImage? barcode => LoadSVG("/Images/barcode.svg");
     public static SvgImage? books => LoadSVG("/Images/books.svg");
     public static SvgImage? camera => LoadSVG("/Images/camera.svg");
+    public static SvgImage? circle_gray => LoadSVG("/Images/circle_gray.svg");
     public static SvgImage? circle_green => LoadSVG("/Images/circle_green.svg");
     public static SvgImage? circle_red => LoadSVG("/Images/circle_red.svg");
     public static SvgImage? clock => LoadSVG("/Images/clock.svg");

+ 19 - 9
PRS.Avalonia/PRS.Avalonia/Modules/InOut/InOutView.axaml

@@ -7,13 +7,23 @@
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PRS.Avalonia.Modules.InOutView"
 			 x:DataType="local:InOutViewModel">
-	<components:AvaloniaDataGrid ItemsSource="{Binding ItemsSource}"
-								 Columns="{Binding Columns}"
-								 Margin="5"
-								 RefreshRequested="Grid_OnRefreshRequested"
-								 SelectionMode="AlwaysSelected"
-								 ShowRecordCount="True"
-								 CanSearch="True"
-								 RefreshVisible="True"
-								 LastUpdated="{Binding LastUpdated}"/>
+	<Grid Margin="2">
+		<Grid.RowDefinitions>
+			<RowDefinition Height="Auto"/>
+			<RowDefinition Height="*"/>
+		</Grid.RowDefinitions>
+		<components:ButtonStrip Name="Filters" Grid.Row="0" ItemsSource="{Binding FilterButtons}" SelectedCommand="{Binding FilterSelectedCommand}"
+								Margin="0,0,0,2" ItemSpacing="2"/>
+		<components:AvaloniaDataGrid Grid.Row="1"
+									 ItemsSource="{Binding ItemsSource}"
+									 Columns="{Binding Columns}"
+									 RefreshRequested="Grid_OnRefreshRequested"
+									 Focusable="False"
+									 SelectionMode="None"
+									 ShowRecordCount="True"
+									 CanSearch="True"
+									 RefreshVisible="True"
+									 LastUpdated="{Binding LastUpdated}"
+									 LoadingRow="Grid_LoadingRow"/>
+	</Grid>
 </UserControl>

+ 5 - 0
PRS.Avalonia/PRS.Avalonia/Modules/InOut/InOutView.axaml.cs

@@ -16,4 +16,9 @@ public partial class InOutView : UserControl
     {
         Model.RefreshCommand.Execute(null);
     }
+    
+    private void Grid_LoadingRow(object? sender, DataGridRowEventArgs e)
+    {
+        Model.LoadRowCommand.Execute(e.Row);
+    }
 }

+ 62 - 17
PRS.Avalonia/PRS.Avalonia/Modules/InOut/InOutViewModel.cs

@@ -3,16 +3,23 @@ using Avalonia.Media;
 using Comal.Classes;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
+using DynamicData;
+using InABox.Avalonia;
 using InABox.Avalonia.Components;
 using InABox.Core;
 using System;
 using System.Collections;
+using System.Collections.ObjectModel;
+using System.Linq;
 using System.Threading.Tasks;
 
 namespace PRS.Avalonia.Modules;
 
 public partial class InOutViewModel : ModuleViewModel
 {
+    const string AllFilter = "All";
+    const string NotInFilter = "Absent";
+
     public override string Title => "In/Out";
 
     [ObservableProperty]
@@ -21,6 +28,9 @@ public partial class InOutViewModel : ModuleViewModel
     [ObservableProperty]
     private IEnumerable? _itemsSource;
 
+    [ObservableProperty]
+    private ObservableCollection<object?> _filterButtons;
+
     [ObservableProperty]
     private DateTime _lastUpdated;
 
@@ -32,37 +42,33 @@ public partial class InOutViewModel : ModuleViewModel
         ProgressVisible = true;
 
         Columns = new AvaloniaDataGridColumns().BeginUpdate();
-        if (!Security.IsAllowed<CanViewMobileInOutBoardDetails>())
-        {
-            Columns.Add(new AvaloniaDataGridImageColumn<InOutShell>()
-            {
-                Column = x => x.In,
-                Caption = "In?",
-                // Header = circle_gray
-                 Margin = 6,
-                Width = new GridLength(30),
-            });
-        }
+        // Columns.Add(new AvaloniaDataGridImageColumn<InOutShell>()
+        // {
+        //     Column = x => x.In,
+        //     Header = Images.circle_gray,
+        //      Margin = 6,
+        //     Width = new GridLength(30),
+        // });
         Columns.Add(new AvaloniaDataGridTextColumn<InOutShell>
         {
             Column = x => x.Name,
             Alignment = TextAlignment.Start,
             Width = GridLength.Star
         });
-        if (Security.IsAllowed<CanViewMobileInOutBoardDetails>())
+        Columns.Add(new AvaloniaDataGridTextColumn<InOutShell>
         {
-            Columns.Add(new AvaloniaDataGridTimeColumn<InOutShell> { Column = x => x.Start, Width = new(50) });
-            Columns.Add(new AvaloniaDataGridTimeColumn<InOutShell> { Column = x => x.Finish, Width = new(50) });
-        }
+            Column = x => x.Status,
+            Alignment = TextAlignment.Start,
+            Width = GridLength.Star
+        });
 
         Columns.Add(new AvaloniaDataGridImageColumn<InOutShell>()
         {
             Column = x => x.Call,
-            Caption = "Call",
             Width = new GridLength(30),
             Tapped = CallEmployee,
             Margin = 6,
-            // Header = phone
+            Header = Images.phone
         });
 
         Columns.EndUpdate();
@@ -77,6 +83,9 @@ public partial class InOutViewModel : ModuleViewModel
     protected override async Task<TimeSpan> OnRefresh()
     {
         await Refresh();
+
+        FilterButtons = [AllFilter, NotInFilter, .. Model.AvailableFilters.Where(x => x.Name != "All")];
+
         return TimeSpan.Zero;
     }
 
@@ -92,4 +101,40 @@ public partial class InOutViewModel : ModuleViewModel
         LastUpdated = Model.LastUpdated;
         ProgressVisible = false;
     }
+
+    [RelayCommand]
+    private void LoadRow(DataGridRow row)
+    {
+        if (row.DataContext is not InOutShell shell) return;
+
+        row.Background = new SolidColorBrush(shell.Colour);
+    }
+
+    [RelayCommand]
+    private async Task FilterSelected(object? filter)
+    {
+        if (filter is string stringFilter)
+        {
+            if (Model.SelectedFilterName is not null)
+            {
+                Model.SelectFilter(null);
+                await Model.RefreshAsync(true);
+            }
+
+            if(stringFilter == AllFilter)
+            {
+                Model.Search(null);
+            }
+            else if(stringFilter == NotInFilter)
+            {
+                Model.Search(x => !x.IsClockedOn);
+            }
+        }
+        else if(filter is CoreRepositoryFilter coreFilter)
+        {
+            Model.SelectFilter(coreFilter.Name);
+            Model.SearchPredicate = null;
+            await Model.RefreshAsync(true);
+        }
+    }
 }

+ 171 - 18
PRS.Avalonia/PRS.Avalonia/Repositories/InOut/InOutModel.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
+using Avalonia.Media;
 using Comal.Classes;
 using InABox.Avalonia;
 using InABox.Core;
@@ -8,49 +10,200 @@ namespace PRS.Avalonia;
 
 public class InOutModel : CoreRepository<InOutModel, InOutShell, Employee>
 {
-    private Tuple<Guid, TimeSpan, TimeSpan>[] _statuses;
+    private Dictionary<Guid, TimeSheet[]> _timeSheets = [];
+    private StandardLeave[] _standardLeave = [];
+    private Dictionary<Guid, LeaveRequest[]> _leaveRequests = [];
+    private Dictionary<Guid, Activity> _activities = [];
+    private Dictionary<Guid, EmployeeRosterItem[]> _rosters = [];
 
     public InOutModel(IModelHost host, Func<Filter<Employee>> filter, Func<string>? filename = null) : base(host, filter, filename)
     {
     }
 
-    protected override void Initialize()
+    public class InOutStatus(string status, Color colour, bool clockedOn)
     {
-        base.Initialize();
-        _statuses = new Tuple<Guid, TimeSpan, TimeSpan>[] { };
-    }
+        public string Status { get; set; } = status;
 
-    public bool IsClockedOn(Guid id)
-    {
-        return _statuses.Any(x => x.Item1.Equals(id) && x.Item3.Equals(TimeSpan.Zero));
-    }
+        public Color Colour { get; set; } = colour;
 
-    public TimeSpan StartTime(Guid id)
-    {
-        return _statuses.FirstOrDefault(x => x.Item1.Equals(id))?.Item2 ?? TimeSpan.Zero;
+        public bool ClockedOn { get; set; } = clockedOn;
     }
 
-    public TimeSpan FinishTime(Guid id)
+    public InOutStatus StatusObject(InOutShell shell)
     {
-        return _statuses.FirstOrDefault(x => x.Item1.Equals(id))?.Item3 ?? TimeSpan.Zero;
+        var time = DateTime.Now.TimeOfDay;
+
+        TimeSheet? closedTimeSheet = null;
+        if(_timeSheets.TryGetValue(shell.ID, out var sheets))
+        {
+            var openTimeSheet = sheets.FirstOrDefault(x => x.Finish == TimeSpan.Zero || x.Finish > time);
+            if(openTimeSheet is not null)
+            {
+                if(_activities.TryGetValue(openTimeSheet.ActivityLink.ID, out var activity))
+                {
+                    return new(openTimeSheet.Address, Color.TryParse(activity.Color, out var colour) ? colour : Colors.LightGreen, true);
+                }
+                else
+                {
+                    return new(openTimeSheet.Address, Colors.LightGreen, true);
+                }
+            }
+            else
+            {
+                closedTimeSheet = sheets?.MaxBy(x => x.Start);
+                // Check leave and rosters before giving a result.
+            }
+        }
+        
+        if(_standardLeave.Length > 0)
+        {
+            var leave = _standardLeave.FirstOrDefault(x =>
+                (x.From < DateTime.Today || x.FromTime <= time) &&
+                (x.To > DateTime.Today || x.ToTime >= time));
+            if(leave is not null)
+            {
+                var activity = _activities.GetValueOrDefault(leave.LeaveType.ID);
+                return new(
+                    activity?.Description ?? "Leave",
+                    activity is not null && Color.TryParse(activity.Color, out var colour) ? colour : Colors.Gainsboro,
+                    false);
+            }
+        }
+
+        if(_leaveRequests.TryGetValue(shell.ID, out var leaveRequests))
+        {
+            var leave = leaveRequests.FirstOrDefault(x =>
+                (x.From < DateTime.Today || x.FromTime <= time) &&
+                (x.To > DateTime.Today || x.ToTime >= time));
+            if(leave is not null)
+            {
+                var activity = _activities.GetValueOrDefault(leave.LeaveType.ID);
+                return new(
+                    activity?.Description ?? "Leave",
+                    activity is not null && Color.TryParse(activity.Color, out var colour) ? colour : Colors.Gainsboro,
+                    false);
+            }
+        }
+
+        if(_rosters.TryGetValue(shell.ID, out var rosters))
+        {
+            var roster = RosterUtils.GetRoster(rosters, shell.RosterStart, DateTime.Today);
+            if(roster is null)
+            {
+                if (closedTimeSheet is not null)
+                {
+                    return new("Finished", Colors.Gainsboro, false);
+                }
+                else
+                {
+                    return new("Not rostered on", Colors.Gainsboro, false);
+                }
+            }
+            else
+            {
+                var block = RosterUtils.GetBlocks(roster, DateTime.Today, TimeSpan.MinValue, TimeSpan.MaxValue)
+                    .FirstOrDefault(x => x.Start <= time && time <= x.Finish);
+                if(block is null)
+                {
+                    if (closedTimeSheet is not null)
+                    {
+                        return new("Finished", Colors.Gainsboro, false);
+                    }
+                    else
+                    {
+                        return new("Not rostered on", Colors.Gainsboro, false);
+                    }
+                }
+                else if(closedTimeSheet is not null)
+                {
+                    if(closedTimeSheet.Finish >= block.Start)
+                    {
+                        return new("Finished early", Colors.LightSalmon, false);
+                    }
+                    else
+                    {
+                        return new("Overdue", Colors.LightSalmon, false);
+                    }
+                }
+                else
+                {
+                    return new("Overdue", Colors.LightSalmon, false);
+                }
+            }
+        }
+
+        if (closedTimeSheet is not null)
+        {
+            return new("Finished", Colors.Gainsboro, false);
+        }
+        else
+        {
+            return new("Not rostered on", Colors.Gainsboro, false);
+        }
     }
 
     protected override void BeforeLoad(MultiQuery query)
     {
         base.BeforeLoad(query);
-        query.Add<TimeSheet>(
+        query.Add(
             new Filter<TimeSheet>(x => x.Date).IsEqualTo(DateTime.Today),
             new Columns<TimeSheet>(ColumnTypeFlags.None).Add(x => x.EmployeeLink.ID)
                 .Add(x => x.Start)
                 .Add(x => x.Finish)
+                .Add(x => x.Address)
+                .Add(x => x.ActivityLink.ID)
+        );
+        query.Add(
+            new Filter<StandardLeave>(x => x.From).IsLessThanOrEqualTo(DateTime.Today)
+                .And(x => x.To).IsGreaterThanOrEqualTo(DateTime.Today),
+            Columns.None<StandardLeave>().Add(x => x.From)
+                .Add(x => x.FromTime)
+                .Add(x => x.To)
+                .Add(x => x.ToTime)
+                .Add(x => x.LeaveType.ID)
         );
+        
+        query.Add(
+            new Filter<LeaveRequest>(x => x.From).IsLessThanOrEqualTo(DateTime.Today)
+                .And(x => x.To).IsGreaterThanOrEqualTo(DateTime.Today)
+                .And(x => x.Status).IsNotEqualTo(LeaveRequestStatus.Rejected),
+            Columns.None<LeaveRequest>().Add(x => x.EmployeeLink.ID)
+                .Add(x => x.From)
+                .Add(x => x.FromTime)
+                .Add(x => x.To)
+                .Add(x => x.ToTime)
+                .Add(x => x.LeaveType.ID)
+        );
+        query.Add(
+            new Filter<EmployeeRosterItem>(x => x.Employee.ID).InQuery(EffectiveFilter(), x => x.ID),
+            sort: new SortOrder<EmployeeRosterItem>(x => x.Day));
+        query.Add(
+            columns: Columns.None<Activity>()
+                .Add(x => x.ID)
+                .Add(x => x.Color)
+                .Add(x => x.Description));
     }
 
     protected override void AfterLoad(MultiQuery query)
     {
+        _timeSheets = query.Get<TimeSheet>()
+            .ToObjects<TimeSheet>()
+            .GroupBy(x => x.EmployeeLink.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+        _standardLeave = query.Get<StandardLeave>()
+            .ToObjects<StandardLeave>().ToArray();
+        _leaveRequests = query.Get<LeaveRequest>()
+            .ToObjects<LeaveRequest>()
+            .GroupBy(x => x.EmployeeLink.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+        _rosters = query.Get<EmployeeRosterItem>()
+            .ToObjects<EmployeeRosterItem>()
+            .GroupBy(x => x.Employee.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+        _activities = query.Get<Activity>()
+            .ToObjects<Activity>().ToDictionary(x => x.ID);
+
+        // Needs to happen after the above things.
         base.AfterLoad(query);
-        _statuses = query.Get<TimeSheet>()
-            .ToTuples<TimeSheet, Guid, TimeSpan, TimeSpan>(x => x.EmployeeLink.ID, x => x.Start, x => x.Finish)
-            .ToArray();
     }
 }

+ 21 - 7
PRS.Avalonia/PRS.Avalonia/Repositories/InOut/InOutShell.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Media;
+using BruTile.Wms;
 using Comal.Classes;
 using InABox.Avalonia;
 
@@ -11,24 +12,37 @@ public class InOutShell : Shell<InOutModel, Employee>
     
     public string Mobile => Get<string>();
 
+    public string Status => StatusObject.Status;
+
+    public Color Colour => StatusObject.Colour;
+
     public IImage? Call => string.IsNullOrWhiteSpace(Mobile)
         ? null
         : Images.phone;
-
-    public TimeSpan Start => Parent.StartTime(ID);
-
-    public TimeSpan Finish => Parent.FinishTime(ID);
     
-    public bool IsClockedOn => Parent.IsClockedOn(ID);
+    public bool IsClockedOn => StatusObject.ClockedOn;
 
-    public IImage? In => Parent.IsClockedOn(ID)
+    public IImage? In => StatusObject.ClockedOn
         ? Images.circle_green
         : Images.circle_red;
 
+    public DateTime RosterStart => Get<DateTime>();
+
+    private InOutModel.InOutStatus? _statusObject;
+    private InOutModel.InOutStatus StatusObject
+    {
+        get
+        {
+            _statusObject ??= Parent.StatusObject(this);
+            return _statusObject;
+        }
+    }
+
     protected override void ConfigureColumns(ShellColumns<InOutModel, Employee> columns)
     {
         columns
             .Map(nameof(Name), x => x.Name)
-            .Map(nameof(Mobile), x => x.Mobile);
+            .Map(nameof(Mobile), x => x.Mobile)
+            .Map(nameof(RosterStart), x => x.RosterStart);
     }
 }

+ 2 - 1
prs.shared/Utilities/RosterUtils.cs → prs.classes/Utilities/RosterUtils.cs

@@ -1,9 +1,10 @@
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using Comal.Classes;
 using InABox.Core;
 
-namespace PRS.Shared
+namespace Comal.Classes
 {
 
     public class RosterBlock

+ 1 - 0
prs.desktop/Components/Calendar/Appointments/LeaveRequestAppointment.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Windows.Media.Imaging;
+using Comal.Classes;
 using InABox.WPF;
 using PRS.Shared;
 

+ 1 - 0
prs.desktop/Components/Calendar/Appointments/StandardLeaveAppointment.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Windows.Media.Imaging;
+using Comal.Classes;
 using InABox.WPF;
 using PRS.Shared;
 

+ 3 - 3
prs.desktop/Panels/Products/Locations/StockLocationGrid.cs

@@ -122,9 +122,9 @@ public class StockLocationGrid : DynamicDataGrid<StockLocation>
             //var bHasHoldings = items.FirstOrDefault().Holdings != 0;
             //editor.Editable = bHasHoldings ? Editable.Disabled : Editable.Enabled;
             //if (bHasHoldings)
-            var item = items?.FirstOrDefault();
-            if (item is not null)
-                item.Active = true;
+            // var item = items?.FirstOrDefault();
+            // if (item is not null)
+            //     item.Active = true;
         }
     }
 }

+ 11 - 10
prs.desktop/Panels/Reservation Management/ReservationManagementItemGrid.cs

@@ -411,23 +411,24 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
 
             var pois = new List<(PurchaseOrderItem poi, PurchaseOrderItemAllocation poia)>();
 
-            foreach (var jobRequisitionItem in jris)
+            foreach (var jri in jris)
             {
                 var poItem = new PurchaseOrderItem
                 {
-                    Description = jobRequisitionItem.Product.Name,
-                    Qty = Math.Max(jobRequisitionItem.Qty - (jobRequisitionItem.InStock + jobRequisitionItem.Issued), 0.0)
+                    Description = jri.Product.Name,
+                    Qty = Math.Max(jri.Qty - (jri.InStock + jri.Issued), 0.0)
                 };
-                poItem.Product.ID = jobRequisitionItem.Product.ID;
-                poItem.Product.Synchronise(jobRequisitionItem.Product);
-                poItem.Dimensions.CopyFrom(jobRequisitionItem.Dimensions);
-                poItem.Style.ID = jobRequisitionItem.Style.ID;
-                poItem.Style.Synchronise(jobRequisitionItem.Style);
+                poItem.Product.ID = jri.Product.ID;
+                poItem.Product.Synchronise(jri.Product);
+                poItem.Dimensions.CopyFrom(jri.Dimensions);
+                poItem.Style.ID = jri.Style.ID;
+                poItem.Style.Synchronise(jri.Style);
                 poItem.PurchaseOrderLink.ID = id;
 
                 var allocation = new PurchaseOrderItemAllocation();
-                allocation.Job.CopyFrom(jobRequisitionItem.Job);
-                allocation.JobRequisitionItem.CopyFrom(jobRequisitionItem);
+                allocation.Job.CopyFrom(jri.Job);
+                allocation.JobRequisitionItem.CopyFrom(jri);
+                allocation.Quantity = poItem.Qty;
 
                 pois.Add((poItem, allocation));
             }