Browse Source

Fixes to Treatment PO to include stock movements

Kenric Nugteren 11 tháng trước cách đây
mục cha
commit
f94e0e1956

+ 2 - 3
prs.classes/Entities/Product/Instance/ProductInstance.cs

@@ -66,9 +66,8 @@ namespace Comal.Classes
         [LoggableProperty]
         public double LastCost { get; set; }
 
-        [EditorSequence(8)]
-        [LoggableProperty]
-        [DoubleEditor]
+        [NullEditor]
+        [Obsolete("", true)]
         public double Parameter { get; set; }
 
     }

+ 149 - 33
prs.desktop/Panels/Reservation Management/ReservationManagementPanel.xaml.cs

@@ -340,28 +340,75 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
 
     private void TreatmentPO_Click(PanelAction action)
     {
-        var jris = JobRequiItems.SelectedRows.ToArray<JobRequisitionItem>();
-        if(jris.Length == 0)
+        var jris = JobRequiItems.SelectedRows.ToObjects<JobRequisitionItem>().ToDictionary(x => x.ID);
+        if(jris.Count == 0)
         {
             MessageWindow.ShowMessage("Please select at least one job requisition item.", "No items selected");
             return;
         }
 
-        if(jris.Any(x => x.TreatmentRequired - x.TreatmentOnOrder <= 0))
+        if(jris.Values.Any(x => x.TreatmentRequired - x.TreatmentOnOrder <= 0))
         {
             MessageWindow.ShowMessage("Please select only items requiring treatment.", "Already treated");
             return;
         }
-        if(jris.Any(x => x.Style.ID == Guid.Empty))
+        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, Columns.None<JobRequisitionItem>().Add(x => x.Product.Name));
+        Client.EnsureColumns(jris.Values, Columns.None<JobRequisitionItem>().Add(x => x.Product.Name));
+
+        var holdings = Client.Query(
+            new Filter<StockMovement>(x => x.JobRequisitionItem.ID).InList(jris.Keys.ToArray()),
+            Columns.None<StockMovement>()
+                .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<StockMovement>()
+            .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<ProductStyle>(x => x.ID).InList(jris.ToArray(x => x.Style.ID)),
+            new Filter<ProductStyle>(x => x.ID).InList(jris.Values.Select(x => x.Style.ID).ToArray()),
             Columns.None<ProductStyle>()
                 .Add(x => x.ID)
                 .Add(x => x.Code)
@@ -379,7 +426,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 .Add(x => x.TreatmentType.Calculation))
             .ToObjects<Product>().ToDictionary(x => x.ID);
         var jriProductsParameters = Client.Query(
-            new Filter<ProductTreatment>(x => x.Product.ID).InList(jris.ToArray(x => x.Product.ID)),
+            new Filter<ProductTreatment>(x => x.Product.ID).InList(jris.Values.Select(x => x.Product.ID).ToArray()),
             Columns.None<ProductTreatment>()
                 .Add(x => x.Product.ID)
                 .Add(x => x.TreatmentType.ID)
@@ -388,7 +435,7 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
 
         var items = new List<ReservationManagementTreatmentPOItem>();
 
-        foreach(var jri in jris)
+        foreach(var (id, jri) in jris)
         {
             if (!styles.TryGetValue(jri.Style.ID, out var style))
             {
@@ -412,14 +459,17 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 return;
             }
 
-            var item = new ReservationManagementTreatmentPOItem();
-            item.Product.CopyFrom(treatmentProduct);
-            item.Style.CopyFrom(style);
-            item.JRI.CopyFrom(jri);
+            var jriHoldings = holdings.GetValueOrDefault(id);
+            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())
             {
-                item.Multiplier = treatment.Parameter * jri.Dimensions.Value;
+                multiplier = treatment.Parameter * jri.Dimensions.Value;
             }
             else
             {
@@ -428,27 +478,50 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 model.Parameter = treatment.Parameter;
 
                 var expression = new CoreExpression<TreatmentTypeCalculationModel, double>(treatmentProduct.TreatmentType.Calculation);
-                if(expression.Evaluate(model).Get(out var multiplier, out var e))
-                {
-                    item.Multiplier = multiplier;
-                }
-                else
+                if(!expression.Evaluate(model).Get(out multiplier, out var e))
                 {
                     MessageWindow.ShowError("Error calculating expression multiplier; using Parameter * Dimensions.Value instead.", e);
-                    item.Multiplier = treatment.Parameter * jri.Dimensions.Value;
+                    multiplier = treatment.Parameter * jri.Dimensions.Value;
                 }
             }
-            item.RequiredQuantity = jri.TreatmentRequired - jri.TreatmentOnOrder;
 
-            items.Add(item);
+            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;
+                item.RequiredQuantity = holding.Units;// jri.TreatmentRequired - jri.TreatmentOnOrder;
+
+                items.Add(item);
+            }
         }
 
         var window = new ReservationManagementTreatmentOrderScreen(items);
         if(window.ShowDialog() == true)
         {
             var orders = new List<Tuple<PurchaseOrder, List<(PurchaseOrderItem, JobRequisitionItemLink)>>>();
+            var movements = new List<StockMovement>();
 
-            foreach(var perSupplier in window.Results.GroupBy(x => x.Supplier.ID))
+            var results = window.Results.GroupBy(x => x.Supplier.ID).Select(x => (x.Key, x.ToArray())).ToArray();
+            var suppliers = Client.Query(
+                new Filter<Supplier>(x => x.ID).InList(results.ToArray(x => x.Key)),
+                Columns.None<Supplier>().Add(x => x.ID).Add(x => x.DefaultLocation.ID))
+                .ToObjects<Supplier>()
+                .ToDictionary(x => x.ID);
+
+            foreach(var (supplierID, perSupplier) in results)
             {
                 var order = new PurchaseOrder();
                 order.RaisedBy.ID = App.EmployeeID;
@@ -456,14 +529,13 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 order.DueDate = DateTime.Today.AddDays(7);
                 order.Notes = [$"Treatment purchase order raised by {App.EmployeeName} from Reservation Management screen"];
 
-                LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, perSupplier.Key);
+                LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, supplierID);
 
                 var orderItems = new List<(PurchaseOrderItem, JobRequisitionItemLink)>();
