소스 검색

Added issued enum for job requisition item and made cancelled functional. Improved functionality of purchase order item receiving.

Kenric Nugteren 1 년 전
부모
커밋
54f83a8030

+ 9 - 1
prs.classes/Entities/Job/Requisitions/JobRequisitionItem.cs

@@ -56,7 +56,12 @@ namespace Comal.Classes
         /// <summary>
         /// The <see cref="JobRequisitionItem/"> has been archived, meaning it has a non-empty <see cref="JobRequisitionItem.Archived"/>.
         /// </summary>
-        Archived
+        Archived,
+
+        /// <summary>
+        /// The <see cref="JobRequisitionItem/"> has been issued, meaning that it has been allocated, and there are stock movements of type <see cref="StockMovementType.Issue"/> adding up to the correct total.
+        /// </summary>
+        Issued
     }
     public class JobRequisitionItemTotalQtyFormula : IFormula<JobRequisitionItem, double>
     {
@@ -153,6 +158,9 @@ namespace Comal.Classes
         [RequiredColumn]
         public JobRequisitionItemStatus Status { get; set; } = JobRequisitionItemStatus.NotChecked;
 
+        /// <summary>
+        /// The amount of this requisition item that is currently in stock, which is an aggregate of the <see cref="StockMovement.Units"/> property.
+        /// </summary>
         [Aggregate(typeof(JobRequisitionItemInStockAggregate))]
         [DoubleEditor(Editable = Editable.Disabled)]
         [EditorSequence(10)]

+ 7 - 0
prs.classes/Entities/Stock/StockHolding.cs

@@ -306,6 +306,12 @@ namespace Comal.Classes
 
     public static class StockHoldingExtensions
     {
+        /// <summary>
+        /// Create a new stock movement from an <see cref="IStockHolding"/>, copying across the "key" properties;
+        /// that is, the job, product, style, location and dimensions.
+        /// </summary>
+        /// <param name="holding"></param>
+        /// <returns></returns>
         public static StockMovement CreateMovement(this IStockHolding holding)
         {
             var movement = new StockMovement();
@@ -371,6 +377,7 @@ namespace Comal.Classes
             holding.Units = units;
             holding.Available = available;
             holding.Qty = movements.Sum(x => x.Units * x.Dimensions.Value);
+            holding.Value = cost;
 
             holding.AverageValue = units.IsEffectivelyEqual(0.0F) ? 0.0d : cost / units;
             holding.Weight = holding.Qty * holding.Dimensions.Weight;

+ 1 - 1
prs.desktop/Panels/Jobs/Requisitions/JobRequisitionItemGrid.cs

@@ -13,7 +13,7 @@ using InABox.WPF;
 
 namespace PRSDesktop;
 
-internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMasterDetailControl<JobRequisition,JobRequisitionItem>
+internal class JobRequisitionItemGrid : DynamicDataGrid<JobRequisitionItem>, IMasterDetailControl<JobRequisition,JobRequisitionItem>, ISpecificGrid
 {
     public JobRequisition? Master { get; set; }
     

+ 3 - 1
prs.desktop/Panels/Products/Locations/StockHoldingGrid.cs

@@ -344,7 +344,9 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
                 return new Filter<JobRequisitionItem>(x => x.ID)
                     .InQuery(
                         StockHolding.GetFilter(holding),
-                        x => x.JobRequisitionItem.ID);
+                        x => x.JobRequisitionItem.ID)
+                    // We don't care about stuff which has nothing in stock, which means it's been either issued or cancelled.
+                    .And(x => x.InStock).IsNotEqualTo(FilterConstant.Zero);
             }
             else
             {

+ 123 - 9
prs.stores/JobRequisitionItemStore.cs

@@ -28,6 +28,109 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
         base.BeforeSave(item);
     }
 
+    protected override void AfterSave(JobRequisitionItem entity)
+    {
+        base.AfterSave(entity);
+
+        if(entity.HasOriginalValue(x => x.Cancelled))
+        {
+            CancelMovements(entity);
+        }
+    }
+
+    private static IEnumerable<StockMovement> GetMovements(IStore store, Guid jriID, Filter<StockMovement>? filter, Columns<StockMovement> columns)
+    {
+        return store.Provider
+            .Query(
+                Filter<StockMovement>.And(
+                    new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(jriID),
+                    filter),
+                columns)
+                .ToObjects<StockMovement>();
+    }
+    private static IEnumerable<StockMovement> GetNotIssued(IStore store, Guid jriID, Columns<StockMovement> columns)
+    {
+        return GetMovements(store, jriID, new Filter<StockMovement>(x => x.Type).IsNotEqualTo(StockMovementType.Issue), columns);
+    }
+    private static IEnumerable<StockMovement> GetIssued(IStore store, Guid jriID, Columns<StockMovement> columns)
+    {
+        return GetMovements(store, jriID, new Filter<StockMovement>(x => x.Type).IsEqualTo(StockMovementType.Issue), columns);
+    }
+
+    private void CancelMovements(JobRequisitionItem entity)
+    {
+        // Here, we care about *all* movements into or out of this requi. If stuff has been issued, it must be included,
+        // since we cannot return issued stock back to general stock for the job.
+        var movements = GetMovements(this, entity.ID, null,
+            new Columns<StockMovement>(
+                x => x.Product.ID,
+                x => x.Style.ID,
+                x => x.Job.ID,
+                x => x.Location.ID,
+                x => x.Units,
+                x => x.Cost,
+                x => x.OrderItem.ID)
+            .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local));
+
+        var newMovements = new List<StockMovement>();
+        foreach(var movement in movements)
+        {
+            var from = movement.CreateMovement();
+            from.Date = entity.Cancelled;
+            from.Cost = movement.Cost;
+            from.System = true;
+            from.JobRequisitionItem.ID = entity.ID;
+            from.OrderItem.ID = movement.OrderItem.ID;
+            from.Notes = "Requisition item cancelled";
+
+            var to = movement.CreateMovement();
+            to.Date = entity.Cancelled;
+            to.Cost = movement.Cost;
+            to.System = true;
+            to.Notes = "Requisition item cancelled";
+            to.OrderItem.ID = movement.OrderItem.ID;
+            to.Transaction = from.Transaction;
+
+            if(movement.Units > 0)
+            {
+                // If this movement was an increase to reservation allocation, we create a transfer out of the reservation.
+                from.Issued = movement.Units;
+                to.Received = movement.Units;
+
+                from.Type = StockMovementType.TransferOut;
+                to.Type = StockMovementType.TransferIn;
+            }
+            else if(movement.Units < 0)
+            {
+                // If this movement was a decrease to reservation allocation, we create a transfer into the reservation.
+                from.Received = -movement.Units;
+                to.Issued = -movement.Units;
+
+                from.Type = StockMovementType.TransferIn;
+                to.Type = StockMovementType.TransferOut;
+            }
+
+            newMovements.Add(from);
+            newMovements.Add(to);
+        }
+
+        if(newMovements.Count > 0)
+        {
+            var batch = new StockMovementBatch
+            {
+                Notes = "Requisition item cancelled."
+            };
+            FindSubStore<StockMovementBatch>().Save(batch, "");
+
+            foreach(var mvt in newMovements)
+            {
+                mvt.Batch.ID = batch.ID;
+            }
+
+            FindSubStore<StockMovement>().Save(newMovements, "Requisition item cancelled.");
+        }
+    }
+
     public static Columns<JobRequisitionItem> StatusRequiredColumns()
     {
         return new Columns<JobRequisitionItem>(
@@ -56,14 +159,12 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
             item.Status = JobRequisitionItemStatus.Cancelled;
         else
         {
-            var stockMovements = store.Provider
-                .Query(
-                    new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID).And(x=>x.Type).IsNotEqualTo(StockMovementType.Issue),
-                    new Columns<StockMovement>(x => x.Units)
-                        .Add(x => x.Style.ID)
-                        .Add(x=>x.Type)
-                    )
-                    .ToObjects<StockMovement>();
+            // We don't care about that which has been issued, because we're just looking at how much was allocated.
+            // If we cared about the issued movements as well, then after issuing a requi item, it would become unallocated.
+            // However, we do include transfers out of this requi, since then the stuff ain't actually been allocated.
+            var stockMovements = GetNotIssued(store, item.ID,
+                new Columns<StockMovement>(x => x.Units)
+                    .Add(x => x.Style.ID));
             var styleTotal = 0.0;
             var total = 0.0;
             foreach (var mvt in stockMovements)
@@ -79,7 +180,20 @@ public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
             var remTotal = item.Qty - total;
 
             if (remStyle <= 0)
-                item.Status = JobRequisitionItemStatus.Allocated;
+            {
+                // Now, we care about what's actually been issued.
+                var issued = GetIssued(store, item.ID, new Columns<StockMovement>(x => x.Units));
+
+                // If everything has been issued, the issued total will be a negative value to balance the Qty.
+                if(item.Qty + issued.Sum(x => x.Units) <= 0)
+                {
+                    item.Status = JobRequisitionItemStatus.Issued;
+                }
+                else
+                {
+                    item.Status = JobRequisitionItemStatus.Allocated;
+                }
+            }
             else if (remTotal <= 0)
             {
                 // Find all unreceived POItems for this guy that are treatments (i.e., wrong product ID).

+ 102 - 98
prs.stores/PurchaseOrderItemStore.cs

@@ -25,6 +25,19 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
     }
 
     private void UpdateStockMovements(PurchaseOrderItem entity)
+    {
+        var movements = Provider.Query<StockMovement>(
+            new Filter<StockMovement>(x => x.OrderItem.ID).IsEqualTo(entity.ID))
+            .ToArray<StockMovement>();
+        foreach(var mvt in movements)
+        {
+            mvt.Date = entity.ReceivedDate;
+            mvt.Cost = entity.Cost;
+        }
+        FindSubStore<StockMovement>().Save(movements, "Updated by purchase order modification");
+    }
+
+    private void CreateStockMovements(PurchaseOrderItem entity)
     {
         if (!entity.Product.IsValid())
         {
@@ -44,51 +57,13 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
         var jobRequisitionItemTask = Task<Guid>.Run(() =>
         {
             return Provider.Query(
-                new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.ID).IsEqualTo(entity.ID),
-                new Columns<JobRequisitionItemPurchaseOrderItem>(x => x.JobRequisitionItem.ID))
-                .ToObjects<JobRequisitionItemPurchaseOrderItem>().FirstOrDefault()?.JobRequisitionItem.ID ?? Guid.Empty;
-        });
-
-        var movementtask = new Task<List<StockMovement>>(() =>
-        {
-            var result = Provider.Query(
-                new Filter<StockMovement>(x => x.OrderItem.ID).IsEqualTo(entity.ID),
-                new Columns<StockMovement>(
-                    x => x.ID,
-                    x => x.Date,
-                    x => x.Product.ID,
-                    x => x.Received,
-                    x => x.Employee.ID,
-                    x => x.OrderItem.ID,
-                    x => x.Job.ID,
-                    x => x.Location.ID,
-                    x => x.Dimensions.Unit.ID,
-                    x => x.Dimensions.Unit.Formula,
-                    x => x.Dimensions.Unit.Format,
-                    x => x.Dimensions.Quantity,
-                    x => x.Dimensions.Length,
-                    x => x.Dimensions.Width,
-                    x => x.Dimensions.Height,
-                    x => x.Dimensions.Weight,
-                    x => x.Notes,
-                    x => x.Cost,
-                    x => x.Dimensions.Unit.HasHeight,
-                    x => x.Dimensions.Unit.HasLength,
-                    x => x.Dimensions.Unit.HasWidth,
-                    x => x.Dimensions.Unit.HasWeight,
-                    x => x.Dimensions.Unit.HasQuantity,
-                    x => x.Dimensions.Unit.Formula,
-                    x => x.Dimensions.Unit.Format,
-                    x => x.Dimensions.Unit.Code,
-                    x => x.Dimensions.Unit.Description,
-                    x => x.Batch.ID
-                )
-            ).Rows.Select(x => x.ToObject<StockMovement>()).ToList();
-            if (!result.Any())
-                result.Add(new StockMovement());
-            return result;
+                new Filter<JobRequisitionItem>(x => x.ID).InQuery(
+                    new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.ID).IsEqualTo(entity.ID),
+                    x => x.JobRequisitionItem.ID),
+                new Columns<JobRequisitionItem>(x => x.ID)
+                    .Add(x => x.Status))
+            .ToObjects<JobRequisitionItem>().FirstOrDefault();
         });
-        movementtask.Start();
 
         var instancetask = new Task<CoreRow?>(() =>
         {
@@ -167,13 +142,12 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
         });
         locationtask.Start();
 
-        Task.WaitAll(movementtask, producttask, locationtask, instancetask, jobRequisitionItemTask);
+        Task.WaitAll(producttask, locationtask, instancetask, jobRequisitionItemTask);
 
-        var movements = movementtask.Result;
         var instancerow = instancetask.Result;
         var productrow = producttask.Result;
         var defaultlocations = locationtask.Result;
-        var jobRequisitionItemID = jobRequisitionItemTask.Result;
+        var jri = jobRequisitionItemTask.Result;
         
         if (productrow is null)
         {
@@ -284,57 +258,69 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
             }
         }
 
-        StockMovementBatch? batch = null;
-        foreach (var movement in movements)
+        var batch = new StockMovementBatch
         {
-            if(movement.Batch.ID == Guid.Empty)
-            {
-                batch ??= new StockMovementBatch
-                {
-                    Type = StockMovementBatchType.Receipt,
-                    TimeStamp = DateTime.Now,
-                    Notes = $"Received on PO"
-                };
-            }
+            Type = StockMovementBatchType.Receipt,
+            TimeStamp = DateTime.Now,
+            Notes = $"Received on PO"
+        };
+        var movements = new List<StockMovement>();
 
-            movement.Date = entity.ReceivedDate;
-            movement.Product.ID = entity.Product.ID;
-            movement.Received = entity.Qty;
-            movement.Employee.ID = Guid.Empty;
-            movement.OrderItem.ID = entity.ID;
-            movement.Job.ID = entity.Job.ID;
-            movement.Location.ID = locationid;
-            movement.Style.ID = entity.Style.ID;
-            movement.Notes = string.Format("Received on PO {0}", entity.PurchaseOrderLink.PONumber);
-            movement.Cost = entity.Cost;
-            movement.Type = StockMovementType.Receive;
-            movement.Dimensions.Unit.ID = entity.Dimensions.Unit.ID;
-            movement.Dimensions.Height = entity.Dimensions.Height;
-            movement.Dimensions.Length = entity.Dimensions.Length;
-            movement.Dimensions.Width = entity.Dimensions.Width;
-            movement.Dimensions.Weight = entity.Dimensions.Weight;
-            movement.Dimensions.Quantity = entity.Dimensions.Quantity;
-            movement.Dimensions.UnitSize = entity.Dimensions.UnitSize;
-            movement.Dimensions.Value = entity.Dimensions.Value;
-            movement.Dimensions.UnitSize = entity.Dimensions.UnitSize;
-            movement.JobRequisitionItem.ID = jobRequisitionItemID;
-        }
+        var movement = new StockMovement();
+        movement.Product.ID = entity.Product.ID;
+        movement.Job.ID = entity.Job.ID;
+        movement.Location.ID = locationid;
+        movement.Style.ID = entity.Style.ID;
+        movement.Dimensions.CopyFrom(entity.Dimensions);
 
