using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Comal.Classes; using InABox.Core; using System; 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), 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.JobRequisitionItem.ID) .Add(x =>x.JobLink.ID) .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local) .Add(x => x.ActualQuantity) .AddDimensionsColumns(x => x.Product.DefaultInstance.Dimensions, Dimensions.ColumnsType.Local) ).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, 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.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 updates = new List(); foreach (var item in items) { if (item.Product.NonStock) continue; var holdingQty = 0.0; 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 dimensions = item.Product.DefaultInstance.Dimensions; var txnid = Guid.NewGuid(); if(holdingQty < qty) { // Don't pull more than the required quantity, meaning if the holding is negative, it will remain negative. var extraRequired = Math.Max(qty - holdingQty, qty); // 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, entity.Filled, batch, item.Product, item.Location, item.Style, null, null, dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer"); from.Issued = extraRequired; from.Type = StockMovementType.TransferOut; // ... to the job. var to = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, item.JobLink, item.JobRequisitionItem, dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer"); to.Received = extraRequired; to.Type = StockMovementType.TransferIn; updates.Add(from); updates.Add(to); // Now we have a full qty in the job holding, and we can issue to site. } JobRequisitionItemLink? link = item.JobRequisitionItem; if (entity.JobLink.ID != item.JobLink.ID) { // Transfer from the item job to the requisition job var from = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, item.JobLink, link, dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer"); from.Issued = qty; from.Type = StockMovementType.TransferOut; // ... to the job. var to = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, entity.JobLink, null, dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer"); to.Received = qty; to.Type = StockMovementType.TransferIn; updates.Add(from); updates.Add(to); link = null; } var mvt = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, entity.JobLink, link, dimensions, txnid, false, $"Requisition #{entity.Number}"); mvt.Issued = qty; mvt.Type = StockMovementType.Issue; 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 } }