-                var results = perSupplier.ToArray();
-                foreach(var item in results)
+                foreach(var item in perSupplier)
                 {
                     var orderItem = new PurchaseOrderItem();
-                    orderItem.Product.ID = item.Item.Product.ID;
+                    orderItem.Product.ID = item.Item.TreatmentProduct.ID;
                     orderItem.Job.ID = item.Item.JRI.Job.ID;
                     
                     orderItems.Add((orderItem, item.Item.JRI));
@@ -476,10 +548,10 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                     orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x.Item1, x.Item1.Job.ID)),
                     x => x.Job);
                 LookupFactory.DoLookups<PurchaseOrderItem, TaxCode, TaxCodeLink>(
-                    orderItems.WithIndex().Select(x => new Tuple<PurchaseOrderItem, Guid>(x.Value.Item1, results[x.Key].SupplierProduct.TaxCode.ID)),
+                    orderItems.WithIndex().Select(x => new Tuple<PurchaseOrderItem, Guid>(x.Value.Item1, perSupplier[x.Key].SupplierProduct.TaxCode.ID)),
                     x => x.TaxCode);
 
-                foreach(var (i, item) in results.WithIndex())
+                foreach (var (i, item) in perSupplier.WithIndex())
                 {
                     var orderItem = orderItems[i].Item1;
                     orderItem.Qty = item.Quantity;
@@ -487,22 +559,64 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                     orderItem.Description = $"Treatment for {item.Item.JRI.Product.Name} ({item.Item.Product.Code}/{item.Item.Product.Name})";
                 }
 
+                if(suppliers.TryGetValue(supplierID, out var supplier))
+                {
+                    foreach(var item in perSupplier)
+                    {
+                        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.CopyFrom(supplier.DefaultLocation);
+
+                        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";
+
+                        movements.Add(tOut);
+                        movements.Add(tIn);
+                    }
+                }
+                else
+                {
+                    MessageWindow.ShowMessage(
+                        $"No default location set up for supplier '{perSupplier[0].Supplier.Code}'; skipping creating stock movements",
+                        "No default location");
+                }
+
                 orders.Add(new(order, orderItems));
             }
 
-            var issue = false;
+            var doIssue = false;
             if(Security.IsAllowed<CanIssueTreatmentPurchaseOrders>())
             {
                 if (_globalSettings.AutoIssueTreatmentPOs)
                 {
-                    issue = true;
+                    doIssue = true;
                 }
                 else if(MessageWindow.ShowYesNo($"Do you wish to mark the purchase order{(orders.Count != 1 ? "s" : "")} as issued?", "Mark as issued?"))
                 {
-                    issue = true;
+                    doIssue = true;
                 }
             }
-            if (issue)
+            if (doIssue)
             {
                 foreach(var (order, _) in orders)
                 {
@@ -528,8 +642,10 @@ public partial class ReservationManagementPanel : UserControl, IPanel<JobRequisi
                 return jriPOI;
             }), "Treatment PO created from Reservation Management screen");
 
