JobRequisitionItemStore.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. using System;
  2. using Comal.Classes;
  3. using Comal.Stores;
  4. using InABox.Core;
  5. using InABox.Database;
  6. using System.Collections.Generic;
  7. using System.Linq;
  8. namespace PRSStores;
  9. public enum JobRequisitionItemAction
  10. {
  11. Created,
  12. Updated,
  13. Deleted,
  14. None
  15. }
  16. public class JobRequisitionItemStore : BaseStore<JobRequisitionItem>
  17. {
  18. protected override void BeforeSave(JobRequisitionItem item)
  19. {
  20. if (item.ID != Guid.Empty)
  21. {
  22. if (CalculateStatus(this, item))
  23. item.Notes += $"{(!String.IsNullOrWhiteSpace(item.Notes) ? "\n" : "")}Status updated to {item.Status.ToString().SplitCamelCase()} because the record was changed";
  24. }
  25. base.BeforeSave(item);
  26. }
  27. protected override void AfterSave(JobRequisitionItem entity)
  28. {
  29. base.AfterSave(entity);
  30. if(entity.HasOriginalValue(x => x.Cancelled))
  31. {
  32. CancelMovements(entity);
  33. }
  34. }
  35. private static IEnumerable<StockMovement> GetMovements(IStore store, Guid jriID, Filter<StockMovement>? filter, Columns<StockMovement> columns)
  36. {
  37. return store.Provider
  38. .Query(
  39. Filter<StockMovement>.And(
  40. new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(jriID),
  41. filter),
  42. columns)
  43. .ToObjects<StockMovement>();
  44. }
  45. private static IEnumerable<StockMovement> GetNotIssued(IStore store, Guid jriID, Columns<StockMovement> columns)
  46. {
  47. return GetMovements(store, jriID, new Filter<StockMovement>(x => x.Type).IsNotEqualTo(StockMovementType.Issue), columns);
  48. }
  49. private static IEnumerable<StockMovement> GetIssued(IStore store, Guid jriID, Columns<StockMovement> columns)
  50. {
  51. return GetMovements(store, jriID, new Filter<StockMovement>(x => x.Type).IsEqualTo(StockMovementType.Issue), columns);
  52. }
  53. private void CancelMovements(JobRequisitionItem entity)
  54. {
  55. // Here, we care about *all* movements into or out of this requi. If stuff has been issued, it must be included,
  56. // since we cannot return issued stock back to general stock for the job.
  57. var movements = GetMovements(this, entity.ID, null,
  58. Columns.None<StockMovement>().Add(
  59. x => x.Product.ID,
  60. x => x.Style.ID,
  61. x => x.Job.ID,
  62. x => x.Location.ID,
  63. x => x.Units,
  64. x => x.Cost,
  65. x => x.OrderItem.ID)
  66. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local));
  67. var newMovements = new List<StockMovement>();
  68. foreach(var movement in movements)
  69. {
  70. var from = movement.CreateMovement();
  71. from.Date = entity.Cancelled;
  72. from.Cost = movement.Cost;
  73. from.System = true;
  74. from.JobRequisitionItem.ID = entity.ID;
  75. from.OrderItem.ID = movement.OrderItem.ID;
  76. from.Notes = "Requisition item cancelled";
  77. var to = movement.CreateMovement();
  78. to.Date = entity.Cancelled;
  79. to.Cost = movement.Cost;
  80. to.System = true;
  81. to.Notes = "Requisition item cancelled";
  82. to.OrderItem.ID = movement.OrderItem.ID;
  83. to.Transaction = from.Transaction;
  84. if(movement.Units > 0)
  85. {
  86. // If this movement was an increase to reservation allocation, we create a transfer out of the reservation.
  87. from.Issued = movement.Units;
  88. to.Received = movement.Units;
  89. from.Type = StockMovementType.TransferOut;
  90. to.Type = StockMovementType.TransferIn;
  91. }
  92. else if(movement.Units < 0)
  93. {
  94. // If this movement was a decrease to reservation allocation, we create a transfer into the reservation.
  95. from.Received = -movement.Units;
  96. to.Issued = -movement.Units;
  97. from.Type = StockMovementType.TransferIn;
  98. to.Type = StockMovementType.TransferOut;
  99. }
  100. newMovements.Add(from);
  101. newMovements.Add(to);
  102. }
  103. if(newMovements.Count > 0)
  104. {
  105. var batch = new StockMovementBatch
  106. {
  107. Notes = "Requisition item cancelled."
  108. };
  109. FindSubStore<StockMovementBatch>().Save(batch, "");
  110. foreach(var mvt in newMovements)
  111. {
  112. mvt.Batch.ID = batch.ID;
  113. }
  114. FindSubStore<StockMovement>().Save(newMovements, "Requisition item cancelled.");
  115. }
  116. }
  117. public static Columns<JobRequisitionItem> StatusRequiredColumns()
  118. {
  119. return Columns.None<JobRequisitionItem>().Add(
  120. x => x.ID,
  121. x => x.Archived,
  122. x => x.Cancelled,
  123. x => x.OrderRequired,
  124. x => x.Status,
  125. x => x.Style.ID,
  126. x => x.Product.ID,
  127. x => x.Qty,
  128. x => x.Dimensions.Value,
  129. x => x.Notes);
  130. }
  131. /// <summary>
  132. /// Ensure that the columns of <paramref name="item"/> match <see cref="StatusRequiredColumns"/>.
  133. /// </summary>
  134. /// <param name="store"></param>
  135. /// <param name="item"></param>
  136. /// <returns></returns>
  137. public static bool CalculateStatus(IStore store, JobRequisitionItem item)
  138. {
  139. if (item.Archived != DateTime.MinValue)
  140. item.Status = JobRequisitionItemStatus.Archived;
  141. else if (item.Cancelled != DateTime.MinValue)
  142. item.Status = JobRequisitionItemStatus.Cancelled;
  143. else
  144. {
  145. // We don't care about that which has been issued, because we're just looking at how much was allocated.
  146. // If we cared about the issued movements as well, then after issuing a requi item, it would become unallocated.
  147. // However, we do include transfers out of this requi, since then the stuff ain't actually been allocated.
  148. var stockMovements = GetNotIssued(store, item.ID,
  149. Columns.None<StockMovement>().Add(x => x.Units)
  150. .Add(x => x.Style.ID)
  151. .Add(x=>x.Dimensions.Value));
  152. var styleTotal = 0.0;
  153. var total = 0.0;
  154. foreach (var mvt in stockMovements)
  155. {
  156. if (mvt.Style.ID == item.Style.ID)
  157. {
  158. styleTotal += mvt.Units * mvt.Dimensions.Value;
  159. }
  160. total += mvt.Units * mvt.Dimensions.Value;
  161. }
  162. var remStyle = (item.Qty * item.Dimensions.Value) - styleTotal;
  163. var remTotal = (item.Qty * item.Dimensions.Value) - total;
  164. if (remStyle <= 0)
  165. {
  166. // Now, we care about what's actually been issued.
  167. var issued = GetIssued(store, item.ID, Columns.None<StockMovement>().Add(x => x.Units).Add(x=>x.Dimensions.Value));
  168. // If everything has been issued, the issued total will be a negative value to balance the Qty.
  169. if(item.Qty + issued.Sum(x => x.Units * x.Dimensions.Value) <= 0)
  170. {
  171. item.Status = JobRequisitionItemStatus.Issued;
  172. }
  173. else
  174. {
  175. item.Status = JobRequisitionItemStatus.Allocated;
  176. }
  177. }
  178. else if (remTotal <= 0)
  179. {
  180. // Find all unreceived POItems for this guy that are treatments (i.e., wrong product ID).
  181. var jriPois = store.Provider.Query(
  182. new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID)
  183. .And(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(DateTime.MinValue)
  184. .And(x => x.PurchaseOrderItem.Product.ID).IsNotEqualTo(item.Product.ID),
  185. Columns.None<JobRequisitionItemPurchaseOrderItem>().Add(x => x.ID));
  186. if (jriPois.Rows.Count > 0)
  187. item.Status = JobRequisitionItemStatus.TreatmentOnOrder;
  188. else
  189. item.Status = JobRequisitionItemStatus.TreatmentRequired;
  190. }
  191. else
  192. {
  193. // Find all unreceived POItems for this guy.
  194. var jriPois = store.Provider.Query(
  195. new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID)
  196. .And(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(DateTime.MinValue),
  197. Columns.None<JobRequisitionItemPurchaseOrderItem>().Add(x => x.PurchaseOrderItem.Product.ID)
  198. .Add(x => x.PurchaseOrderItem.Qty)
  199. .Add(x=>x.PurchaseOrderItem.Dimensions.Value))
  200. .ToObjects<JobRequisitionItemPurchaseOrderItem>()
  201. .ToList();
  202. var stockOrders = jriPois.Where(x => x.PurchaseOrderItem.Product.ID == item.Product.ID).ToList();
  203. var treatmentOrders = jriPois.Where(x => x.PurchaseOrderItem.Product.ID != item.Product.ID).ToList();
  204. remTotal -= stockOrders.Sum(x => x.PurchaseOrderItem.Qty * x.PurchaseOrderItem.Dimensions.Value);
  205. if (remTotal <= 0)
  206. {
  207. if (stockOrders.Count > 0)
  208. item.Status = JobRequisitionItemStatus.OnOrder;
  209. else
  210. {
  211. // This should be impossible to reach. We are at this point because remTotal <= 0, but stockOrders was an empty list. Therefore
  212. // remTotal is was <= 0 before checking PurchaseOrderItems, but then we should be TreatmentRequired, as above.
  213. store.Logger.Send(LogType.Error, store.UserID, $"Internal assertion failed: there is enough stock, but we didn't reach the correct clause.");
  214. if (treatmentOrders.Count > 0)
  215. item.Status = JobRequisitionItemStatus.TreatmentOnOrder;
  216. else
  217. item.Status = JobRequisitionItemStatus.TreatmentRequired;
  218. }
  219. }
  220. else if (item.OrderRequired != DateTime.MinValue)
  221. item.Status = JobRequisitionItemStatus.OrderRequired;
  222. else
  223. {
  224. // Even after all the orders have come through, we still don't have enough. We must order more.
  225. item.Status = JobRequisitionItemStatus.NotChecked;
  226. }
  227. }
  228. }
  229. return item.HasOriginalValue(x => x.Status);
  230. }
  231. public static bool CalculateStatus(IStore store, Guid jobRequiItemID)
  232. {
  233. var item = store.Provider.Query(
  234. new Filter<JobRequisitionItem>(x => x.ID).IsEqualTo(jobRequiItemID),
  235. StatusRequiredColumns())
  236. .ToObjects<JobRequisitionItem>()
  237. .FirstOrDefault();
  238. if(item is null)
  239. {
  240. store.Logger.Send(LogType.Error, store.UserID, $"No {nameof(JobRequisitionItem)} with ID {jobRequiItemID}");
  241. return false;
  242. }
  243. return CalculateStatus(store, item);
  244. }
  245. public static void UpdateStatus(IStore store, Guid jobRequiItemID, JobRequisitionItemAction action)
  246. {
  247. store.Logger.Send(LogType.Information, "",
  248. $" ** Updating Requisition Item Status ({store.GetType().EntityName()}) -> {jobRequiItemID}");
  249. var item = store.Provider.Query(
  250. new Filter<JobRequisitionItem>(x => x.ID).IsEqualTo(jobRequiItemID),
  251. StatusRequiredColumns())
  252. .ToObjects<JobRequisitionItem>()
  253. .FirstOrDefault();
  254. if (item is null)
  255. {
  256. store.Logger.Send(LogType.Error, store.UserID, $"No {nameof(JobRequisitionItem)} with ID {jobRequiItemID}");
  257. }
  258. else
  259. {
  260. if (CalculateStatus(store, item))
  261. {
  262. if (action != JobRequisitionItemAction.None)
  263. item.Notes += $"{(!String.IsNullOrWhiteSpace(item.Notes) ? "\n" : "")}Status updated to {item.Status.ToString().SplitCamelCase()} because a {store.Type.EntityName().Split('.').Last().SplitCamelCase()} was {action.ToString().ToLower()}";
  264. store.Provider.Save(item);
  265. }
  266. }
  267. }
  268. }