RequisitionStore.cs 18 KB

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