+            Client.Save(movements, "Treatment PO created from Reservation Management screen");
+
             MessageWindow.ShowMessage(
-                $"{orders.Count} treatment purchase order{(orders.Count != 1 ? "s" : "")} {(issue ? "issued" : "raised")}:\n" +
+                $"{orders.Count} treatment purchase order{(orders.Count != 1 ? "s" : "")} {(doIssue ? "issued" : "raised")}:\n" +
                 $"- {string.Join(',', orders.Select(x => x.Item1.PONumber))}",
                 "Success");
 

+ 45 - 11
prs.desktop/Panels/Reservation Management/Treatment PO/ReservationManagementTreatmentOrderGrid.cs

@@ -31,13 +31,20 @@ public class ReservationManagementTreatmentPOItem : BaseObject
 
     public event ChangedHandler? Changed;
 
-    [EditorSequence(1)]
+    public ProductLink TreatmentProduct { get; set; }
+
+    public ProductStyleLink Finish { get; set; }
+
+    public JobLink Job { get; set; }
+
     public ProductLink Product { get; set; }
 
-    [EditorSequence(2)]
     public ProductStyleLink Style { get; set; }
 
-    [EditorSequence(3)]
+    public StockDimensions Dimensions { get; set; }
+
+    public StockLocationLink Location { get; set; }
+
     public JobRequisitionItemLink JRI { get; set; }
 
     public double Multiplier { get; set; }
@@ -124,9 +131,13 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         }
     }
 
+    private DynamicGridCustomColumnsComponent<ReservationManagementTreatmentPOItem> ColumnsComponent;
+
     public ReservationManagementTreatmentOrderGrid()
     {
         HiddenColumns.Add(x => x.Product.Image.ID);
+
+        ColumnsComponent = new(this, nameof(ReservationManagementTreatmentOrderGrid));
     }
 
     #region UI Component
@@ -278,6 +289,33 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         InvalidateGrid();
     }
 
+    protected override void SaveColumns(DynamicGridColumns columns)
+    {
+        ColumnsComponent.SaveColumns(columns);
+    }
+
+    protected override void LoadColumnsMenu(ContextMenu menu)
+    {
+        ColumnsComponent.LoadColumnsMenu(menu);
+    }
+
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var columns = new DynamicGridColumns();
+
+        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleLeft);
+        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Location.Code, 80, "Location", "", Alignment.MiddleLeft);
+        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleLeft);
+
+        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Finish.Code, 80, "Finish", "", Alignment.MiddleCenter);
+        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.TreatmentProduct.Code, 80, "Treatment Product", "", Alignment.MiddleCenter);
+
+        columns.Add<ReservationManagementTreatmentPOItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
+        columns.Add<ReservationManagementTreatmentPOItem, double>(x => x.Multiplier, 80, "Multiplier", "", Alignment.MiddleCenter);
+
+        return columns;
+    }
+
     protected override DynamicGridColumns LoadColumns()
     {
         if (!_loadedData)
@@ -289,12 +327,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
         ActionColumns.Add(new DynamicImagePreviewColumn<ReservationManagementTreatmentPOItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
 
-        var columns = new DynamicGridColumns();
-        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
-        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Product.Name, 200, "Product Name", "", Alignment.MiddleLeft);
-        columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter);
-        columns.Add<ReservationManagementTreatmentPOItem, double>(x => x.Multiplier, 80, "Multiplier", "", Alignment.MiddleCenter);
-        columns.Add<ReservationManagementTreatmentPOItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
+        var columns = ColumnsComponent.LoadColumns();
 
         QuantityColumns = new DynamicActionColumn[Suppliers.Length];
         CostColumns = new DynamicActionColumn[Suppliers.Length];
@@ -432,6 +465,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
             var editor = new DoubleTextBox
             {
                 VerticalAlignment = VerticalAlignment.Stretch,
+                HorizontalContentAlignment = HorizontalAlignment.Center,
                 HorizontalAlignment = HorizontalAlignment.Stretch,
                 Background = new SolidColorBrush(Colors.LightYellow),
                 BorderThickness = new Thickness(0.0),
@@ -489,7 +523,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         })
         {
             HeaderText = "Qty.",
-            Width = 80,
+            Width = 60,
             ContextMenu = contextMenuFunc
         };
         CostColumns[idx] = new DynamicTextColumn(row =>
@@ -513,7 +547,7 @@ public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<Reser
         })
         {
             HeaderText = "Cost",
-            Width = 80,
+            Width = 70,
             ContextMenu = contextMenuFunc,
             GetSummary = () =>
             {