|
@@ -22,9 +22,27 @@ using System.Windows.Controls;
|
|
|
using System.Windows.Media;
|
|
|
using System.Windows.Media.Imaging;
|
|
|
using Columns = InABox.Core.Columns;
|
|
|
+using PRSDesktop.Panels.StockForecast.OrderScreen;
|
|
|
|
|
|
namespace PRSDesktop;
|
|
|
|
|
|
+public class StockForecastBreakupKey(Guid jobID, Guid requiID)
|
|
|
+{
|
|
|
+ public Guid JobID { get; set; } = jobID != Guid.Empty ? jobID : throw new ArgumentException("jobID cannot be Guid.Empty!", "jobID");
|
|
|
+
|
|
|
+ public Guid JobRequiItemID { get; set; } = requiID;
|
|
|
+
|
|
|
+ public override bool Equals(object? obj)
|
|
|
+ {
|
|
|
+ return obj is StockForecastBreakupKey key && key.JobID == JobID && key.JobRequiItemID == JobRequiItemID;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override int GetHashCode()
|
|
|
+ {
|
|
|
+ return HashCode.Combine(JobID, JobRequiItemID);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
public class StockForecastOrderData(ProductLink product, ProductStyleLink style, StockDimensions dimensions)
|
|
|
{
|
|
|
public ProductLink Product { get; set; } = product;
|
|
@@ -66,17 +84,19 @@ public class StockForecastOrderingItemQuantity
|
|
|
{
|
|
|
public event Action? Changed;
|
|
|
|
|
|
- private double _total;
|
|
|
- public double Total
|
|
|
+ private double _stockTotal;
|
|
|
+ public double StockTotal
|
|
|
{
|
|
|
- get => _total;
|
|
|
+ get => _stockTotal;
|
|
|
set
|
|
|
{
|
|
|
- _total = value;
|
|
|
+ _stockTotal = value;
|
|
|
Changed?.Invoke();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ public Dictionary<StockForecastBreakupKey, double> JobTotals { get; init; } = [];
|
|
|
+
|
|
|
private SupplierProduct? _supplierProduct;
|
|
|
/// <summary>
|
|
|
/// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column.
|
|
@@ -95,6 +115,12 @@ public class StockForecastOrderingItemQuantity
|
|
|
{
|
|
|
Changed?.Invoke();
|
|
|
}
|
|
|
+
|
|
|
+ public double JobTotal => JobTotals.Values.Sum();
|
|
|
+
|
|
|
+ public double GetTotal(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
|
|
|
+ ? StockTotal
|
|
|
+ : JobTotal;
|
|
|
}
|
|
|
|
|
|
public class StockForecastOrderingItem : BaseObject
|
|
@@ -118,6 +144,16 @@ public class StockForecastOrderingItem : BaseObject
|
|
|
[EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
|
|
|
public SupplierProductOrderStrategy OrderStrategy { get; set; }
|
|
|
|
|
|
+ private Dictionary<StockForecastBreakupKey, double> JobRequiredQuantities { get; set; } = [];
|
|
|
+ public Dictionary<StockForecastBreakupKey, double> GetJobRequiredQuantities()
|
|
|
+ {
|
|
|
+ return JobRequiredQuantities;
|
|
|
+ }
|
|
|
+ public void SetJobRequiredQuantity(Guid jobID, Guid requiID, double requiredQuantity)
|
|
|
+ {
|
|
|
+ JobRequiredQuantities[new(jobID, requiID)] = requiredQuantity;
|
|
|
+ }
|
|
|
+
|
|
|
public bool CustomStrategy { get; set; } = false;
|
|
|
|
|
|
public StockForecastOrderingItemQuantity[] Quantities = [];
|
|
@@ -125,8 +161,8 @@ public class StockForecastOrderingItem : BaseObject
|
|
|
public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i];
|
|
|
|
|
|
public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
|
|
|
- ? Quantities.Sum(x => x.Total)
|
|
|
- : Quantities.Sum(x => x.Total);
|
|
|
+ ? Quantities.Sum(x => x.StockTotal)
|
|
|
+ : Quantities.Sum(x => x.JobTotal);
|
|
|
|
|
|
public void SetQuantities(StockForecastOrderingItemQuantity[] quantities)
|
|
|
{
|
|
@@ -175,7 +211,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
private DynamicActionColumn[] QuantityColumns = [];
|
|
|
private DynamicActionColumn[] CostColumns = [];
|
|
|
|
|
|
- //private readonly Dictionary<Guid, Job> JobDetails = [];
|
|
|
+ private readonly Dictionary<Guid, Job> JobDetails = [];
|
|
|
+ private readonly Dictionary<Guid, JobRequisitionItem> JobRequiDetails = [];
|
|
|
|
|
|
private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
|
|
|
|
|
@@ -192,6 +229,10 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
{
|
|
|
CalculateQuantities(true);
|
|
|
UIComponent.UpdateOrderType(OrderType);
|
|
|
+ foreach(var control in QuantityControls)
|
|
|
+ {
|
|
|
+ control.UpdateControl(OrderType);
|
|
|
+ }
|
|
|
|
|
|
Refresh(true, true);
|
|
|
}
|
|
@@ -248,16 +289,22 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- if(qty.Total > 0)
|
|
|
+ if(OrderType == StockForecastOrderingType.StockOrder && qty.StockTotal > 0)
|
|
|
{
|
|
|
- if(OrderType == StockForecastOrderingType.StockOrder && qty.Total > 0)
|
|
|
- {
|
|
|
- yield return new(supplier, new List<StockForecastOrderData.QuantityBreakup>() , item, qty.Total, qty.SupplierProduct);
|
|
|
- }
|
|
|
- else
|
|
|
+ yield return new(supplier, new(), item, qty.StockTotal, qty.SupplierProduct);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var breakups = new List<StockForecastOrderData.QuantityBreakup>();
|
|
|
+ foreach(var (key, q) in qty.JobTotals)
|
|
|
{
|
|
|
- yield return new(supplier, item.Breakups, item, qty.Total, qty.SupplierProduct);
|
|
|
+ // Check JobID because we are to skip the empty job (this is just the difference between all the allocations and the quantity on the PO).
|
|
|
+ if(q > 0 && key.JobID != Guid.Empty)
|
|
|
+ {
|
|
|
+ breakups.Add(new(key.JobID, key.JobRequiItemID, "", q));
|
|
|
+ }
|
|
|
}
|
|
|
+ yield return new(supplier, breakups, item, qty.JobTotal, qty.SupplierProduct);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -394,8 +441,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
|
|
|
Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
|
|
|
|
|
|
- //LoadJobData(OrderData.SelectMany(x => x.GetRequiredQuantities().Keys).Distinct().Where(x => x != Guid.Empty));
|
|
|
-
|
|
|
CalculateQuantities(true);
|
|
|
|
|
|
_loadedData = true;
|
|
@@ -415,13 +460,19 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
return qty;
|
|
|
}
|
|
|
|
|
|
- private SupplierProduct CalculateSupplierProduct(StockForecastOrderingItem item, int supplierIdx)
|
|
|
+ private SupplierProduct? CalculateSupplierProduct(StockForecastOrderingItem item, int supplierIdx)
|
|
|
{
|
|
|
var supplierProduct = SelectSupplierProduct(SupplierProducts.Where(x => x.Product.ID == item.Product.ID && x.Style.ID == item.Style.ID && x.SupplierLink.ID == Suppliers[supplierIdx].ID), item);
|
|
|
|
|
|
var qty = item.GetQuantity(supplierIdx);
|
|
|
qty.SupplierProduct = supplierProduct;
|
|
|
- qty.Total = 0;
|
|
|
+ qty.StockTotal = 0;
|
|
|
+ qty.JobTotals.Clear();
|
|
|
+ foreach(var id in item.GetJobRequiredQuantities().Keys)
|
|
|
+ {
|
|
|
+ qty.JobTotals[id] = 0;
|
|
|
+ }
|
|
|
+
|
|
|
return supplierProduct;
|
|
|
}
|
|
|
|
|
@@ -444,7 +495,19 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
.FirstOrDefault(x => x.Value.ID == selectedSupplierProduct.SupplierLink.ID, new KeyValuePair<int, SupplierLink>(-1, null)).Key;
|
|
|
if(supplierIdx != -1)
|
|
|
{
|
|
|
- item.GetQuantity(supplierIdx).Total = GetRequiredQuantity(item, selectedSupplierProduct);
|
|
|
+ var qty = item.GetQuantity(supplierIdx);
|
|
|
+ if(OrderType == StockForecastOrderingType.StockOrder)
|
|
|
+ {
|
|
|
+ qty.StockTotal = GetRequiredQuantity(item, selectedSupplierProduct, item.RequiredQuantity);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ qty.JobTotals.Clear();
|
|
|
+ foreach(var (id, q) in item.GetJobRequiredQuantities())
|
|
|
+ {
|
|
|
+ qty.JobTotals[id] = GetRequiredQuantity(item, selectedSupplierProduct, q);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -463,18 +526,10 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
item.Style.CopyFrom(dataItem.Style);
|
|
|
item.Dimensions.CopyFrom(dataItem.Dimensions);
|
|
|
item.OrderStrategy = ForecastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
|
|
|
-
|
|
|
- if(OrderType == StockForecastOrderingType.StockOrder)
|
|
|
+ item.RequiredQuantity = dataItem.RequiredQuantity;
|
|
|
+ foreach(var breakup in dataItem.GetRequiredQuantities())
|
|
|
{
|
|
|
- item.RequiredQuantity = dataItem.RequiredQuantity;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- foreach(var breakup in dataItem.GetRequiredQuantities())
|
|
|
- {
|
|
|
- item.RequiredQuantity += breakup.Quantity;
|
|
|
- item.Breakups.Add(breakup);
|
|
|
- }
|
|
|
+ item.SetJobRequiredQuantity(breakup.JobID, breakup.JobRequiItemID, breakup.Quantity);
|
|
|
}
|
|
|
Items.Add(item);
|
|
|
}
|
|
@@ -536,18 +591,18 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private double GetRequiredQuantity(StockForecastOrderingItem item, SupplierProduct supplierProduct)
|
|
|
+ private double GetRequiredQuantity(StockForecastOrderingItem item, SupplierProduct supplierProduct, double requiredQuantity)
|
|
|
{
|
|
|
switch (item.OrderStrategy)
|
|
|
{
|
|
|
case SupplierProductOrderStrategy.Exact:
|
|
|
- return item.RequiredQuantity;
|
|
|
+ return requiredQuantity;
|
|
|
case SupplierProductOrderStrategy.RoundUp:
|
|
|
- return Math.Ceiling(item.RequiredQuantity);
|
|
|
+ return Math.Ceiling(requiredQuantity);
|
|
|
case SupplierProductOrderStrategy.LowestOverallPrice:
|
|
|
case SupplierProductOrderStrategy.LowestUnitPrice:
|
|
|
case SupplierProductOrderStrategy.LowestOverstock:
|
|
|
- return Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / (supplierProduct.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : supplierProduct.Dimensions.Value));
|
|
|
+ return Math.Ceiling(requiredQuantity * item.Dimensions.Value / (supplierProduct.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : supplierProduct.Dimensions.Value));
|
|
|
default:
|
|
|
return 0.0;
|
|
|
}
|
|
@@ -684,7 +739,17 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
var newQty = CreateQuantity(itemIdx);
|
|
|
|
|
|
quantities[newIdx] = newQty;
|
|
|
- newQty.Total = 0;
|
|
|
+ if(OrderType == StockForecastOrderingType.StockOrder)
|
|
|
+ {
|
|
|
+ newQty.StockTotal = 0;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ foreach(var id in item.GetJobRequiredQuantities().Keys)
|
|
|
+ {
|
|
|
+ newQty.JobTotals[id] = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
item.SetQuantities(quantities);
|
|
|
}
|
|
|
|
|
@@ -718,6 +783,39 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private void LoadJobData(IEnumerable<Guid> ids)
|
|
|
+ {
|
|
|
+ var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray();
|
|
|
+ if(neededIDs.Length > 0)
|
|
|
+ {
|
|
|
+ var details = Client.Query(
|
|
|
+ new Filter<Job>(x => x.ID).InList(neededIDs),
|
|
|
+ Columns.None<Job>().Add(x => x.ID)
|
|
|
+ .Add(x => x.JobNumber)
|
|
|
+ .Add(x => x.Name));
|
|
|
+ foreach(var job in details.ToObjects<Job>())
|
|
|
+ {
|
|
|
+ JobDetails[job.ID] = job;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private void LoadJobRequiData(IEnumerable<Guid> ids)
|
|
|
+ {
|
|
|
+ var neededIDs = ids.Where(x => x != Guid.Empty && !JobRequiDetails.ContainsKey(x)).ToArray();
|
|
|
+ if(neededIDs.Length > 0)
|
|
|
+ {
|
|
|
+ var details = Client.Query(
|
|
|
+ new Filter<JobRequisitionItem>(x => x.ID).InList(neededIDs),
|
|
|
+ Columns.None<JobRequisitionItem>().Add(x => x.ID)
|
|
|
+ .Add(x => x.Requisition.Number)
|
|
|
+ .Add(x => x.Requisition.Description));
|
|
|
+ foreach(var requi in details.ToObjects<JobRequisitionItem>())
|
|
|
+ {
|
|
|
+ JobRequiDetails[requi.ID] = requi;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private class QuantityControl : ContentControl
|
|
|
{
|
|
|
private readonly StockForecastOrderingItem Item;
|
|
@@ -742,21 +840,105 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Otherwise, simple quantity textbox editor.
|
|
|
- var editor = new DoubleTextBox
|
|
|
+ if(mode == StockForecastOrderingType.StockOrder)
|
|
|
{
|
|
|
- VerticalAlignment = VerticalAlignment.Stretch,
|
|
|
- HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
|
- Background = new SolidColorBrush(Colors.LightYellow),
|
|
|
- BorderThickness = new Thickness(0.0),
|
|
|
- MinValue = 0.0,
|
|
|
- Value = Item.GetQuantity(SupplierIndex).Total
|
|
|
- };
|
|
|
- editor.ValueChanged += (o, e) =>
|
|
|
+ // Otherwise, simple quantity textbox editor.
|
|
|
+ var editor = new DoubleTextBox
|
|
|
+ {
|
|
|
+ VerticalAlignment = VerticalAlignment.Stretch,
|
|
|
+ HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
|
+ Background = new SolidColorBrush(Colors.LightYellow),
|
|
|
+ BorderThickness = new Thickness(0.0),
|
|
|
+ MinValue = 0.0,
|
|
|
+ Value = Item.GetQuantity(SupplierIndex).StockTotal
|
|
|
+ };
|
|
|
+ editor.ValueChanged += (o, e) =>
|
|
|
+ {
|
|
|
+ Item.GetQuantity(SupplierIndex).StockTotal = editor.Value ?? default;
|
|
|
+ };
|
|
|
+ Content = editor;
|
|
|
+ }
|
|
|
+ else if(mode == StockForecastOrderingType.Breakup)
|
|
|
{
|
|
|
- Item.GetQuantity(SupplierIndex).Total = editor.Value ?? default;
|
|
|
- };
|
|
|
- Content = editor;
|
|
|
+ var grid = new Grid();
|
|
|
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
|
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
|
|
|
+
|
|
|
+ var editor = new TextBox
|
|
|
+ {
|
|
|
+ VerticalAlignment = VerticalAlignment.Stretch,
|
|
|
+ HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
|
+ VerticalContentAlignment = VerticalAlignment.Center,
|
|
|
+ HorizontalContentAlignment = HorizontalAlignment.Center,
|
|
|
+ Background = new SolidColorBrush(Colors.White),
|
|
|
+ BorderThickness = new Thickness(0.0),
|
|
|
+ IsReadOnly = true,
|
|
|
+ Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal)
|
|
|
+ };
|
|
|
+ Grid.SetColumn(editor, 0);
|
|
|
+ grid.Children.Add(editor);
|
|
|
+
|
|
|
+ var btn = new Button
|
|
|
+ {
|
|
|
+ VerticalAlignment = VerticalAlignment.Stretch,
|
|
|
+ VerticalContentAlignment = VerticalAlignment.Center,
|
|
|
+ HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
|
+ Content = "..",
|
|
|
+ Margin = new Thickness(1),
|
|
|
+ Focusable = false
|
|
|
+ };
|
|
|
+ btn.SetValue(Grid.ColumnProperty, 1);
|
|
|
+ btn.SetValue(Grid.RowProperty, 0);
|
|
|
+ btn.Click += (o, e) =>
|
|
|
+ {
|
|
|
+ var qty = Item.GetQuantity(SupplierIndex);
|
|
|
+
|
|
|
+ Parent.LoadJobData(qty.JobTotals.Keys.Select(x => x.JobID));
|
|
|
+ Parent.LoadJobRequiData(qty.JobTotals.Keys.Select(x => x.JobRequiItemID));
|
|
|
+
|
|
|
+ var items = qty.JobTotals.Select(x =>
|
|
|
+ {
|
|
|
+ var item = new StockForecastOrderingJobItem
|
|
|
+ {
|
|
|
+ JobID = x.Key.JobID,
|
|
|
+ JobRequiID = x.Key.JobRequiItemID,
|
|
|
+ RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key),
|
|
|
+ Quantity = x.Value
|
|
|
+ };
|
|
|
+ if(item.JobID == Guid.Empty)
|
|
|
+ {
|
|
|
+ item.Description = "General Stock";
|
|
|
+ }
|
|
|
+ else if(Parent.JobDetails.TryGetValue(item.JobID, out var job))
|
|
|
+ {
|
|
|
+ if(Parent.JobRequiDetails.TryGetValue(item.JobRequiID, out var requi))
|
|
|
+ {
|
|
|
+ item.Description = $"{job.JobNumber}: Requi #{requi.Requisition.Number} ({requi.Requisition.Description})";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ item.Description = $"{job.JobNumber}: {job.Name}";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return item;
|
|
|
+ }).ToList();
|
|
|
+
|
|
|
+ var window = new StockForecastOrderJobScreen();
|
|
|
+ window.Items = items;
|
|
|
+ if(window.ShowDialog() == true)
|
|
|
+ {
|
|
|
+ foreach(var item in items)
|
|
|
+ {
|
|
|
+ qty.JobTotals[new(item.JobID, item.JobRequiID)] = item.Quantity;
|
|
|
+ }
|
|
|
+ qty.DoChanged();
|
|
|
+ editor.Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ grid.Children.Add(btn);
|
|
|
+
|
|
|
+ Content = grid;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -835,10 +1017,10 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
}
|
|
|
|
|
|
var instance = LoadItem(row);
|
|
|
- var qty = instance.GetQuantity(idx);//.Total;
|
|
|
+ var qty = instance.GetQuantity(idx);
|
|
|
if(qty.SupplierProduct is not null)
|
|
|
{
|
|
|
- return $"{qty.Total * qty.SupplierProduct.CostPrice:C2}";
|
|
|
+ return $"{qty.GetTotal(OrderType) * qty.SupplierProduct.CostPrice:C2}";
|
|
|
}
|
|
|
else
|
|
|
{
|
|
@@ -919,7 +1101,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
|
|
|
var qty = item.GetQuantity(SupplierIndex);
|
|
|
if(qty.SupplierProduct is not null)
|
|
|
{
|
|
|
- Sum += qty.Total * qty.SupplierProduct.CostPrice;
|
|
|
+ Sum += qty.GetTotal(Grid.OrderType) * qty.SupplierProduct.CostPrice;
|
|
|
}
|
|
|
}
|
|
|
}
|