using Comal.Classes;
using InABox.Configuration;
using InABox.Core;
using InABox.DynamicGrid;
using InABox.WPF;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Controls;
using InABox.Clients;
using InABox.Wpf;
using Columns = InABox.Core.Columns;
using PRSDesktop.Panels.ReservationManagement.TreatmentPO;
using Syncfusion.UI.Xaml.Grid;
namespace PRSDesktop;
///
/// Interaction logic for JobRequisitionsPanel.xaml
///
public partial class ReservationManagementPanel : UserControl, IPanel
{
private ReservationManagementGlobalSettings _globalSettings = null!; // Initialised in Setup()
public ReservationManagementPanel()
{
InitializeComponent();
PanelLink = new(this);
}
private void Reconfigure()
{
JobRequiItems.Reconfigure();
OnUpdateDataModel?.Invoke(SectionName, DataModel(Selection.None));
}
public bool IsReady { get; set; }
public string SectionName => "Job Requisitions";
public event DataModelUpdateEvent? OnUpdateDataModel;
public void CreateToolbarButtons(IPanelHost host)
{
ProductSetupActions.Standard(host);
host.CreateSetupAction(new PanelAction() { Caption = "Reservation Management Settings", Image = PRSDesktop.Resources.specifications, OnExecute = ConfigSettingsClick });
if (Security.IsAllowed())
host.CreatePanelAction(new PanelAction("Treatment PO", PRSDesktop.Resources.purchase, TreatmentPO_Click));
}
private void ConfigSettingsClick(PanelAction obj)
{
var grid = new DynamicItemsListGrid();
if(grid.EditItems(new ReservationManagementGlobalSettings[] { _globalSettings }))
{
new GlobalConfiguration().Save(_globalSettings);
JobRequiItems.CompanyDefaultStyle = _globalSettings.ProductStyle;
JobRequiItems.DueDateAlert = _globalSettings.DueDateAlert;
JobRequiItems.DueDateWarning = _globalSettings.DueDateWarning;
}
}
public DataModel DataModel(Selection selection)
{
var ids = JobRequiItems.ExtractValues(x => x.ID, selection).ToArray();
return new BaseDataModel(new Filter(x => x.ID).InList(ids));
}
public void Heartbeat(TimeSpan time)
{
}
public void Refresh()
{
JobRequiItems.Refresh(false, true);
}
public Dictionary Selected()
{
return new Dictionary
{
[typeof(JobRequisitionItem).EntityName()] = JobRequiItems.SelectedRows
};
}
public void Setup()
{
_globalSettings = new GlobalConfiguration().Load();
JobRequiItems.DueDateAlert = _globalSettings.DueDateAlert;
JobRequiItems.DueDateWarning = _globalSettings.DueDateWarning;
JobRequiItems.CompanyDefaultStyle = _globalSettings.ProductStyle;
JobRequiItems.Refresh(true, false);
}
// This class is to manage a reference back to this current panel; this link class is referred to by the SubPanels when
// a treatment PO is created; but we don't want the panel to continue to be referenced once it has shutdown, preventing it
// being garbage collected. Hence, when we shutdown, we set the 'Panel' property to 'null', removing the reference to this panel, allowing
// it to be garbage collected.
private class PanelWrapper(ReservationManagementPanel panel)
{
public ReservationManagementPanel? Panel { get; set; } = panel;
}
private PanelWrapper PanelLink;
public void Shutdown(CancelEventArgs? cancel)
{
PanelLink.Panel = null;
}
#region TreatmentPO
private void TreatmentPO_Click(PanelAction action)
{
var jris = JobRequiItems.SelectedRows.ToObjects().ToDictionary(x => x.ID);
if(jris.Count == 0)
{
MessageWindow.ShowMessage("Please select at least one job requisition item.", "No items selected");
return;
}
if(jris.Values.Any(x => x.TreatmentRequired <= 0))
{
MessageWindow.ShowMessage("Please select only items requiring treatment.", "Already treated");
return;
}
if(jris.Values.Any(x => x.Style.ID == Guid.Empty))
{
MessageWindow.ShowMessage("Please select only items with a style.", "No style");
return;
}
Client.EnsureColumns(jris.Values, Columns.None()
.Add(x => x.Product.Code)
.Add(x => x.Product.Name)
.Add(x => x.Requisition.Number)
.Add(x => x.Job.JobNumber)
.Add(x => x.Dimensions.Value));
// Here, we grab every stock movement for the selected JRIs, and group them per JRI. For each JRI, any stock movements that are in the wrong style
// are grouped according to their holding key. Note that this mimics precisely the TreatmentRequired aggregate on JRI. Hence, these holdings represent
// the TreatmentRequired amounts; the 'Units' field is equivalent to TreatmentRequired.
var holdings = Client.Query(
new Filter(x => x.JobRequisitionItem.ID).InList(jris.Keys.ToArray()),
Columns.None()
.Add(x => x.JobRequisitionItem.ID)
.Add(x => x.Job.ID)
.Add(x => x.Job.JobNumber)
.Add(x => x.Job.Name)
.Add(x => x.Product.ID)
.Add(x => x.Product.Code)
.Add(x => x.Product.Name)
.Add(x => x.Location.ID)
.Add(x => x.Location.Code)
.Add(x => x.Location.Description)
.Add(x => x.Style.ID)
.Add(x => x.Style.Code)
.Add(x => x.Style.Description)
.AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
.Add(x => x.Units))
.ToObjects()
.GroupBy(x => x.JobRequisitionItem.ID)
.ToDictionary(x => x.Key, x =>
{
var jri = jris[x.Key];
return x.Where(x => x.Style.ID != jri.Style.ID).GroupBy(x => new
{
Job = x.Job.ID,
Product = x.Product.ID,
Location = x.Location.ID,
Style = x.Style.ID,
x.Dimensions
}).ToDictionary(
x => x.Key,
x =>
{
var items = x.ToArray();
return new
{
Job = items[0].Job,
Product = items[0].Product,
Location = items[0].Location,
Style = items[0].Style,
Dimensions = items[0].Dimensions,
Units = items.Sum(x => x.Units)
};
});
});
var styles = Client.Query(
new Filter(x => x.ID).InList(jris.Values.Select(x => x.Style.ID).ToArray()),
Columns.None()
.Add(x => x.ID)
.Add(x => x.Code)
.Add(x => x.StockTreatmentProduct.ID))
.ToObjects().ToDictionary(x => x.ID);
// We need to load the treatment product for the styles that we need.
var treatmentProducts = Client.Query(
new Filter(x => x.ID).InList(styles.Select(x => x.Value.StockTreatmentProduct.ID).ToArray()),
Columns.None()
.Add(x => x.ID)
.Add(x => x.Code)
.Add(x => x.Name)
.Add(x => x.Supplier.ID)
.Add(x => x.TreatmentType.ID)
.Add(x => x.TreatmentType.Description)
.Add(x => x.TreatmentType.Calculation)
.Add(x => x.TaxCode.ID))
.ToObjects().ToDictionary(x => x.ID);
// Also, the ProductTreatment contains the parameter we need.
var jriProductsParameters = Client.Query(
new Filter(x => x.Product.ID).InList(jris.Values.Select(x => x.Product.ID).ToArray()),
Columns.None()
.Add(x => x.Product.ID)
.Add(x => x.TreatmentType.ID)
.Add(x => x.Parameter))
.ToObjects().ToDictionary(x => (x.Product.ID, x.TreatmentType.ID));
var items = new List();
foreach(var (id, jri) in jris)
{
if (!styles.TryGetValue(jri.Style.ID, out var style))
{
continue;
}
if(!treatmentProducts.TryGetValue(style.StockTreatmentProduct.ID, out var treatmentProduct))
{
MessageWindow.ShowMessage($"No stock treatment product found for style {style.Code}", "Treatment product not found");
return;
}
else if(treatmentProduct.TreatmentType.ID == Guid.Empty)
{
MessageWindow.ShowMessage($"Treatment product {treatmentProduct.Code} does not have a treatment type", "No treatment type");
return;
}
if(!jriProductsParameters.TryGetValue((jri.Product.ID, treatmentProduct.TreatmentType.ID), out var treatment))
{
MessageWindow.ShowMessage(
$"No treatment parameter found for product {jri.Product.Code} for treatment type {treatmentProduct.TreatmentType.Description}",
"Treatment product not found");
return;
}
var jriHoldings = holdings.GetValueOrDefault(id);
// We know here that the TreatmentRequired > 0, because of the check at the top of the function. Hence, there definitely should be holdings.
// This therefore shouldn't ever happen, but if it does, we've made a logic mistake, and this error will tell us that.
if(jriHoldings is null || jriHoldings.Count == 0)
{
MessageWindow.ShowError($"Internal error for requisition {jri.Requisition.Number} for job {jri.Job.JobNumber}", $"No holdings even though TreatmentRequired is greater than 0.");
continue;
}
double multiplier;
if (treatmentProduct.TreatmentType.Calculation.IsNullOrWhiteSpace())
{
// This is the default calculation.
multiplier = treatment.Parameter * jri.Dimensions.Value;
}
else
{
var model = new TreatmentTypeCalculationModel();
model.Dimensions.CopyFrom(jri.Dimensions);
model.Parameter = treatment.Parameter;
var expression = new CoreExpression(treatmentProduct.TreatmentType.Calculation);
if(!expression.Evaluate(model).Get(out multiplier, out var e))
{
MessageWindow.ShowError("Error calculating expression multiplier; using Parameter * Dimensions.Value instead.", e);
multiplier = treatment.Parameter * jri.Dimensions.Value;
}
}
foreach(var (key, holding) in jriHoldings)
{
if (!holding.Units.IsEffectivelyGreaterThan(0)) continue;
var item = new ReservationManagementTreatmentPOItem();
item.Product.CopyFrom(holding.Product);
item.Style.CopyFrom(holding.Style);
item.Job.CopyFrom(holding.Job);
item.Location.CopyFrom(holding.Location);
item.Dimensions.CopyFrom(holding.Dimensions);
item.TreatmentProduct.CopyFrom(treatmentProduct);
item.Finish.CopyFrom(style);
item.JRI.CopyFrom(jri);
item.Multiplier = multiplier;
// holding.Units should be TreatmentRequired
item.RequiredQuantity = holding.Units;
items.Add(item);
}
}
var window = new ReservationManagementTreatmentOrderScreen(items);
if(window.ShowDialog() == true)
{
var results = window.Results.GroupBy(x => x.Supplier.ID).ToDictionary(x => x.Key, x => x.ToArray());
var suppliers = Client.Query(
new Filter(x => x.ID).InList(results.Keys.ToArray()),
Columns.None().Add(x => x.ID).Add(x => x.DefaultLocation.ID))
.ToObjects()
.ToDictionary(x => x.ID);
var doIssue = false;
if(Security.IsAllowed())
{
if (_globalSettings.AutoIssueTreatmentPOs)
{
doIssue = true;
}
else if(MessageWindow.ShowYesNo($"Do you wish to mark the purchase order as issued?", "Mark as issued?"))
{
doIssue = true;
}
}
var grid = new SupplierPurchaseOrders();
var windows = new List();
grid.OnAfterSave += (editor, items) =>
{
var order = items.FirstOrDefault();
var window = windows.FirstOrDefault(x => x.Order == order);
window?.AfterSave();
};
foreach(var (supplierID, perSupplier) in results)
{
var order = perSupplier.First().PurchaseOrder;
if(order is null)
{
order = new PurchaseOrder();
order.RaisedBy.ID = App.EmployeeID;
order.DueDate = DateTime.Today.AddDays(7);
order.Notes = [$"Treatment purchase order raised by {App.EmployeeName} from Reservation Management screen"];
LookupFactory.DoLookup(order, x => x.SupplierLink, supplierID);
}
else
{
Client.EnsureColumns(order, DynamicGridUtils.LoadEditorColumns());
}
if (doIssue)
{
order.IssuedBy.ID = App.EmployeeID;
order.IssuedDate = DateTime.Now;
}
var orderItems = new List<(PurchaseOrderItem, JobRequisitionItemLink, StockForecastTreatmentOrderingResult)>();
foreach(var item in perSupplier)
{
var orderItem = new PurchaseOrderItem();
orderItem.Product.ID = item.Item.TreatmentProduct.ID;
orderItem.TaxCode.ID = item.SupplierProduct.TaxCode.ID != Guid.Empty
? item.SupplierProduct.TaxCode.ID
: item.Item.TreatmentProduct.TaxCode.ID;
orderItems.Add((orderItem, item.Item.JRI, item));
}
var productIDs = orderItems.ToArray(x => new Tuple(x.Item1, x.Item1.Product.ID));
var taxCodeIDs = orderItems.WithIndex()
.Select(x => new Tuple(x.Value.Item1, perSupplier[x.Key].SupplierProduct.TaxCode.ID)).ToArray();
var jobIDs = orderItems.ToArray(x => new Tuple(x.Item1, x.Item2.Job.ID));
LookupFactory.DoLookups(productIDs, x => x.Product);
LookupFactory.DoLookups(taxCodeIDs, x => x.TaxCode);
LookupFactory.DoLookups(jobIDs, x => x.Job);
foreach (var (i, item) in perSupplier.WithIndex())
{
var orderItem = orderItems[i].Item1;
orderItem.Qty = item.Quantity;
orderItem.Cost = item.SupplierProduct.CostPrice * item.Item.Multiplier;
orderItem.Description = $"Treatment for {item.Item.JRI.Product.Name} ({item.Item.Product.Code}/{item.Item.Product.Name})";
}
var editorWindow = new ReservationManagementPanelTreatmentPOWindow(grid, new(order, orderItems), perSupplier, PanelLink);
editorWindow.Form.Show();
ISubPanelHost.Global.AddSubPanel(editorWindow.Form);
windows.Add(editorWindow);
}
}
}
private class ReservationManagementPanelTreatmentPOWindow
{
public PurchaseOrder Order { get; private set; }
StockForecastTreatmentOrderingResult[] ResultItems;
PanelWrapper Panel;
public DynamicEditorForm Form { get; private set; }
public ReservationManagementPanelTreatmentPOWindow(
SupplierPurchaseOrders grid,
Tuple> order,
StockForecastTreatmentOrderingResult[] resultItems,
PanelWrapper panel
)
{
Form = new DynamicEditorForm
{
Title = "Edit Treatment PO"
};
Order = order.Item1;
ResultItems = resultItems;
Panel = panel;
Form.Form.DoChanged();
Form.SetLayoutType();
grid.InitialiseEditorForm(Form, [order.Item1], null, true);
var oneToManyPage = Form.Pages?.OfType().FirstOrDefault();
if(oneToManyPage is null)
{
Logger.Send(LogType.Error, "", "Could not find POI page when saving treatment PO");
}
else
{
oneToManyPage.Items.AddRange(order.Item2.Select(x => x.Item1));
oneToManyPage.Refresh(false, true);
oneToManyPage.OnCustomiseEditor += (form, items, column, editor) =>
{
if(column.ColumnName == $"{nameof(PurchaseOrderItem.Product)}.{nameof(PurchaseOrderItem.Product.ID)}")
{
editor.Editable = editor.Editable.Combine(Editable.Disabled);
}
};
oneToManyPage.ColumnsLoaded += (o, args) =>
{
var column = args.DataColumns.FirstOrDefault(x => x.ColumnName == $"{nameof(PurchaseOrderItem.Product)}.{nameof(PurchaseOrderItem.Product.ID)}");
if(column is not null)
{
column.Editor.Editable = column.Editor.Editable.Combine(Editable.Disabled);
}
};
oneToManyPage.Simplified = true;
oneToManyPage.Reconfigure(options =>
{
options.AddRows = false;
options.DeleteRows = false;
});
foreach(var (i, item) in order.Item2.WithIndex())
{
var jriPOI = new PurchaseOrderItemAllocation();
jriPOI.Job.ID = item.Item2.Job.ID;
jriPOI.JobRequisitionItem.ID = item.Item2.ID;
jriPOI.Item.ID = item.Item1.ID;
jriPOI.Quantity = item.Item1.Qty;
oneToManyPage.Allocations.Add(new(oneToManyPage.Items[i], jriPOI));
}
}
}
public void AfterSave()
{
var locationID = Client.Query(
new Filter(x => x.ID).IsEqualTo(Order.SupplierLink.ID),
Columns.None().Add(x => x.DefaultLocation.ID))
.Rows.FirstOrDefault()?.Get(x => x.DefaultLocation.ID) ?? Guid.Empty;
if (locationID != Guid.Empty)
{
var holdings = StockHoldingExtensions.LoadStockHoldings(ResultItems.Select(x => x.Item),
Columns.None().Add(x => x.AverageValue));
var movements = new List();
foreach(var item in ResultItems)
{
var tOut = new StockMovement();
tOut.Job.CopyFrom(item.Item.Job);
tOut.Style.CopyFrom(item.Item.Style);
tOut.Location.CopyFrom(item.Item.Location);
tOut.Product.CopyFrom(item.Item.Product);
tOut.Dimensions.CopyFrom(item.Item.Dimensions);
tOut.Employee.ID = App.EmployeeID;
tOut.Date = DateTime.Now;
tOut.Issued = item.Quantity;
tOut.Type = StockMovementType.TransferOut;
tOut.JobRequisitionItem.CopyFrom(item.Item.JRI);
tOut.Notes = "Stock movement for treatment purchase order created from Reservation Management";
var tIn = tOut.CreateMovement();
tIn.Transaction = tOut.Transaction;
tIn.Style.CopyFrom(item.Item.JRI.Style);
tIn.Location.ID = locationID;
tIn.Employee.ID = App.EmployeeID;
tIn.Date = tOut.Date;
tIn.Received = item.Quantity;
tIn.Type = StockMovementType.TransferIn;
tIn.JobRequisitionItem.CopyFrom(item.Item.JRI);
tIn.Notes = "Stock movement for treatment purchase order created from Reservation Management";
if(holdings.TryGetValue((tOut.Product.ID, tOut.Style.ID, tOut.Location.ID, tOut.Job.ID, tOut.Dimensions), out var holding))
{
tOut.Cost = holding.AverageValue;
tIn.Cost = holding.AverageValue;
}
movements.Add(tOut);
movements.Add(tIn);
}
Client.Save(movements, "Treatment PO created from Reservation Management screen");
}
var panel = Panel.Panel;
panel?.JobRequiItems.Refresh(false, true);
}
}
#endregion
}