-        if(batch is not null)
+        movement.Date = entity.ReceivedDate;
+        movement.Received = entity.Qty;
+        movement.Employee.ID = Guid.Empty;
+        movement.OrderItem.ID = entity.ID;
+        movement.Notes = string.Format("Received on PO {0}", entity.PurchaseOrderLink.PONumber);
+        movement.Cost = entity.Cost;
+        movement.Type = StockMovementType.Receive;
+        movements.Add(movement);
+
+        if(jri is not null)
         {
-            FindSubStore<StockMovementBatch>().Save(batch, "Received on PO");
-            foreach(var movement in movements)
+            movement.JobRequisitionItem.ID = jri.ID;
+            if (!jri.Cancelled.IsEmpty())
             {
-                if(movement.Batch.ID == Guid.Empty)
-                {
-                    movement.Batch.ID = batch.ID;
-                }
+                // We need to create an immediate transfer in and out of the job requisition item.
+
+                var tOut = movement.CreateMovement();
+                tOut.JobRequisitionItem.ID = jri.ID;
+                tOut.Date = entity.ReceivedDate;
+                tOut.Issued = entity.Qty;
+                tOut.OrderItem.ID = entity.ID;
+                tOut.Notes = "Internal transfer from cancelled requisition";
+                tOut.System = true;
+                tOut.Cost = entity.Cost;
+                tOut.Type = StockMovementType.TransferOut;
+
+                var tIn = movement.CreateMovement();
+                tIn.Transaction = tOut.Transaction;
+                tIn.Date = entity.ReceivedDate;
+                tIn.Received = entity.Qty;
+                tIn.OrderItem.ID = entity.ID;
+                tOut.Notes = "Internal transfer from cancelled requisition";
+                tOut.System = true;
+                tIn.Cost = entity.Cost;
+                tIn.Type = StockMovementType.TransferIn;
+
+                movements.Add(tOut);
+                movements.Add(tIn);
             }
         }
 
