PurchaseOrderItemStore.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Threading.Tasks;
  4. using Comal.Classes;
  5. using InABox.Core;
  6. using PRSStores;
  7. using System;
  8. using InABox.Database;
  9. using InABox.Scripting;
  10. using NPOI.SS.Formula.Functions;
  11. using Columns = InABox.Core.Columns;
  12. namespace Comal.Stores;
  13. internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
  14. {
  15. static PurchaseOrderItemStore()
  16. {
  17. RegisterListener<ProductDimensionUnit>(ReloadProductDimensionUnitCache);
  18. }
  19. private static Dictionary<Guid, ScriptDocument>? _productdimensionunitcache = null;
  20. private static void ReloadProductDimensionUnitCache(Guid[]? ids)
  21. {
  22. if (_productdimensionunitcache == null)
  23. _productdimensionunitcache = new Dictionary<Guid, ScriptDocument>();
  24. var scripts = DbFactory.NewProvider(Logger.Main).Query(
  25. ids != null
  26. ? new Filter<ProductDimensionUnit>(x => x.ID).InList(ids)
  27. : null,
  28. Columns.None<ProductDimensionUnit>()
  29. .Add(x => x.ID)
  30. .Add(x => x.Conversion)
  31. ).ToDictionary<ProductDimensionUnit, Guid, String>(x => x.ID, x => x.Conversion);
  32. foreach (var id in scripts.Keys)
  33. {
  34. var doc = !String.IsNullOrWhiteSpace(scripts[id]) ? new ScriptDocument(scripts[id]) : null;
  35. if (doc?.Compile() == true)
  36. _productdimensionunitcache[id] = doc;
  37. else
  38. _productdimensionunitcache.Remove(id);
  39. }
  40. }
  41. private void TransformDimensions(PurchaseOrderItem item)
  42. {
  43. if (_productdimensionunitcache == null)
  44. ReloadProductDimensionUnitCache(null);
  45. if (_productdimensionunitcache?.TryGetValue(item.Dimensions.Unit.ID, out ScriptDocument? script) == true)
  46. script.Execute("Module",DimensionUnit.ConvertDimensionsMethodName(), [item]);
  47. }
  48. private void UpdateStockMovements(PurchaseOrderItem entity)
  49. {
  50. var movements = Provider.Query<StockMovement>(
  51. new Filter<StockMovement>(x => x.OrderItem.ID).IsEqualTo(entity.ID))
  52. .ToArray<StockMovement>();
  53. foreach(var mvt in movements)
  54. {
  55. mvt.Date = entity.ReceivedDate;
  56. mvt.Cost = entity.Cost;
  57. }
  58. FindSubStore<StockMovement>().Save(movements, "Updated by purchase order modification");
  59. }
  60. private void CreateStockMovements(PurchaseOrderItem entity)
  61. {
  62. if (!entity.Product.IsValid())
  63. {
  64. Logger.Send(LogType.Information, UserID, "PurchaseOrderItem.Product.ID is blank!");
  65. return;
  66. }
  67. if (entity.Qty == 0)
  68. {
  69. Logger.Send(LogType.Information, UserID, "PurchaseOrderItem Qty is blank!");
  70. return;
  71. }
  72. var locationid = entity.StockLocation.ID;
  73. var locationValid = entity.StockLocation.IsValid();
  74. var poiaTask = Task.Run(() =>
  75. {
  76. return Provider.Query(
  77. new Filter<PurchaseOrderItemAllocation>(x => x.Item.ID).IsEqualTo(entity.ID),
  78. Columns.None<PurchaseOrderItemAllocation>()
  79. .Add(x => x.ID)
  80. .Add(x => x.JobRequisitionItem.ID)
  81. .Add(x => x.JobRequisitionItem.Cancelled))
  82. .ToArray<PurchaseOrderItemAllocation>();
  83. });
  84. var consigntask = Task.Run(() =>
  85. {
  86. if (entity.Consignment.ID != Guid.Empty && !entity.Consignment.ExTax.IsEffectivelyEqual(0.0))
  87. {
  88. var values = Provider.Query(
  89. new Filter<PurchaseOrderItem>(x => x.Consignment.ID).IsEqualTo(entity.Consignment.ID),
  90. Columns.None<PurchaseOrderItem>().Add(x => x.ExTax)
  91. ).Rows.Select(r => r.Get<PurchaseOrderItem, double>(c => c.ExTax));
  92. return values.Sum();
  93. }
  94. else
  95. {
  96. return 0.0;
  97. }
  98. });
  99. var instancetask = Task.Run(() =>
  100. {
  101. return Provider.Query(
  102. new Filter<ProductInstance>(x => x.Product.ID).IsEqualTo(entity.Product.ID)
  103. .And(x => x.Style.ID).IsEqualTo(entity.Style.ID)
  104. .And(x => x.Dimensions).DimensionEquals(entity.Dimensions),
  105. Columns.Required<ProductInstance>()
  106. .Add(x => x.ID)
  107. .Add(x => x.FreeStock)
  108. .Add(x => x.AverageCost)
  109. .Add(x => x.LastCost)
  110. ).Rows.FirstOrDefault();
  111. });
  112. var producttask = Task.Run(
  113. () => Provider.Query(
  114. new Filter<Product>(x => x.ID).IsEqualTo(entity.Product.ID),
  115. Columns.None<Product>()
  116. .Add(x => x.ID)
  117. .Add(x => x.DefaultLocation.ID)
  118. .Add(x => x.Warehouse.ID)
  119. .AddDimensionsColumns(x => x.DefaultInstance.Dimensions, Dimensions.ColumnsType.All)
  120. .Add(x => x.NonStock))
  121. .Rows.FirstOrDefault());
  122. var locationtask = Task.Run(
  123. () => Provider.Query(
  124. new Filter<StockLocation>(x => x.Default).IsEqualTo(true),
  125. Columns.None<StockLocation>().Add(x => x.ID, x => x.Warehouse.ID, x => x.Warehouse.Default)));
  126. Task.WaitAll(producttask, locationtask, instancetask, poiaTask, consigntask);
  127. var instancerow = instancetask.Result;
  128. var productrow = producttask.Result;
  129. var defaultlocations = locationtask.Result;
  130. var allocations = poiaTask.Result.ToArray();
  131. if (productrow is null)
  132. {
  133. Logger.Send(LogType.Information, UserID, "Cannot Find PurchaseOrderItem.Product.ID!");
  134. return;
  135. }
  136. if (productrow.Get<Product, bool>(x => x.NonStock))
  137. {
  138. Logger.Send(LogType.Information, UserID, "PurchaseOrderItem.Product is marked as Non Stock!");
  139. return;
  140. }
  141. if (!locationValid)
  142. {
  143. Logger.Send(LogType.Information, UserID, "PurchaseOrderItem.Location.ID is blank!");
  144. var productlocationid = productrow.EntityLinkID<Product, StockLocationLink>(x => x.DefaultLocation) ?? Guid.Empty;
  145. if (productlocationid != Guid.Empty)
  146. {
  147. Logger.Send(LogType.Information, UserID, "- Using Product.DefaultLocation.ID as location");
  148. locationid = productlocationid;
  149. }
  150. else
  151. {
  152. var productwarehouseid = productrow.Get<Product, Guid>(c => c.Warehouse.ID);
  153. var row = defaultlocations.Rows.FirstOrDefault(r => r.Get<StockLocation, Guid>(c => c.Warehouse.ID) == productwarehouseid);
  154. if (row != null)
  155. {
  156. Logger.Send(LogType.Information, UserID, "- Using Product.Warehouse -> Default as location");
  157. locationid = row.Get<StockLocation, Guid>(x => x.ID);
  158. }
  159. }
  160. if (locationid == Guid.Empty)
  161. {
  162. var row = defaultlocations.Rows.FirstOrDefault(r => r.Get<StockLocation, bool>(c => c.Warehouse.Default));
  163. if (row != null)
  164. {
  165. Logger.Send(LogType.Information, UserID, "- Using Default Warehouse -> Default Location as location");
  166. locationid = row.Get<StockLocation, Guid>(x => x.ID);
  167. }
  168. }
  169. if (locationid == Guid.Empty)
  170. {
  171. Logger.Send(LogType.Information, UserID, "- Cannot find Location : Skipping Movement Creation");
  172. return;
  173. }
  174. }
  175. if (entity.Dimensions.Unit.ID == Guid.Empty
  176. && entity.Dimensions.Height == 0
  177. && entity.Dimensions.Width == 0
  178. && entity.Dimensions.Length == 0
  179. && entity.Dimensions.Weight == 0)
  180. {
  181. Logger.Send(LogType.Information, UserID, "PurchaseOrderItem.Unit Size is zero!");
  182. entity.Dimensions.CopyFrom(productrow.ToObject<Product>().DefaultInstance.Dimensions);
  183. }
  184. TransformDimensions(entity);
  185. // Actual logic begins here.
  186. var batch = new StockMovementBatch
  187. {
  188. Type = StockMovementBatchType.Receipt,
  189. TimeStamp = DateTime.Now,
  190. Notes = $"Received on PO"
  191. };
  192. var movements = new List<StockMovement>();
  193. var _pototal = entity.Qty;
  194. var poCost = entity.Cost;
  195. if (!consigntask.Result.IsEffectivelyEqual(0.0)
  196. && !entity.Qty.IsEffectivelyEqual(0.0))
  197. {
  198. poCost += entity.Cost * entity.Consignment.ExTax / consigntask.Result;
  199. }
  200. foreach (var poia in allocations)
  201. {
  202. var jri = poia.JobRequisitionItem.ID == Guid.Empty ? null : poia.JobRequisitionItem;
  203. CreateMovement(entity, locationid, movements, poia.Job, jri, poia.Quantity, poCost);
  204. //CreateMovement(entity, locationid, movements, poia.Job, poia.JobRequisitionItem, poia.Quantity, poCost);
  205. //// Going through each jri, make sure we don't allocate more than the po line allows
  206. //var jriQty = Math.Min(jri.Qty, _pototal);
  207. //// And reduce the po balance by the jri Allocation
  208. //_pototal -= jriQty;
  209. //
  210. //// Let's not make zero-quantity transactions
  211. //if (!jriQty.IsEffectivelyEqual(0.0))
  212. // CreateMovement(entity, locationid, movements, jri, jriQty, poCost);
  213. }
  214. var totalAllocations = allocations.Sum(x => x.Quantity);
  215. var freeQty = entity.Qty - totalAllocations;
  216. // If there is any left over (ie over/under-ordered), now we can create
  217. // a second transaction to receive the unallocated stock
  218. CreateMovement(entity, locationid, movements, null, null, freeQty, poCost);
  219. FindSubStore<StockMovementBatch>().Save(batch, "Received on PO");
  220. foreach(var mvt in movements)
  221. {
  222. mvt.Batch.ID = batch.ID;
  223. }
  224. FindSubStore<StockMovement>().Save(movements, "Updated by Purchase Order Modification");
  225. // Update the AverageCost on the instance
  226. if (!freeQty.IsEffectivelyEqual(0.0))
  227. {
  228. var instance = instancerow?.ToObject<ProductInstance>();
  229. if (instance == null)
  230. {
  231. instance = new ProductInstance();
  232. instance.Product.ID = entity.Product.ID;
  233. instance.Style.ID = entity.Style.ID;
  234. instance.Dimensions.CopyFrom(entity.Dimensions);
  235. }
  236. instance.LastCost = entity.Cost;
  237. var freeqty = instance.FreeStock;
  238. var freeavg = instance.AverageCost;
  239. var freecost = instance.FreeStock * freeavg;
  240. var poqty = freeQty * (Math.Abs(entity.Dimensions.Value) > 0.0001F ? entity.Dimensions.Value : 1.0F);
  241. var pocost = entity.Cost * poqty;
  242. if (!consigntask.Result.IsEffectivelyEqual(0.0))
  243. pocost += freeQty * entity.Cost * entity.Consignment.ExTax / consigntask.Result;
  244. var totalqty = freeqty + poqty;
  245. var totalcost = freecost + pocost;
  246. var averagecost = Math.Abs(totalqty) > 0.0001F
  247. ? totalcost / totalqty
  248. : pocost;
  249. if (Math.Abs(averagecost - freeavg) > 0.0001F)
  250. {
  251. instance.AverageCost = averagecost;
  252. FindSubStore<ProductInstance>().Save(instance,
  253. $"Updated Average Cost: " +
  254. $"({freeqty} @ {freeavg:C2}) + ({poqty} @ {entity.Cost:C2}) = {totalcost:C2} / {totalqty}"
  255. );
  256. }
  257. }
  258. entity.CancelChanges();
  259. }
  260. private static void CreateMovement(PurchaseOrderItem entity, Guid locationid, List<StockMovement> movements, IJob? job, IJobRequisitionItem? jri, double qty, double cost)
  261. {
  262. if (qty.IsEffectivelyEqual(0.0)) return;
  263. var movement = new StockMovement();
  264. movement.Product.ID = entity.Product.ID;
  265. if(job is not null)
  266. {
  267. movement.Job.ID = job.ID;
  268. }
  269. movement.Location.ID = locationid;
  270. movement.Style.ID = entity.Style.ID;
  271. movement.Dimensions.CopyFrom(entity.Dimensions);
  272. movement.Date = entity.ReceivedDate;
  273. movement.Received = qty;
  274. movement.Employee.ID = Guid.Empty;
  275. movement.OrderItem.ID = entity.ID;
  276. movement.Notes = string.Format("Received on PO {0}", entity.PurchaseOrderLink.PONumber);
  277. movement.Cost = cost;
  278. movement.Type = StockMovementType.Receive;
  279. movements.Add(movement);
  280. if (jri is not null)
  281. {
  282. movement.JobRequisitionItem.ID = jri.ID;
  283. if (!jri.Cancelled.IsEmpty())
  284. {
  285. // We need to create an immediate transfer in and out of the job requisition item.
  286. var tOut = movement.CreateMovement();
  287. tOut.JobRequisitionItem.ID = jri.ID;
  288. tOut.Date = entity.ReceivedDate;
  289. tOut.Issued = qty;
  290. tOut.OrderItem.ID = entity.ID;
  291. tOut.Notes = "Internal transfer from cancelled requisition";
  292. tOut.System = true;
  293. tOut.Cost = entity.Cost;
  294. tOut.Type = StockMovementType.TransferOut;
  295. var tIn = movement.CreateMovement();
  296. tIn.Transaction = tOut.Transaction;
  297. tIn.Date = entity.ReceivedDate;
  298. tIn.Received = qty;
  299. tIn.OrderItem.ID = entity.ID;
  300. tOut.Notes = "Internal transfer from cancelled requisition";
  301. tOut.System = true;
  302. tIn.Cost = entity.Cost;
  303. tIn.Type = StockMovementType.TransferIn;
  304. movements.Add(tOut);
  305. movements.Add(tIn);
  306. }
  307. }
  308. }
  309. private void DeleteStockMovements(PurchaseOrderItem entity)
  310. {
  311. var movements = Provider.Query(
  312. new Filter<StockMovement>(x => x.OrderItem.ID).IsEqualTo(entity.ID),
  313. Columns.None<StockMovement>().Add(x => x.ID)
  314. ).Rows.Select(x => x.ToObject<StockMovement>());
  315. if (movements.Any())
  316. FindSubStore<StockMovement>().Delete(movements, "Purchase Order Item marked as Unreceived");
  317. }
  318. protected override void AfterSave(PurchaseOrderItem entity)
  319. {
  320. base.AfterSave(entity);
  321. if (entity.HasOriginalValue(x=>x.ReceivedDate))
  322. {
  323. if (entity.ReceivedDate.IsEmpty())
  324. DeleteStockMovements(entity);
  325. else
  326. {
  327. var original = entity.GetOriginalValue(x => x.ReceivedDate);
  328. if(original == DateTime.MinValue)
  329. {
  330. var item = Provider.Query(
  331. new Filter<PurchaseOrderItem>(x => x.ID).IsEqualTo(entity.ID),
  332. LookupFactory.RequiredColumns<PurchaseOrderItem>())
  333. .ToObjects<PurchaseOrderItem>().FirstOrDefault();
  334. if(item is not null)
  335. CreateStockMovements(item);
  336. }
  337. else
  338. {
  339. var item = Provider.Query(
  340. new Filter<PurchaseOrderItem>(x => x.ID).IsEqualTo(entity.ID),
  341. Columns.None<PurchaseOrderItem>().Add(x => x.ID)
  342. .Add(x => x.ReceivedDate)
  343. .Add(x => x.Cost))
  344. .ToObjects<PurchaseOrderItem>().FirstOrDefault();
  345. if(item is not null)
  346. UpdateStockMovements(item);
  347. }
  348. }
  349. }
  350. }
  351. private Guid GetJobRequisitionID(PurchaseOrderItem entity)
  352. {
  353. var jri = Provider.Query(
  354. new Filter<PurchaseOrderItemAllocation>(x => x.Item.ID).IsEqualTo(entity.ID),
  355. Columns.Required<PurchaseOrderItemAllocation>())
  356. .Rows
  357. .FirstOrDefault()?
  358. .Get<PurchaseOrderItemAllocation, Guid>(x => x.JobRequisitionItem.ID) ?? Guid.Empty;
  359. return jri;
  360. }
  361. protected override void BeforeDelete(PurchaseOrderItem entity)
  362. {
  363. base.BeforeDelete(entity);
  364. DeleteStockMovements(entity);
  365. }
  366. }