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 }