using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Comal.Classes; using InABox.Core; using System; using NPOI.Util; namespace Comal.Stores { internal class RequisitionStore : BaseStore { private readonly bool _debug = true; private void Log(string format, params object[] values) { if (_debug) Logger.Send(LogType.Information, UserID, string.Format("- RequisitionStore:" + format, values)); } private bool NeedsUpdating(Requisition entity, TObject obj, Expression> property) where TObject : BaseObject { // If this is a new Requisition, we don't need to do anything if (entity.HasOriginalValue(x => x.ID)) { var originalid = entity.GetOriginalValue(x => x.ID); if (originalid == Guid.Empty) { Log("NeedsUpdating() return false - original id is empty"); return false; } } // if the Property has not changed, we don't need to do anything if (!obj.HasOriginalValue(property)) { Log("NeedsUpdating() return false - {0} has not changed", property.ToString()); return false; } return true; } private bool LoadRequisitionItems(Requisition entity, ref IList requisitionitems) { requisitionitems ??= Provider.Query( new Filter(x => x.RequisitionLink.ID).IsEqualTo(entity.ID).And(x=>x.ActualQuantity).IsNotEqualTo(0.0), Columns.None().Add(x => x.ID) .Add(x => x.Description) .Add(x => x.Quantity) .Add(x => x.Code) .Add(x => x.Location.ID) .Add(x => x.Style.ID) .Add(x => x.Product.ID) .Add(x => x.Product.NonStock) .Add(x => x.SourceJRI.ID) .Add(x => x.JobRequisitionItem.ID) .Add(x =>x.JobLink.ID) .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.All) .Add(x => x.ActualQuantity) .AddSubColumns(x => x.Charge, Columns.All()) .Add(x => x.JobScope.ID) .AddDimensionsColumns(x => x.Product.DefaultInstance.Dimensions, Dimensions.ColumnsType.All) .Add(x => x.Cost) ).ToList(); return requisitionitems.Any(); } private bool LoadDeliveryItems(Requisition entity, ref IList deliveryitems) { deliveryitems ??= Provider.Query( new Filter(x => x.RequisitionLink.ID).IsEqualTo(entity.ID), Columns.None().Add(x => x.ID, x => x.DeliveredDate) ).ToList(); return deliveryitems.Any(); } #region TakenBy private void UpdateTakenBy(Requisition entity, ref IList items, ref IList deliveryitems) { Log("UpdateTakenBy() - starting"); if (!NeedsUpdating(entity, entity.TakenBy, x => x.ID)) return; if (!LoadDeliveryItems(entity, ref deliveryitems)) { Log("UpdateTakenBy() - no delivery items to update"); return; } foreach (var deliveryitem in deliveryitems) if (entity.TakenBy.IsValid() && deliveryitem.DeliveredDate.IsEmpty()) { Log("UpdateTakenBy() - Setting DeliveryDate"); deliveryitem.DeliveredDate = DateTime.Now; } else if (!entity.TakenBy.IsValid() && !deliveryitem.DeliveredDate.IsEmpty()) { Log("UpdateTakenBy() - Clearing DeliveryDate"); deliveryitem.DeliveredDate = DateTime.MinValue; } var updates = deliveryitems.Where(x => x.IsChanged()); if (updates.Any()) FindSubStore().Save(updates, entity.TakenBy.IsValid() ? "Requisition taken by " + entity.TakenBy.Code : "Requisition [TakenBy] has been cleared"); Log("UpdateTakenBy() - done"); } #endregion protected override void BeforeSave(Requisition entity) { base.BeforeSave(entity); if (entity.TakenBy.IsValid() || entity.Delivery.Completed != DateTime.MinValue || entity.Delivery.Delivered != DateTime.MinValue) { if (entity.Archived.IsEmpty()) entity.Archived = DateTime.Now; } else { if (!entity.Archived.IsEmpty()) entity.Archived = DateTime.MinValue; } } protected override void AfterSave(Requisition entity) { base.AfterSave(entity); IList requisitionitems = null; IList deliveryitems = null; UpdateDeliveryItems(entity, ref requisitionitems, ref deliveryitems); UpdateTakenBy(entity, ref requisitionitems, ref deliveryitems); UpdateStockBatches(entity, ref requisitionitems); UpdateTrackingKanban(entity, e => { if (!entity.Archived.Equals(DateTime.MinValue) || entity.TakenBy.IsValid()) return KanbanStatus.Complete; if (entity.Delivery.IsValid()) { if (entity.Delivery.Completed != DateTime.MinValue) { return KanbanStatus.Complete; } } if (!entity.Filled.Equals(DateTime.MinValue)) return KanbanStatus.Waiting; if (Provider.Query( new Filter(x => x.RequisitionLink.ID).IsEqualTo(entity.ID), Columns.None().Add(x => x.ID) ).Rows.Any() ) return KanbanStatus.InProgress; return KanbanStatus.Open; }); } protected override void BeforeDelete(Requisition entity) { UnlinkTrackingKanban(entity); } #region Delivery Items private void CreateDeliveryItems(Requisition entity, ref IList requisitionitems, ref IList deliveryitems) { if (!LoadRequisitionItems(entity, ref requisitionitems)) { Log("CreateDeliveryItems() - no requisition items to update"); return; } var updates = new List(); foreach (var item in requisitionitems) updates.Add(item.CreateDeliveryItem(entity)); if (updates.Any()) FindSubStore().Save(updates, "Requisition [Filled] flag has been set"); deliveryitems = updates; } private void ClearDeliveryItems(Requisition entity, ref IList deliveryitems) { if (!LoadDeliveryItems(entity, ref deliveryitems)) { Log("ClearDeliveryItems() - no delivery items to update"); return; } if (deliveryitems.Any()) FindSubStore().Delete(deliveryitems, "Requisition [Filled] flag has been cleared"); deliveryitems = new List(); } private void UpdateDeliveryItems(Requisition entity, ref IList requisitionitems, ref IList deliveryitems) { Log("UpdateDeliveryItems() - starting"); if (!NeedsUpdating(entity, entity, x => x.Filled)) { Log("UpdateDeliveryItems() - NeedsUpdate() return false"); return; } var oldfilled = entity.GetOriginalValue(x => x.Filled); var newfilled = entity.Filled; // Gone from Blank to Filled -> Create a Batch if (oldfilled.IsEmpty() && !newfilled.IsEmpty()) { Log("UpdateDeliveryItems() - Filled has been set"); ClearDeliveryItems(entity, ref deliveryitems); CreateDeliveryItems(entity, ref requisitionitems, ref deliveryitems); } // Gone from Filled to Blank -> Clear Out the Batch else if (newfilled.IsEmpty() && !oldfilled.IsEmpty()) { Log("UpdateDeliveryItems() - Filled has been cleared"); ClearDeliveryItems(entity, ref deliveryitems); } // Do nothing - filled flag has been updated, not set or cleared Log("UpdateDeliveryItems() - done"); } #endregion #region StockMovements private StockMovement CreateStockMovement(IEmployee employee, DateTime date, IStockMovementBatch batch, IProduct product, IStockLocation location, IProductStyle style, IJob? job, IJobRequisitionItem? jri, IDimensions dimensions, Guid txnid, ActualCharge charge, JobScopeLink scope, bool system, string note) { var movement = new StockMovement(); movement.Batch.ID = batch.ID; movement.Product.ID = product.ID; movement.Location.ID = location.ID; movement.Style.ID = style.ID; movement.Job.ID = job?.ID ?? Guid.Empty; movement.JobRequisitionItem.ID = jri?.ID ?? Guid.Empty; movement.Dimensions.CopyFrom(dimensions); movement.Charge.CopyFrom(charge); movement.JobScope.CopyFrom(scope); movement.System = system; movement.Transaction = txnid; movement.Notes = note; movement.Date = date; movement.Employee.ID = employee.ID; return movement; } private void CreateStockBatch(Requisition entity, ref IList items) { if(!LoadRequisitionItems(entity, ref items)) { Log("CreateStockBatch() - no items to update!"); return; } var batch = new StockMovementBatch { Type = StockMovementBatchType.Issue, TimeStamp = entity.Filled, Notes = string.Format("Requisition #{0}", entity.Number) }; batch.Employee.ID = entity.Employee.ID; batch.Requisition.ID = entity.ID; FindSubStore().Save(batch, ""); var timestamp = entity.Filled; var updates = new List(); foreach (var item in items) { if (item.Product.NonStock) continue; var holdingQty = 0.0; var dimensions = item.Dimensions; if(item.JobLink.ID != Guid.Empty) { var holdings = Provider.Query( new Filter(x => x.Location.ID).IsEqualTo(item.Location.ID) .And(x => x.Product.ID).IsEqualTo(item.Product.ID) .And(x => x.Style.ID).IsEqualTo(item.Style.ID) .And(x => x.Dimensions).DimensionEquals(item.Dimensions) .And(x => x.Job.ID).IsEqualTo(item.JobLink.ID), Columns.None().Add(x => x.Units) ); holdingQty = holdings.Rows.FirstOrDefault()?.Get(x => x.Units) ?? 0.0; } var qty = item.ActualQuantity; var txnid = Guid.NewGuid(); if (holdingQty.IsEffectivelyLessThan(qty)) { // Don't pull more than the required quantity, meaning if the holding is negative, it will remain negative. var extraRequired = qty - Math.Max(0.0,holdingQty); // We're going to redirect this general stock direct to the target job, so reduce the amount required from the selected holding qty = holdingQty; // We don't have enough stock in this case, so transfer the necessary stock from general. We don't check for quantity in general stock, // but instead will let general stock go negative if not enough. Obviously we have the stock, because its being sent to site. So if we do // get negatives, it means probably our number are wrong I think. // Transfer the necessary balance from General Stock... var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, null, null, dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer"); from.Issued = extraRequired; from.Type = StockMovementType.TransferOut; from.Cost = item.Cost; timestamp = timestamp.AddTicks(1); // ... to the target job that we are issuing from. var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer"); to.Received = extraRequired; to.Type = StockMovementType.TransferIn; to.Cost = item.Cost; timestamp = timestamp.AddTicks(1); updates.Add(from); updates.Add(to); // The remainder that wasn't in the holding has now been moved straight to the target job/JRI } // if we have to change either job or JRI if (!qty.IsEffectivelyEqual(0.0) && (entity.JobLink.ID != item.JobLink.ID || item.JobRequisitionItem.ID != item.SourceJRI.ID)) { // Transfer from the item job to the requisition job var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, item.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer"); from.Issued = qty; from.Type = StockMovementType.TransferOut; from.Cost = item.Cost; timestamp = timestamp.AddTicks(1); // ... to the job. var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer"); to.Received = qty; to.Type = StockMovementType.TransferIn; to.Cost = item.Cost; timestamp = timestamp.AddTicks(1); updates.Add(from); updates.Add(to); } // Now we can issue to full original quantity to the entity job :-) var mvt = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid, item.Charge, item.JobScope, false, $"Requisition #{entity.Number}"); mvt.Issued = item.ActualQuantity; mvt.Type = StockMovementType.Issue; mvt.Cost = item.Cost; updates.Add(mvt); } FindSubStore().Save(updates, ""); } private void ClearStockBatch(Requisition entity) { Log("ClearStockBatch()"); var batches = Provider.Query( new Filter(x => x.Requisition.ID).IsEqualTo(entity.ID), Columns.None().Add(x => x.ID) ).Rows.Select(x => x.ToObject()); if (batches.Any()) FindSubStore().Delete(batches, ""); } private void UpdateStockBatches(Requisition entity, ref IList items) { Log("UpdateStockBatch() - starting"); if (!NeedsUpdating(entity, entity, x => x.StockUpdated)) return; var oldupdate = entity.GetOriginalValue(x => x.StockUpdated); var newupdate = entity.StockUpdated; // Gone from Blank to Updated -> Create a Batch if (oldupdate.IsEmpty() && !newupdate.IsEmpty()) { Log("UpdateStockBatch() - creating batch"); ClearStockBatch(entity); CreateStockBatch(entity, ref items); } // Gone from Updated to Blank -> Clear Out the Batch else if (newupdate.IsEmpty() && !oldupdate.IsEmpty()) { Log("UpdateStockBatch() - clearing batch"); ClearStockBatch(entity); } // Do nothing - Updated flag has been updated, not set or cleared Log("UpdateStockBatch() - done"); } #endregion } }