JobRequisitionItemStore.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using Comal.Classes;
  2. using Comal.Stores;
  3. using InABox.Core;
  4. using InABox.Database;
  5. using System;
  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. new Columns<StockMovement>(
  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 new Columns<JobRequisitionItem>(
  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.Notes);
  129. }
  130. /// <summary>
  131. /// Ensure that the columns of <paramref name="item"/> match <see cref="StatusRequiredColumns"/>.
  132. /// </summary>
  133. /// <param name="store"></param>
  134. /// <param name="item"></param>
  135. /// <returns></returns>
  136. public static bool CalculateStatus(IStore store, JobRequisitionItem item)
  137. {
  138. if (item.Archived != DateTime.MinValue)
  139. item.Status = JobRequisitionItemStatus.Archived;
  140. else if (item.Cancelled != DateTime.MinValue)
  141. item.Status = JobRequisitionItemStatus.Cancelled;
  142. else
  143. {
  144. // We don't care about that which has been issued, because we're just looking at how much was allocated.
  145. // If we cared about the issued movements as well, then after issuing a requi item, it would become unallocated.
  146. // However, we do include transfers out of this requi, since then the stuff ain't actually been allocated.
  147. var stockMovements = GetNotIssued(store, item.ID,
  148. new Columns<StockMovement>(x => x.Units)
  149. .Add(x => x.Style.ID));
  150. var styleTotal = 0.0;
  151. var total = 0.0;
  152. foreach (var mvt in stockMovements)
  153. {
  154. if (mvt.Style.ID == item.Style.ID)
  155. {
  156. styleTotal += mvt.Units;
  157. }
  158. total += mvt.Units;
  159. }
  160. var remStyle = item.Qty - styleTotal;
  161. var remTotal = item.Qty - total;
  162. if (remStyle <= 0)
  163. {
  164. // Now, we care about what's actually been issued.
  165. var issued = GetIssued(store, item.ID, new Columns<StockMovement>(x => x.Units));
  166. // If everything has been issued, the issued total will be a negative value to balance the Qty.
  167. if(item.Qty + issued.Sum(x => x.Units) <= 0)
  168. {
  169. item.Status = JobRequisitionItemStatus.Issued;
  170. }
  171. else
  172. {
  173. item.Status = JobRequisitionItemStatus.Allocated;
  174. }
  175. }
  176. else if (remTotal <= 0)
  177. {
  178. // Find all unreceived POItems for this guy that are treatments (i.e., wrong product ID).
  179. var jriPois = store.Provider.Query(
  180. new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID)
  181. .And(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(DateTime.MinValue)
  182. .And(x => x.PurchaseOrderItem.Product.ID).IsNotEqualTo(item.Product.ID),
  183. new Columns<JobRequisitionItemPurchaseOrderItem>(x => x.ID));
  184. if (jriPois.Rows.Count > 0)
  185. item.Status = JobRequisitionItemStatus.TreatmentOnOrder;
  186. else
  187. item.Status = JobRequisitionItemStatus.TreatmentRequired;
  188. }
  189. else
  190. {
  191. // Find all unreceived POItems for this guy.
  192. var jriPois = store.Provider.Query(
  193. new Filter<JobRequisitionItemPurchaseOrderItem>(x => x.JobRequisitionItem.ID).IsEqualTo(item.ID)
  194. .And(x => x.PurchaseOrderItem.ReceivedDate).IsEqualTo(DateTime.MinValue),
  195. new Columns<JobRequisitionItemPurchaseOrderItem>(x => x.PurchaseOrderItem.Product.ID)
  196. .Add(x => x.PurchaseOrderItem.Qty))
  197. .ToObjects<JobRequisitionItemPurchaseOrderItem>()
  198. .ToList();
  199. var stockOrders = jriPois.Where(x => x.PurchaseOrderItem.Product.ID == item.Product.ID).ToList();
  200. var treatmentOrders = jriPois.Where(x => x.PurchaseOrderItem.Product.ID != item.Product.ID).ToList();
  201. remTotal -= stockOrders.Sum(x => x.PurchaseOrderItem.Qty);
  202. if (remTotal <= 0)
  203. {
  204. if (stockOrders.Count > 0)
  205. item.Status = JobRequisitionItemStatus.OnOrder;
  206. else
  207. {
  208. // This should be impossible to reach. We are at this point because remTotal <= 0, but stockOrders was an empty list. Therefore
  209. // remTotal is was <= 0 before checking PurchaseOrderItems, but then we should be TreatmentRequired, as above.
  210. Logger.Send(LogType.Error, store.UserID, $"Internal assertion failed: there is enough stock, but we didn't reach the correct clause.");
  211. if (treatmentOrders.Count > 0)
  212. item.Status = JobRequisitionItemStatus.TreatmentOnOrder;
  213. else
  214. item.Status = JobRequisitionItemStatus.TreatmentRequired;
  215. }
  216. }
  217. else if (item.OrderRequired != DateTime.MinValue)
  218. item.Status = JobRequisitionItemStatus.OrderRequired;
  219. else
  220. {
  221. // Even after all the orders have come through, we still don't have enough. We must order more.
  222. item.Status = JobRequisitionItemStatus.NotChecked;
  223. }
  224. }
  225. }
  226. return item.HasOriginalValue(x => x.Status);
  227. }
  228. public static bool CalculateStatus(IStore store, Guid jobRequiItemID)
  229. {
  230. var item = store.Provider.Query(
  231. new Filter<JobRequisitionItem>(x => x.ID).IsEqualTo(jobRequiItemID),
  232. StatusRequiredColumns())
  233. .ToObjects<JobRequisitionItem>()
  234. .FirstOrDefault();
  235. if(item is null)
  236. {
  237. Logger.Send(LogType.Error, store.UserID, $"No {nameof(JobRequisitionItem)} with ID {jobRequiItemID}");
  238. return false;
  239. }
  240. return CalculateStatus(store, item);
  241. }
  242. public static void UpdateStatus(IStore store, Guid jobRequiItemID, JobRequisitionItemAction action)
  243. {
  244. Logger.Send(LogType.Information, "", " ** Updating Requisition Item Status ({0}) -> {1}",
  245. store.GetType().EntityName(), jobRequiItemID);
  246. var item = store.Provider.Query(
  247. new Filter<JobRequisitionItem>(x => x.ID).IsEqualTo(jobRequiItemID),
  248. StatusRequiredColumns())
  249. .ToObjects<JobRequisitionItem>()
  250. .FirstOrDefault();
  251. if (item is null)
  252. {
  253. Logger.Send(LogType.Error, store.UserID, $"No {nameof(JobRequisitionItem)} with ID {jobRequiItemID}");
  254. }
  255. else
  256. {
  257. if (CalculateStatus(store, item))
  258. {
  259. if (action != JobRequisitionItemAction.None)
  260. 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()}";
  261. store.Provider.Save(item);
  262. }
  263. }
  264. }
  265. }