RequisitionStore.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Linq.Expressions;
  4. using Comal.Classes;
  5. using InABox.Core;
  6. using System;
  7. using NPOI.Util;
  8. namespace Comal.Stores
  9. {
  10. internal class RequisitionStore : BaseStore<Requisition>
  11. {
  12. private readonly bool _debug = true;
  13. private void Log(string format, params object[] values)
  14. {
  15. if (_debug)
  16. Logger.Send(LogType.Information, UserID, string.Format("- RequisitionStore:" + format, values));
  17. }
  18. private bool NeedsUpdating<TObject, T>(Requisition entity, TObject obj, Expression<Func<TObject, T>> property)
  19. where TObject : BaseObject
  20. {
  21. // If this is a new Requisition, we don't need to do anything
  22. if (entity.HasOriginalValue(x => x.ID))
  23. {
  24. var originalid = entity.GetOriginalValue(x => x.ID);
  25. if (originalid == Guid.Empty)
  26. {
  27. Log("NeedsUpdating() return false - original id is empty");
  28. return false;
  29. }
  30. }
  31. // if the Property has not changed, we don't need to do anything
  32. if (!obj.HasOriginalValue(property))
  33. {
  34. Log("NeedsUpdating() return false - {0} has not changed", property.ToString());
  35. return false;
  36. }
  37. return true;
  38. }
  39. private bool LoadRequisitionItems(Requisition entity, ref IList<RequisitionItem> requisitionitems)
  40. {
  41. requisitionitems ??= Provider.Query(
  42. new Filter<RequisitionItem>(x => x.RequisitionLink.ID).IsEqualTo(entity.ID),
  43. Columns.None<RequisitionItem>().Add(x => x.ID)
  44. .Add(x => x.Description)
  45. .Add(x => x.Quantity)
  46. .Add(x => x.Code)
  47. .Add(x => x.Location.ID)
  48. .Add(x => x.Style.ID)
  49. .Add(x => x.Product.ID)
  50. .Add(x => x.Product.NonStock)
  51. .Add(x => x.SourceJRI.ID)
  52. .Add(x => x.JobRequisitionItem.ID)
  53. .Add(x =>x.JobLink.ID)
  54. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
  55. .Add(x => x.ActualQuantity)
  56. .AddSubColumns(x => x.Charge, Columns.All<ActualCharge>())
  57. .Add(x => x.JobScope.ID)
  58. .AddDimensionsColumns(x => x.Product.DefaultInstance.Dimensions, Dimensions.ColumnsType.Local)
  59. ).ToList<RequisitionItem>();
  60. return requisitionitems.Any();
  61. }
  62. private bool LoadDeliveryItems(Requisition entity, ref IList<DeliveryItem> deliveryitems)
  63. {
  64. deliveryitems ??= Provider.Query(
  65. new Filter<DeliveryItem>(x => x.RequisitionLink.ID).IsEqualTo(entity.ID),
  66. Columns.None<DeliveryItem>().Add(x => x.ID, x => x.DeliveredDate)
  67. ).ToList<DeliveryItem>();
  68. return deliveryitems.Any();
  69. }
  70. #region TakenBy
  71. private void UpdateTakenBy(Requisition entity, ref IList<RequisitionItem> items, ref IList<DeliveryItem> deliveryitems)
  72. {
  73. Log("UpdateTakenBy() - starting");
  74. if (!NeedsUpdating(entity, entity.TakenBy, x => x.ID))
  75. return;
  76. if (!LoadDeliveryItems(entity, ref deliveryitems))
  77. {
  78. Log("UpdateTakenBy() - no delivery items to update");
  79. return;
  80. }
  81. foreach (var deliveryitem in deliveryitems)
  82. if (entity.TakenBy.IsValid() && deliveryitem.DeliveredDate.IsEmpty())
  83. {
  84. Log("UpdateTakenBy() - Setting DeliveryDate");
  85. deliveryitem.DeliveredDate = DateTime.Now;
  86. }
  87. else if (!entity.TakenBy.IsValid() && !deliveryitem.DeliveredDate.IsEmpty())
  88. {
  89. Log("UpdateTakenBy() - Clearing DeliveryDate");
  90. deliveryitem.DeliveredDate = DateTime.MinValue;
  91. }
  92. var updates = deliveryitems.Where(x => x.IsChanged());
  93. if (updates.Any())
  94. FindSubStore<DeliveryItem>().Save(updates,
  95. entity.TakenBy.IsValid() ? "Requisition taken by " + entity.TakenBy.Code : "Requisition [TakenBy] has been cleared");
  96. Log("UpdateTakenBy() - done");
  97. }
  98. #endregion
  99. protected override void BeforeSave(Requisition entity)
  100. {
  101. base.BeforeSave(entity);
  102. if (entity.TakenBy.IsValid() || entity.Delivery.Completed != DateTime.MinValue || entity.Delivery.Delivered != DateTime.MinValue)
  103. {
  104. if (entity.Archived.IsEmpty())
  105. entity.Archived = DateTime.Now;
  106. }
  107. else
  108. {
  109. if (!entity.Archived.IsEmpty())
  110. entity.Archived = DateTime.MinValue;
  111. }
  112. }
  113. protected override void AfterSave(Requisition entity)
  114. {
  115. base.AfterSave(entity);
  116. IList<RequisitionItem> requisitionitems = null;
  117. IList<DeliveryItem> deliveryitems = null;
  118. UpdateDeliveryItems(entity, ref requisitionitems, ref deliveryitems);
  119. UpdateTakenBy(entity, ref requisitionitems, ref deliveryitems);
  120. UpdateStockBatches(entity, ref requisitionitems);
  121. UpdateTrackingKanban<RequisitionKanban, Requisition, RequisitionLink>(entity, e =>
  122. {
  123. if (!entity.Archived.Equals(DateTime.MinValue) || entity.TakenBy.IsValid())
  124. return KanbanStatus.Complete;
  125. if (entity.Delivery.IsValid())
  126. {
  127. if (entity.Delivery.Completed != DateTime.MinValue)
  128. {
  129. return KanbanStatus.Complete;
  130. }
  131. }
  132. if (!entity.Filled.Equals(DateTime.MinValue))
  133. return KanbanStatus.Waiting;
  134. if (Provider.Query(
  135. new Filter<RequisitionItem>(x => x.RequisitionLink.ID).IsEqualTo(entity.ID),
  136. Columns.None<RequisitionItem>().Add(x => x.ID)
  137. ).Rows.Any()
  138. )
  139. return KanbanStatus.InProgress;
  140. return KanbanStatus.Open;
  141. });
  142. }
  143. protected override void BeforeDelete(Requisition entity)
  144. {
  145. UnlinkTrackingKanban<RequisitionKanban, Requisition, RequisitionLink>(entity);
  146. }
  147. #region Delivery Items
  148. private void CreateDeliveryItems(Requisition entity, ref IList<RequisitionItem> requisitionitems,
  149. ref IList<DeliveryItem> deliveryitems)
  150. {
  151. if (!LoadRequisitionItems(entity, ref requisitionitems))
  152. {
  153. Log("CreateDeliveryItems() - no requisition items to update");
  154. return;
  155. }
  156. var updates = new List<DeliveryItem>();
  157. foreach (var item in requisitionitems)
  158. updates.Add(item.CreateDeliveryItem(entity));
  159. if (updates.Any())
  160. FindSubStore<DeliveryItem>().Save(updates, "Requisition [Filled] flag has been set");
  161. deliveryitems = updates;
  162. }
  163. private void ClearDeliveryItems(Requisition entity, ref IList<DeliveryItem> deliveryitems)
  164. {
  165. if (!LoadDeliveryItems(entity, ref deliveryitems))
  166. {
  167. Log("ClearDeliveryItems() - no delivery items to update");
  168. return;
  169. }
  170. if (deliveryitems.Any())
  171. FindSubStore<DeliveryItem>().Delete(deliveryitems, "Requisition [Filled] flag has been cleared");
  172. deliveryitems = new List<DeliveryItem>();
  173. }
  174. private void UpdateDeliveryItems(Requisition entity, ref IList<RequisitionItem> requisitionitems,
  175. ref IList<DeliveryItem> deliveryitems)
  176. {
  177. Log("UpdateDeliveryItems() - starting");
  178. if (!NeedsUpdating(entity, entity, x => x.Filled))
  179. {
  180. Log("UpdateDeliveryItems() - NeedsUpdate() return false");
  181. return;
  182. }
  183. var oldfilled = entity.GetOriginalValue(x => x.Filled);
  184. var newfilled = entity.Filled;
  185. // Gone from Blank to Filled -> Create a Batch
  186. if (oldfilled.IsEmpty() && !newfilled.IsEmpty())
  187. {
  188. Log("UpdateDeliveryItems() - Filled has been set");
  189. ClearDeliveryItems(entity, ref deliveryitems);
  190. CreateDeliveryItems(entity, ref requisitionitems, ref deliveryitems);
  191. }
  192. // Gone from Filled to Blank -> Clear Out the Batch
  193. else if (newfilled.IsEmpty() && !oldfilled.IsEmpty())
  194. {
  195. Log("UpdateDeliveryItems() - Filled has been cleared");
  196. ClearDeliveryItems(entity, ref deliveryitems);
  197. }
  198. // Do nothing - filled flag has been updated, not set or cleared
  199. Log("UpdateDeliveryItems() - done");
  200. }
  201. #endregion
  202. #region StockMovements
  203. private StockMovement CreateStockMovement(IEmployee employee, DateTime date, IStockMovementBatch batch, IProduct product, IStockLocation location,
  204. IProductStyle style, IJob? job, IJobRequisitionItem? jri, IDimensions dimensions, Guid txnid, ActualCharge charge, JobScopeLink scope, bool system, string note)
  205. {
  206. var movement = new StockMovement();
  207. movement.Batch.ID = batch.ID;
  208. movement.Product.ID = product.ID;
  209. movement.Location.ID = location.ID;
  210. movement.Style.ID = style.ID;
  211. movement.Job.ID = job?.ID ?? Guid.Empty;
  212. movement.JobRequisitionItem.ID = jri?.ID ?? Guid.Empty;
  213. movement.Dimensions.CopyFrom(dimensions);
  214. movement.Charge.CopyFrom(charge);
  215. movement.JobScope.CopyFrom(scope);
  216. movement.System = system;
  217. movement.Transaction = txnid;
  218. movement.Notes = note;
  219. movement.Date = date;
  220. movement.Employee.ID = employee.ID;
  221. return movement;
  222. }
  223. private void CreateStockBatch(Requisition entity, ref IList<RequisitionItem> items)
  224. {
  225. if(!LoadRequisitionItems(entity, ref items))
  226. {
  227. Log("CreateStockBatch() - no items to update!");
  228. return;
  229. }
  230. var batch = new StockMovementBatch
  231. {
  232. Type = StockMovementBatchType.Issue,
  233. TimeStamp = entity.Filled,
  234. Notes = string.Format("Requisition #{0}", entity.Number)
  235. };
  236. batch.Employee.ID = entity.Employee.ID;
  237. batch.Requisition.ID = entity.ID;
  238. FindSubStore<StockMovementBatch>().Save(batch, "");
  239. var updates = new List<StockMovement>();
  240. foreach (var item in items)
  241. {
  242. if (item.Product.NonStock) continue;
  243. var holdingQty = 0.0;
  244. var dimensions = item.Dimensions;
  245. if(item.JobLink.ID != Guid.Empty)
  246. {
  247. var holdings = Provider.Query<StockHolding>(
  248. new Filter<StockHolding>(x => x.Location.ID).IsEqualTo(item.Location.ID)
  249. .And(x => x.Product.ID).IsEqualTo(item.Product.ID)
  250. .And(x => x.Style.ID).IsEqualTo(item.Style.ID)
  251. .And(x => x.Dimensions).DimensionEquals(item.Dimensions)
  252. .And(x => x.Job.ID).IsEqualTo(item.JobLink.ID),
  253. Columns.None<StockHolding>().Add(x => x.Units)
  254. );
  255. holdingQty = holdings.Rows.FirstOrDefault()?.Get<StockHolding, double>(x => x.Units) ?? 0.0;
  256. }
  257. var qty = item.ActualQuantity;
  258. var timestamp = entity.Filled;
  259. var txnid = Guid.NewGuid();
  260. if (holdingQty.IsEffectivelyLessThan(qty))
  261. {
  262. // Don't pull more than the required quantity, meaning if the holding is negative, it will remain negative.
  263. var extraRequired = qty - Math.Max(0.0,holdingQty);
  264. // We're going to redirect this general stock direct to the target job, so reduce the amount required from the selected holding
  265. qty = holdingQty;
  266. // 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,
  267. // 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
  268. // get negatives, it means probably our number are wrong I think.
  269. // Transfer the necessary balance from General Stock...
  270. var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, null, null,
  271. dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer");
  272. from.Issued = extraRequired;
  273. from.Type = StockMovementType.TransferOut;
  274. timestamp = timestamp.AddTicks(1);
  275. // ... to the job - note this is the final (entity) job/JRI, not the holding job/JRI
  276. var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope, true,
  277. $"Requisition #{entity.Number} Internal Transfer");
  278. to.Received = extraRequired;
  279. to.Type = StockMovementType.TransferIn;
  280. timestamp = timestamp.AddTicks(1);
  281. updates.Add(from);
  282. updates.Add(to);
  283. // Now we have a full qty in the job holding, and we can issue to site.
  284. }
  285. // if we have to change either job or JRI
  286. if (!qty.IsEffectivelyEqual(0.0) && (entity.JobLink.ID != item.JobLink.ID || item.JobRequisitionItem.ID != item.SourceJRI.ID))
  287. {
  288. // Transfer from the item job to the requisition job
  289. var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, item.JobLink, item.JobRequisitionItem,
  290. dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer");
  291. from.Issued = qty;
  292. from.Type = StockMovementType.TransferOut;
  293. timestamp = timestamp.AddTicks(1);
  294. // ... to the job.
  295. var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope, true,
  296. $"Requisition #{entity.Number} Internal Transfer");
  297. to.Received = qty;
  298. to.Type = StockMovementType.TransferIn;
  299. timestamp = timestamp.AddTicks(1);
  300. updates.Add(from);
  301. updates.Add(to);
  302. }
  303. var mvt = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope,
  304. false,
  305. $"Requisition #{entity.Number}");
  306. // Now we can issue to full original quantity to the entity job :-)
  307. mvt.Issued = item.ActualQuantity;
  308. mvt.Type = StockMovementType.Issue;
  309. updates.Add(mvt);
  310. }
  311. FindSubStore<StockMovement>().Save(updates, "");
  312. }
  313. private void ClearStockBatch(Requisition entity)
  314. {
  315. Log("ClearStockBatch()");
  316. var batches = Provider.Query(
  317. new Filter<StockMovementBatch>(x => x.Requisition.ID).IsEqualTo(entity.ID),
  318. Columns.None<StockMovementBatch>().Add(x => x.ID)
  319. ).Rows.Select(x => x.ToObject<StockMovementBatch>());
  320. if (batches.Any())
  321. FindSubStore<StockMovementBatch>().Delete(batches, "");
  322. }
  323. private void UpdateStockBatches(Requisition entity, ref IList<RequisitionItem> items)
  324. {
  325. Log("UpdateStockBatch() - starting");
  326. if (!NeedsUpdating(entity, entity, x => x.StockUpdated))
  327. return;
  328. var oldupdate = entity.GetOriginalValue(x => x.StockUpdated);
  329. var newupdate = entity.StockUpdated;
  330. // Gone from Blank to Updated -> Create a Batch
  331. if (oldupdate.IsEmpty() && !newupdate.IsEmpty())
  332. {
  333. Log("UpdateStockBatch() - creating batch");
  334. ClearStockBatch(entity);
  335. CreateStockBatch(entity, ref items);
  336. }
  337. // Gone from Updated to Blank -> Clear Out the Batch
  338. else if (newupdate.IsEmpty() && !oldupdate.IsEmpty())
  339. {
  340. Log("UpdateStockBatch() - clearing batch");
  341. ClearStockBatch(entity);
  342. }
  343. // Do nothing - Updated flag has been updated, not set or cleared
  344. Log("UpdateStockBatch() - done");
  345. }
  346. #endregion
  347. }
  348. }