StockHolding.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Linq.Expressions;
  5. using InABox.Core;
  6. using PRSClasses;
  7. namespace Comal.Classes
  8. {
  9. // public class StockHoldingUnitAggregate : CoreAggregate<StockHolding, StockMovement, double>
  10. // {
  11. // public override Expression<Func<StockMovement, double>> Aggregate => x => x.Units;
  12. //
  13. // public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
  14. // new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
  15. // {
  16. // { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
  17. // { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
  18. // { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
  19. // { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
  20. // { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
  21. // { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
  22. // { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
  23. // { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
  24. // { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
  25. // { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
  26. // };
  27. //
  28. // public override AggregateCalculation Calculation => AggregateCalculation.Sum;
  29. // }
  30. //
  31. // public class StockHoldingValueAggregate : CoreAggregate<StockHolding, StockMovement, double>
  32. // {
  33. // public override Expression<Func<StockMovement, double>> Aggregate => x => x.Value;
  34. //
  35. // public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
  36. // new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
  37. // {
  38. // { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
  39. // { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
  40. // { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
  41. // { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
  42. // { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
  43. // { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
  44. // { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
  45. // { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
  46. // { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
  47. // { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
  48. // };
  49. //
  50. // public override AggregateCalculation Calculation => AggregateCalculation.Sum;
  51. // }
  52. //
  53. // public class StockHoldingAverageValueFormula : IFormula<StockHolding, double>
  54. // {
  55. // public Expression<Func<StockHolding, double>> Value => x => x.Value;
  56. //
  57. // public Expression<Func<StockHolding, double>>[] Modifiers => new Expression<Func<StockHolding, double>>[] { x => x.Units };
  58. //
  59. // public FormulaOperator Operator => FormulaOperator.Divide;
  60. //
  61. // public FormulaType Type => FormulaType.Virtual;
  62. // }
  63. //
  64. // public class StockHoldingQuantityAggregate : CoreAggregate<StockHolding, StockMovement, double>
  65. // {
  66. // public override Expression<Func<StockMovement, double>> Aggregate => x => x.Qty;
  67. //
  68. // public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
  69. // new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
  70. // {
  71. // { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
  72. // { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
  73. // { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
  74. // { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
  75. // { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
  76. // { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
  77. // { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
  78. // { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
  79. // { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
  80. // { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
  81. // };
  82. //
  83. // public override AggregateCalculation Calculation => AggregateCalculation.Sum;
  84. // }
  85. // public class StockHoldingAvailableAggregate : CoreAggregate<StockHolding, StockMovement, double>
  86. // {
  87. // public override Expression<Func<StockMovement, double>> Aggregate => x => x.Units;
  88. //
  89. // public override Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>> Links =>
  90. // new Dictionary<Expression<Func<StockMovement, object>>, Expression<Func<StockHolding, object>>>()
  91. // {
  92. // { StockMovement => StockMovement.Product.ID, StockHolding => StockHolding.Product.ID },
  93. // { StockMovement => StockMovement.Job.ID, StockHolding => StockHolding.Job.ID },
  94. // { StockMovement => StockMovement.Location.ID, StockHolding => StockHolding.Location.ID },
  95. // { StockMovement => StockMovement.Style.ID, StockHolding => StockHolding.Style.ID },
  96. // { StockMovement => StockMovement.Dimensions.Unit.ID, StockHolding => StockHolding.Dimensions.Unit.ID },
  97. // { StockMovement => StockMovement.Dimensions.Quantity, StockHolding => StockHolding.Dimensions.Quantity },
  98. // { StockMovement => StockMovement.Dimensions.Length, StockHolding => StockHolding.Dimensions.Length },
  99. // { StockMovement => StockMovement.Dimensions.Width, StockHolding => StockHolding.Dimensions.Width },
  100. // { StockMovement => StockMovement.Dimensions.Height, StockHolding => StockHolding.Dimensions.Height },
  101. // { StockMovement => StockMovement.Dimensions.Weight, StockHolding => StockHolding.Dimensions.Weight },
  102. // };
  103. //
  104. // public override AggregateCalculation Calculation => AggregateCalculation.Sum;
  105. //
  106. // public override Filter<StockMovement>? Filter => new Filter<StockMovement>(x => x.JobRequisitionItem.ID).IsEqualTo(Guid.Empty);
  107. // }
  108. // public class StockHoldingWeightFormula : IFormula<StockHolding, double>
  109. // {
  110. // public Expression<Func<StockHolding, double>> Value => x => x.Qty;
  111. //
  112. // public Expression<Func<StockHolding, double>>[] Modifiers => new Expression<Func<StockHolding, double>>[] { x => x.Dimensions.Weight };
  113. //
  114. // public FormulaOperator Operator => FormulaOperator.Multiply;
  115. //
  116. // public FormulaType Type => FormulaType.Virtual;
  117. // }
  118. //
  119. // public class StockHoldingIsRemnantCondition : ICondition<StockHolding, double, object>
  120. // {
  121. // public Expression<Func<StockHolding, double>> Left => x => x.Dimensions.Value;
  122. //
  123. // public Condition Condition => Condition.LessThan;
  124. //
  125. // public Expression<Func<StockHolding, double>> Right => x => x.Product.DefaultInstance.Dimensions.Value;
  126. //
  127. // public Expression<Func<StockHolding, object>> True => x => true;
  128. //
  129. // public Expression<Func<StockHolding, object>> False => x => null;
  130. //
  131. // public ConditionType Type => ConditionType.Virtual;
  132. // }
  133. // public class StockHoldingUnionGenerator : AutoEntityUnionGenerator<IStockHolding>
  134. // {
  135. // protected override void Configure()
  136. // {
  137. // AddTable<StockMovement>();
  138. // }
  139. //
  140. // public override bool Distinct => true;
  141. //
  142. // public override Column<IStockHolding>[] IDColumns => Columns;
  143. //
  144. // public static Column<IStockHolding>[] Columns => new Column<IStockHolding>[]
  145. // {
  146. // new Column<IStockHolding>(x => x.Job.ID),
  147. // new Column<IStockHolding>(x => x.Location.ID),
  148. // new Column<IStockHolding>(x => x.Product.ID),
  149. // new Column<IStockHolding>(x => x.Style.ID),
  150. // new Column<IStockHolding>(x => x.Dimensions.Unit.ID),
  151. // new Column<IStockHolding>(x => x.Dimensions.Quantity),
  152. // new Column<IStockHolding>(x => x.Dimensions.Length),
  153. // new Column<IStockHolding>(x => x.Dimensions.Width),
  154. // new Column<IStockHolding>(x => x.Dimensions.Height),
  155. // new Column<IStockHolding>(x => x.Dimensions.Weight),
  156. // };
  157. //
  158. // public static Filter<StockMovement>? GetFilter(IStockHolding holding)
  159. // {
  160. // var filter = new Filters<StockMovement>();
  161. //
  162. // foreach(var column in Columns)
  163. // {
  164. // filter.Add(new Filter<StockMovement>(column.Cast<StockMovement>()).IsEqualTo(CoreUtils.GetPropertyValue(holding, column.Property)));
  165. // }
  166. //
  167. // return filter.Combine();
  168. // }
  169. // }
  170. //
  171. // [UserTracking(typeof(StockMovement))]
  172. // [AutoEntity(typeof(StockHoldingUnionGenerator))]
  173. // public class StockHoldingView : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>,
  174. // IStockHolding, ILicense<WarehouseLicense>
  175. // {
  176. //
  177. // public override ProductLink Product { get; set; }
  178. //
  179. // public ProductStyleLink Style { get; set; }
  180. //
  181. // public StockLocationLink Location { get; set; }
  182. //
  183. // public JobLink Job { get; set; }
  184. //
  185. // [RequiredColumn]
  186. // [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
  187. // public override StockDimensions Dimensions { get; set; }
  188. //
  189. // [Condition(typeof(StockHoldingIsRemnantCondition))]
  190. // [NullEditor]
  191. // public bool IsRemnant { get; set; }
  192. //
  193. // [Aggregate(typeof(StockHoldingUnitAggregate))]
  194. // [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  195. // public double Units { get; set; }
  196. //
  197. // [Aggregate(typeof(StockHoldingQuantityAggregate))]
  198. // [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  199. // public double Qty { get; set; }
  200. //
  201. // [Formula(typeof(StockHoldingWeightFormula))]
  202. // [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  203. // public double Weight { get; set; }
  204. //
  205. // [Aggregate(typeof(StockHoldingValueAggregate))]
  206. // [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  207. // public double Value { get; set; }
  208. //
  209. // [Formula(typeof(StockHoldingAverageValueFormula))]
  210. // [DoubleEditor(Editable = Editable.Disabled)]
  211. // public double AverageValue { get; set; }
  212. // }
  213. // FV 6/2/24: This used to be a view (see above), but we were running into performance issues trying to
  214. // get aggregates fof stock locations (ie show me how many holdings in a given location was taking upwrds of
  215. // 20 seconds. Have moved update logic to StockHoldingStore / StockMovementStore so updates are a bit slower,
  216. // but reads are now much faster.
  217. [UserTracking(typeof(StockMovement))]
  218. [Unrecoverable]
  219. public class StockHolding : StockEntity, IRemotable, IPersistent, IOneToMany<StockLocation>, IOneToMany<Product>,
  220. IStockHolding, ILicense<WarehouseLicense>
  221. {
  222. [Editable(Editable.Disabled)]
  223. [EditorSequence(1)]
  224. public StockLocationLink Location { get; set; }
  225. private class ProductLookupGenerator : LookupDefinitionGenerator<Product, StockHolding>
  226. {
  227. public override Filter<Product>? DefineFilter(StockHolding[] items)
  228. => LookupFactory.DefineFilter<Product>().And(x => x.NonStock).IsEqualTo(false);
  229. }
  230. [Editable(Editable.Disabled)]
  231. [EditorSequence(2)]
  232. [LookupDefinition(typeof(ProductLookupGenerator))]
  233. public override ProductLink Product { get; set; }
  234. [DimensionsEditor(typeof(StockDimensions), AllowEditingUnit = false)]
  235. [Editable(Editable.Disabled)]
  236. [EditorSequence(3)]
  237. public override StockDimensions Dimensions { get; set; }
  238. [Editable(Editable.Disabled)]
  239. [EditorSequence(4)]
  240. public ProductStyleLink Style { get; set; }
  241. [Editable(Editable.Disabled)]
  242. [EditorSequence(4)]
  243. public JobLink Job { get; set; }
  244. [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  245. [EditorSequence(5)]
  246. public double Units { get; set; }
  247. [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  248. [EditorSequence(6)]
  249. public double Qty { get; set; }
  250. [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  251. [EditorSequence(7)]
  252. public double Weight { get; set; }
  253. [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  254. [EditorSequence(8)]
  255. public double Value { get; set; }
  256. [DoubleEditor(Editable = Editable.Disabled)]
  257. [EditorSequence(9)]
  258. public double AverageValue { get; set; }
  259. [DoubleEditor(Editable = Editable.Disabled, Summary = Summary.Sum)]
  260. [EditorSequence(10)]
  261. public double Available { get; set; }
  262. public static Column<IStockHolding>[] Columns => new Column<IStockHolding>[]
  263. {
  264. new Column<IStockHolding>(x => x.Job.ID),
  265. new Column<IStockHolding>(x => x.Location.ID),
  266. new Column<IStockHolding>(x => x.Product.ID),
  267. new Column<IStockHolding>(x => x.Style.ID),
  268. new Column<IStockHolding>(x => x.Dimensions.Unit.ID),
  269. new Column<IStockHolding>(x => x.Dimensions.Quantity),
  270. new Column<IStockHolding>(x => x.Dimensions.Length),
  271. new Column<IStockHolding>(x => x.Dimensions.Width),
  272. new Column<IStockHolding>(x => x.Dimensions.Height),
  273. new Column<IStockHolding>(x => x.Dimensions.Weight),
  274. };
  275. public static Filter<StockMovement>? GetFilter(IStockHolding holding)
  276. {
  277. var filter = new Filters<StockMovement>();
  278. foreach(var column in Columns)
  279. {
  280. filter.Add(new Filter<StockMovement>(column.Cast<StockMovement>()).IsEqualTo(CoreUtils.GetPropertyValue(holding, column.Property)));
  281. }
  282. return filter.Combine();
  283. }
  284. }
  285. public static class StockHoldingExtensions
  286. {
  287. /// <summary>
  288. /// Create a new stock movement from an <see cref="IStockHolding"/>, copying across the "key" properties;
  289. /// that is, the job, product, style, location and dimensions.
  290. /// </summary>
  291. /// <param name="holding"></param>
  292. /// <returns></returns>
  293. public static StockMovement CreateMovement(this IStockHolding holding)
  294. {
  295. var movement = new StockMovement();
  296. movement.Job.ID = holding.Job.ID;
  297. movement.Job.Synchronise(holding.Job);
  298. movement.Product.ID = holding.Product.ID;
  299. movement.Product.Synchronise(holding.Product);
  300. movement.Style.ID = holding.Style.ID;
  301. movement.Style.Synchronise(holding.Style);
  302. movement.Location.ID = holding.Location.ID;
  303. movement.Location.Synchronise(holding.Location);
  304. movement.Dimensions.CopyFrom(holding.Dimensions);
  305. return movement;
  306. }
  307. public static IEnumerable<StockHolding> GroupMovements(IEnumerable<StockMovement> movements)
  308. {
  309. var grouped = new List<StockHolding>();
  310. var toGroup = movements.AsList();
  311. while (toGroup.Count > 0)
  312. {
  313. var first = toGroup.First();
  314. var selected = toGroup.Where(x => x.IsEqualTo(first)).ToList();
  315. var holding = grouped.FirstOrDefault(x => x.IsEqualTo(first));
  316. if (holding == null)
  317. {
  318. holding = new StockHolding();
  319. holding.Location.ID = first.Location.ID;
  320. holding.Product.ID = first.Product.ID;
  321. holding.Style.ID = first.Style.ID;
  322. holding.Job.ID = first.Job.ID;
  323. holding.Dimensions.CopyFrom(first.Dimensions);
  324. }
  325. holding.Recalculate(selected);
  326. toGroup.RemoveAll(x => selected.Any(s => s.ID == x.ID));
  327. }
  328. return grouped;
  329. }
  330. public static bool IsEqualTo(this IStockHolding h1, IStockHolding h2)
  331. {
  332. return h1.Product.ID == h2.Product.ID
  333. && h1.Location.ID == h2.Location.ID
  334. && h1.Job.ID == h2.Job.ID
  335. && h1.Style.ID == h2.Style.ID
  336. && h1.Dimensions.Unit.ID == h2.Dimensions.Unit.ID
  337. && h1.Dimensions.Length.IsEffectivelyEqual(h2.Dimensions.Length)
  338. && h1.Dimensions.Width.IsEffectivelyEqual(h2.Dimensions.Width)
  339. && h1.Dimensions.Height.IsEffectivelyEqual(h2.Dimensions.Height)
  340. && h1.Dimensions.Quantity.IsEffectivelyEqual(h2.Dimensions.Quantity)
  341. && h1.Dimensions.Weight.IsEffectivelyEqual(h2.Dimensions.Weight);
  342. }
  343. public static void Recalculate(this StockHolding holding, IEnumerable<StockMovement> movements)
  344. {
  345. movements = movements.AsIList();
  346. var units = movements.Sum(x => x.Units);
  347. var cost = movements.Select(x => x.Units * x.Cost).Sum();
  348. var available = movements.Where(x => x.JobRequisitionItem.ID == Guid.Empty).Sum(x => x.Units);
  349. holding.Units = units;
  350. holding.Available = available;
  351. holding.Qty = movements.Sum(x => x.Units * x.Dimensions.Value);
  352. holding.Value = cost;
  353. holding.AverageValue = units.IsEffectivelyEqual(0.0F) ? 0.0d : cost / units;
  354. holding.Weight = holding.Qty * holding.Dimensions.Weight;
  355. }
  356. }
  357. }