-        var updates = movements.Where(x => x.IsChanged()).ToList();
-        if (updates.Any())
-            FindSubStore<StockMovement>().Save(updates, "Updated by Purchase Order Modification");
+        FindSubStore<StockMovementBatch>().Save(batch, "Received on PO");
+        foreach(var mvt in movements)
+        {
+            mvt.Batch.ID = batch.ID;
+        }
+        
+        FindSubStore<StockMovement>().Save(movements, "Updated by Purchase Order Modification");
     }
 
 
@@ -354,7 +340,7 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
     {
         base.AfterSave(entity);
         
-        if (entity.HasOriginalValue<PurchaseOrderItem,DateTime>(x=>x.ReceivedDate))
+        if (entity.HasOriginalValue(x=>x.ReceivedDate))
         {
             if (entity.ReceivedDate.IsEmpty())
             {
@@ -363,14 +349,32 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
             }
             else
             {
-                var item = Provider.Query(
-                        new Filter<PurchaseOrderItem>(x => x.ID).IsEqualTo(entity.ID),
-                        RequiredColumns())
-                    .ToObjects<PurchaseOrderItem>().FirstOrDefault();
-                if (item != null)
+                var original = entity.GetOriginalValue(x => x.ReceivedDate);
+                if(original == DateTime.MinValue)
+                {
+                    var item = Provider.Query(
+                            new Filter<PurchaseOrderItem>(x => x.ID).IsEqualTo(entity.ID),
+                            RequiredColumns())
+                        .ToObjects<PurchaseOrderItem>().FirstOrDefault();
+                    if(item is not null)
+                    {
+                        CreateStockMovements(item);
+                        UpdateJobRequiItems(item, JobRequisitionItemAction.Updated);
+                    }
+                }
+                else
                 {
-                    UpdateStockMovements(item);
-                    UpdateJobRequiItems(item, JobRequisitionItemAction.Updated);
+                    var item = Provider.Query(
+                            new Filter<PurchaseOrderItem>(x => x.ID).IsEqualTo(entity.ID),
+                            new Columns<PurchaseOrderItem>(x => x.ID)
+                                .Add(x => x.ReceivedDate)
+                                .Add(x => x.Cost))
+                        .ToObjects<PurchaseOrderItem>().FirstOrDefault();
+                    if(item is not null)
+                    {
+                        UpdateStockMovements(item);
+                        UpdateJobRequiItems(item, JobRequisitionItemAction.Updated);
+                    }
                 }
             }
         }