StockForecastOrderingGrid.cs 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.DynamicGrid;
  5. using InABox.Wpf;
  6. using InABox.WPF;
  7. using NPOI.SS.Formula.Functions;
  8. using Syncfusion.Data;
  9. using Syncfusion.Data.Extensions;
  10. using Syncfusion.UI.Xaml.Grid;
  11. using System;
  12. using System.Collections;
  13. using System.Collections.Generic;
  14. using System.ComponentModel;
  15. using System.Data;
  16. using System.Linq;
  17. using System.Text;
  18. using System.Threading.Tasks;
  19. using System.Windows;
  20. using System.Windows.Controls;
  21. using System.Windows.Media;
  22. using System.Windows.Media.Imaging;
  23. using Columns = InABox.Core.Columns;
  24. using PRSDesktop.Panels.StockForecast.OrderScreen;
  25. using PRSDimensionUtils;
  26. using Syncfusion.Windows.Shared;
  27. namespace PRSDesktop;
  28. public class StockForecastBreakupKey(Guid jobID, Guid requiID)
  29. {
  30. public Guid JobID { get; set; } = jobID;
  31. public Guid RequiID { get; set; } = requiID;
  32. public override bool Equals(object? obj)
  33. {
  34. return obj is StockForecastBreakupKey key && key.JobID == JobID && key.RequiID == RequiID;
  35. }
  36. public override int GetHashCode()
  37. {
  38. return HashCode.Combine(JobID, RequiID);
  39. }
  40. }
  41. public class StockForecastOrderData(ProductLink product, ProductStyleLink style, StockDimensions dimensions)
  42. {
  43. public ProductLink Product { get; set; } = product;
  44. public ProductStyleLink Style { get; set; } = style;
  45. public StockDimensions Dimensions { get; set; } = dimensions;
  46. public double RequiredQuantity { get; set; }
  47. public class QuantityBreakup(Guid jobID, Guid requiID, string description, double qty)
  48. {
  49. // This may be blank, in which case this refers to General Stock.
  50. public Guid JobID { get; set; } = jobID;
  51. public Guid JobRequiItemID { get; set; } = requiID;
  52. public string Description { get; set; } = description;
  53. public double Quantity { get; set; } = qty;
  54. }
  55. private List<QuantityBreakup> RequiredQuantities { get; set; } = [];
  56. public List<QuantityBreakup> GetRequiredQuantities() => RequiredQuantities;
  57. public void SetRequiredQuantity(Guid jobID, Guid requiID, string description, double qty)
  58. {
  59. RequiredQuantities.Add(new(jobID, requiID, description,qty));
  60. }
  61. }
  62. public enum StockForecastOrderingType
  63. {
  64. StockOrder,
  65. Breakup
  66. }
  67. public class StockForecastOrderingItemQuantity
  68. {
  69. public event Action? Changed;
  70. private double orderTotal;
  71. public double OrderTotal
  72. {
  73. get => orderTotal;
  74. set
  75. {
  76. orderTotal = value;
  77. Changed?.Invoke();
  78. }
  79. }
  80. public Dictionary<StockForecastBreakupKey, double> Breakups { get; init; } = [];
  81. private SupplierProduct? _supplierProduct;
  82. /// <summary>
  83. /// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column.
  84. /// </summary>
  85. public SupplierProduct? SupplierProduct
  86. {
  87. get => _supplierProduct;
  88. set
  89. {
  90. _supplierProduct = value;
  91. Changed?.Invoke();
  92. }
  93. }
  94. public void DoChanged()
  95. {
  96. Changed?.Invoke();
  97. }
  98. public double JobTotal => Breakups.Values.Sum();
  99. public double GetTotal(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  100. ? OrderTotal
  101. : JobTotal;
  102. }
  103. public class StockOrderingItem : BaseObject
  104. {
  105. [EditorSequence(1)]
  106. public ProductLink Product { get; set; }
  107. [EditorSequence(2)]
  108. public ProductStyleLink Style { get; set; }
  109. [EditorSequence(3)]
  110. public StockDimensions Dimensions { get; set; }
  111. [EditorSequence(5)]
  112. [DoubleEditor]
  113. public double RequiredQuantity { get; set; }
  114. [EditorSequence(6)]
  115. [EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
  116. public SupplierProductOrderStrategy OrderStrategy { get; set; }
  117. private Dictionary<StockForecastBreakupKey, double> JobRequiredQuantities { get; set; } = new()
  118. {
  119. { new(Guid.Empty, Guid.Empty), 0.0 }
  120. };
  121. public Dictionary<StockForecastBreakupKey, double> GetJobRequiredQuantities()
  122. {
  123. return JobRequiredQuantities;
  124. }
  125. public void SetJobRequiredQuantity(Guid jobID, Guid requiID, double requiredQuantity)
  126. {
  127. JobRequiredQuantities[new(jobID, requiID)] = requiredQuantity;
  128. }
  129. public bool CustomStrategy { get; set; } = false;
  130. public StockForecastOrderingItemQuantity[] Quantities = [];
  131. public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i];
  132. public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  133. ? Quantities.Sum(x => x.OrderTotal)
  134. : Quantities.Sum(x => x.JobTotal);
  135. public double GetRequiredQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  136. ? RequiredQuantity
  137. : JobRequiredQuantities.Values.Sum();
  138. public void SetQuantities(StockForecastOrderingItemQuantity[] quantities)
  139. {
  140. Quantities = quantities;
  141. }
  142. }
  143. public class StockForecastOrderingResult(
  144. SupplierLink supplier,
  145. List<StockForecastOrderData.QuantityBreakup> breakups,
  146. StockOrderingItem item,
  147. double quantity,
  148. SupplierProduct supplierProduct)
  149. {
  150. public SupplierLink Supplier { get; set; } = supplier;
  151. public StockOrderingItem Item { get; set; } = item;
  152. public SupplierProduct SupplierProduct { get; set; } = supplierProduct;
  153. public double Quantity { get; set; } = quantity;
  154. public List<StockForecastOrderData.QuantityBreakup> Breakups { get; set; } = breakups;
  155. }
  156. public enum StockForecastOrderingStrategy
  157. {
  158. PerProduct,
  159. Exact,
  160. RoundUp,
  161. LowestUnitPrice,
  162. LowestOverallPrice,
  163. LowestOverstock
  164. }
  165. public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>, ISpecificGrid
  166. {
  167. #region Internal Data + Caches
  168. private List<SupplierProduct> SupplierProducts = [];
  169. private SupplierLink[] Suppliers = [];
  170. private DynamicActionColumn[] SupplierProductColumns = [];
  171. private DynamicActionColumn[] QuantityColumns = [];
  172. private DynamicActionColumn[] CostColumns = [];
  173. private readonly Dictionary<Guid, Job> JobDetails = [];
  174. private readonly Dictionary<Guid, JobRequisitionItem> JobRequiDetails = [];
  175. #endregion
  176. private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
  177. #region Public Properties
  178. public IList<StockForecastOrderData> OrderData { get; set; }
  179. private StockForecastOrderingType _orderType = StockForecastOrderingType.Breakup;
  180. public StockForecastOrderingType OrderType
  181. {
  182. get => _orderType;
  183. set
  184. {
  185. if(_orderType != value)
  186. {
  187. _orderType = value;
  188. if (OrderData != null)
  189. {
  190. CalculateQuantities(true);
  191. UIComponent.UpdateOrderType(OrderType);
  192. foreach(var control in QuantityControls)
  193. {
  194. control.UpdateControl(OrderType);
  195. }
  196. Refresh(true, true);
  197. }
  198. }
  199. }
  200. }
  201. private StockForecastOrderingStrategy orderStrategy;
  202. public StockForecastOrderingStrategy OrderStrategy
  203. {
  204. get => orderStrategy;
  205. set
  206. {
  207. orderStrategy = value;
  208. if (OrderData != null)
  209. {
  210. foreach (var item in Items)
  211. {
  212. item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(value, item.Product.OrderStrategy);
  213. item.CustomStrategy = false;
  214. }
  215. CalculateQuantities(false);
  216. Refresh(false, true);
  217. }
  218. }
  219. }
  220. public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType));
  221. public IEnumerable<StockForecastOrderingResult> Results
  222. {
  223. get
  224. {
  225. for(int i = 0; i < Suppliers.Length; ++i)
  226. {
  227. var supplier = Suppliers[i];
  228. foreach(var item in Items)
  229. {
  230. var qty = item.GetQuantity(i);
  231. if (qty.SupplierProduct is null)
  232. {
  233. continue;
  234. }
  235. if(OrderType == StockForecastOrderingType.StockOrder && qty.OrderTotal > 0)
  236. {
  237. yield return new(supplier, new(), item, qty.OrderTotal, qty.SupplierProduct);
  238. }
  239. else if(qty.OrderTotal > 0)
  240. {
  241. var breakups = new List<StockForecastOrderData.QuantityBreakup>();
  242. foreach(var (key, q) in qty.Breakups)
  243. {
  244. // Check JobID because we are to skip the empty job (this is just the difference between all the allocations and the quantity on the PO).
  245. if(q > 0 && key.JobID != Guid.Empty)
  246. {
  247. breakups.Add(new(key.JobID, key.RequiID, "", q));
  248. }
  249. }
  250. yield return new(supplier, breakups, item, qty.OrderTotal, qty.SupplierProduct);
  251. }
  252. }
  253. }
  254. }
  255. }
  256. #endregion
  257. public StockForecastOrderingGrid()
  258. {
  259. HiddenColumns.Add(x => x.Product.Image.ID);
  260. }
  261. private static SupplierProductOrderStrategy CastOrderStrategyToProductOrderStrategy(StockForecastOrderingStrategy strategy, SupplierProductOrderStrategy defaultValue)
  262. {
  263. return strategy switch
  264. {
  265. StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact,
  266. StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy
  267. .LowestOverallPrice,
  268. StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice,
  269. StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock,
  270. StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp,
  271. StockForecastOrderingStrategy.PerProduct or _ => defaultValue
  272. };
  273. }
  274. #region UI Component
  275. private Component? _uiComponent;
  276. private Component UIComponent
  277. {
  278. get
  279. {
  280. _uiComponent ??= new Component(this);
  281. return _uiComponent;
  282. }
  283. }
  284. protected override IDynamicGridUIComponent<StockOrderingItem> CreateUIComponent()
  285. {
  286. return UIComponent;
  287. }
  288. private class Component : DynamicGridGridUIComponent<StockOrderingItem>
  289. {
  290. private StockForecastOrderingGrid Grid;
  291. public Component(StockForecastOrderingGrid grid)
  292. {
  293. Parent = grid;
  294. Grid = grid;
  295. UpdateOrderType(grid.OrderType);
  296. }
  297. public void UpdateOrderType(StockForecastOrderingType type)
  298. {
  299. DataGrid.FrozenColumnCount = 8;
  300. }
  301. protected override Brush? GetCellSelectionBackgroundBrush()
  302. {
  303. return null;
  304. }
  305. protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
  306. {
  307. var item = Grid.LoadItem(row);
  308. if(column is DynamicActionColumn ac)
  309. {
  310. var qIdx = Grid.QuantityColumns.IndexOf(ac);
  311. var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
  312. if(idx != -1)
  313. {
  314. var supplierProduct = item.GetQuantity(idx).SupplierProduct;
  315. return supplierProduct is null
  316. ? Brushes.Gainsboro
  317. : Brushes.WhiteSmoke;
  318. }
  319. }
  320. return base.GetCellBackground(row, column);
  321. }
  322. }
  323. #endregion
  324. private bool _observing = true;
  325. private void SetObserving(bool observing)
  326. {
  327. _observing = observing;
  328. }
  329. protected override void Changed()
  330. {
  331. if (_observing)
  332. {
  333. base.Changed();
  334. }
  335. }
  336. protected override void DoReconfigure(DynamicGridOptions options)
  337. {
  338. options.Clear();
  339. options.FilterRows = true;
  340. }
  341. private bool _loadedData = false;
  342. private void LoadData()
  343. {
  344. var supplierProductColumns = Columns.None<SupplierProduct>().Add(x => x.ID)
  345. .Add(x => x.SupplierLink.ID)
  346. .Add(x => x.Product.ID)
  347. .Add(x => x.Style.ID)
  348. .Add(x => x.Style.Code)
  349. .Add(x => x.Job.ID)
  350. .Add(x => x.Job.JobNumber)
  351. .Add(x => x.ForeignCurrencyPrice)
  352. .Add(x => x.CostPrice)
  353. .AddDimensionsColumns(x => x.Dimensions)
  354. .Add(x => x.SupplierLink.Code);
  355. SupplierProducts = Client.Query(
  356. new Filter<SupplierProduct>(x => x.Product.ID).InList(OrderData.Select(x => x.Product.ID).ToArray())
  357. .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
  358. supplierProductColumns,
  359. new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
  360. .ToList<SupplierProduct>();
  361. Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
  362. CalculateQuantities(true);
  363. _loadedData = true;
  364. }
  365. private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx)
  366. {
  367. var qty = new StockForecastOrderingItemQuantity();
  368. qty.Changed += () =>
  369. {
  370. if (!_observing) return;
  371. var row = Data.Rows[itemIdx];
  372. InvalidateRow(row);
  373. DoChanged();
  374. };
  375. return qty;
  376. }
  377. private SupplierProduct? CalculateSupplierProduct(StockOrderingItem item, int supplierIdx)
  378. {
  379. var supplierProducts = string.IsNullOrWhiteSpace(item.Dimensions.Unit.Conversion)
  380. ? SupplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions))
  381. : SupplierProducts;
  382. var supplierProduct = SelectSupplierProduct(supplierProducts.Where(x => x.Product.ID == item.Product.ID && x.Style.ID == item.Style.ID && x.SupplierLink.ID == Suppliers[supplierIdx].ID), item);
  383. var qty = item.GetQuantity(supplierIdx);
  384. qty.SupplierProduct = supplierProduct;
  385. qty.OrderTotal = 0;
  386. qty.Breakups.Clear();
  387. foreach(var id in item.GetJobRequiredQuantities().Keys)
  388. {
  389. qty.Breakups[id] = 0;
  390. }
  391. return supplierProduct;
  392. }
  393. private void SetSupplierProduct(StockOrderingItem item, SupplierProduct supplierProduct)
  394. {
  395. var supplierIdx = Suppliers.WithIndex()
  396. .FirstOrDefault(x => x.Value.ID == supplierProduct.SupplierLink.ID, new KeyValuePair<int, SupplierLink>(-1, null)).Key;
  397. if(supplierIdx != -1)
  398. {
  399. var qty = item.GetQuantity(supplierIdx);
  400. if(OrderType == StockForecastOrderingType.Breakup)
  401. {
  402. qty.Breakups.Clear();
  403. foreach(var (id, q) in item.GetJobRequiredQuantities())
  404. {
  405. qty.Breakups[id] = q;
  406. }
  407. }
  408. qty.OrderTotal = GetRequiredQuantity(item, supplierProduct);
  409. }
  410. }
  411. private void CalculateSupplierProduct(StockOrderingItem item)
  412. {
  413. var selectedSupplierProducts = new List<SupplierProduct>();
  414. for(int i = 0; i < Suppliers.Length; ++i)
  415. {
  416. var supplierProduct = CalculateSupplierProduct(item, i);
  417. if(supplierProduct is not null)
  418. {
  419. selectedSupplierProducts.Add(supplierProduct);
  420. }
  421. }
  422. var selectedSupplierProduct = SelectSupplierProduct(selectedSupplierProducts, item);
  423. if(selectedSupplierProduct is not null)
  424. {
  425. SetSupplierProduct(item, selectedSupplierProduct);
  426. }
  427. }
  428. private void CalculateQuantities(bool recreateItems)
  429. {
  430. SetObserving(false);
  431. if (recreateItems)
  432. {
  433. Items.Clear();
  434. foreach(var dataItem in OrderData)
  435. {
  436. var item = new StockOrderingItem();
  437. item.Product.CopyFrom(dataItem.Product);
  438. item.Style.CopyFrom(dataItem.Style);
  439. item.Dimensions.CopyFrom(dataItem.Dimensions);
  440. item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
  441. item.RequiredQuantity = dataItem.RequiredQuantity;
  442. foreach(var breakup in dataItem.GetRequiredQuantities())
  443. {
  444. item.SetJobRequiredQuantity(breakup.JobID, breakup.JobRequiItemID, breakup.Quantity);
  445. }
  446. Items.Add(item);
  447. }
  448. }
  449. foreach(var (itemIdx, item) in Items.WithIndex())
  450. {
  451. var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
  452. for(int i = 0; i < Suppliers.Length; ++i)
  453. {
  454. quantities[i] = CreateQuantity(itemIdx);
  455. }
  456. item.SetQuantities(quantities);
  457. }
  458. foreach(var item in Items)
  459. {
  460. CalculateSupplierProduct(item);
  461. }
  462. SetObserving(true);
  463. DoChanged();
  464. }
  465. #region Order Strategy
  466. private double CalculateSupplierProductRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
  467. {
  468. var supplierIdx = Suppliers.WithIndex().FirstOrDefault(x => x.Value.ID == supplierProduct.SupplierLink.ID).Key;
  469. var qty = item.GetQuantity(supplierIdx);
  470. var req = OrderType == StockForecastOrderingType.StockOrder ? item.RequiredQuantity : qty.Breakups.Sum(x => x.Value);
  471. var d = new StockDimensions();
  472. d.CopyFrom(supplierProduct.Dimensions);
  473. var result = DimensionUtils.ConvertDimensions(d, 1.0, (f,c) => Client.Query(f,c));
  474. req = Math.Ceiling(req / result);
  475. return req;
  476. }
  477. private SupplierProduct? SelectSupplierProduct(IEnumerable<SupplierProduct> supplierProducts, StockOrderingItem item)
  478. {
  479. double DimensionsRatio(SupplierProduct x)
  480. {
  481. return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value);
  482. }
  483. switch (item.OrderStrategy)
  484. {
  485. case SupplierProductOrderStrategy.Exact:
  486. case SupplierProductOrderStrategy.RoundUp:
  487. return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions))
  488. .MinBy(x => x.CostPrice);
  489. case SupplierProductOrderStrategy.LowestOverallPrice:
  490. return supplierProducts.MinBy(x => x.CostPrice * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x)));
  491. case SupplierProductOrderStrategy.LowestUnitPrice:
  492. return supplierProducts.MinBy(x => x.CostPrice * DimensionsRatio(x));
  493. case SupplierProductOrderStrategy.LowestOverstock:
  494. return supplierProducts.MinBy(x => x.Dimensions.Value * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x)));
  495. default:
  496. return null;
  497. }
  498. }
  499. private double GetRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
  500. {
  501. var requiredQuantity = CalculateSupplierProductRequiredQuantity(item, supplierProduct);
  502. double DimensionsRatio(SupplierProduct x)
  503. {
  504. return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value);
  505. }
  506. var _strategy = item.CustomStrategy
  507. ? SupplierProductOrderStrategy.LowestOverstock
  508. : item.OrderStrategy;
  509. switch (_strategy)
  510. {
  511. case SupplierProductOrderStrategy.Exact:
  512. return requiredQuantity;
  513. case SupplierProductOrderStrategy.RoundUp:
  514. return Math.Ceiling(requiredQuantity);
  515. case SupplierProductOrderStrategy.LowestOverallPrice:
  516. case SupplierProductOrderStrategy.LowestUnitPrice:
  517. case SupplierProductOrderStrategy.LowestOverstock:
  518. return Math.Ceiling(requiredQuantity * DimensionsRatio(supplierProduct));
  519. default:
  520. return 0.0;
  521. }
  522. }
  523. #endregion
  524. private bool _loadedColumns = false;
  525. protected override DynamicGridColumns LoadColumns()
  526. {
  527. if (!_loadedData)
  528. {
  529. LoadData();
  530. }
  531. var columns = new DynamicGridColumns();
  532. columns.Add<StockOrderingItem, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
  533. columns.Add<StockOrderingItem, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
  534. columns.Add<StockOrderingItem, string>(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter);
  535. columns.Add<StockOrderingItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter);
  536. columns.Add<StockOrderingItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
  537. if (!_loadedColumns)
  538. {
  539. ActionColumns.Clear();
  540. ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
  541. ActionColumns.Add(new DynamicImagePreviewColumn<StockOrderingItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
  542. ActionColumns.Add(new DynamicTemplateColumn(row =>
  543. {
  544. var item = LoadItem(row);
  545. var box = new ComboBox();
  546. box.ItemsSource = Enum.GetValues<SupplierProductOrderStrategy>()
  547. .Select(x => new KeyValuePair<SupplierProductOrderStrategy, string>(x, CoreUtils.Neatify(x.ToString())));
  548. box.DisplayMemberPath = "Value";
  549. box.SelectedValuePath = "Key";
  550. box.SelectedValue = item.CustomStrategy ? null : item.OrderStrategy;
  551. box.SelectionChanged += (o, e) =>
  552. {
  553. if (box.SelectedValue is not SupplierProductOrderStrategy strategy) return;
  554. item.OrderStrategy = strategy;
  555. item.CustomStrategy = false;
  556. CalculateSupplierProduct(item);
  557. InvalidateRow(row);
  558. };
  559. box.Margin = new Thickness(2);
  560. box.VerticalContentAlignment = VerticalAlignment.Center;
  561. return box;
  562. })
  563. {
  564. HeaderText = "Order Strategy.",
  565. Width = 140
  566. });
  567. SupplierProductColumns = new DynamicActionColumn[Suppliers.Length];
  568. QuantityColumns = new DynamicActionColumn[Suppliers.Length];
  569. CostColumns = new DynamicActionColumn[Suppliers.Length];
  570. QuantityControls.Clear();
  571. for(int i = 0; i < Suppliers.Length; ++i)
  572. {
  573. InitialiseSupplierColumn(i);
  574. }
  575. ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
  576. _loadedColumns = true;
  577. }
  578. return columns;
  579. }
  580. private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
  581. {
  582. grid.OnCustomiseEditor += (sender, items, column, editor) =>
  583. {
  584. if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
  585. || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
  586. || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName))
  587. {
  588. editor.Editable = editor.Editable.Combine(Editable.Disabled);
  589. }
  590. };
  591. }
  592. private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
  593. {
  594. if (row is null) return;
  595. column.AddItem("New Supplier", null, row =>
  596. {
  597. if (row is null) return;
  598. var selection = new MultiSelectDialog<Supplier>(
  599. new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
  600. Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
  601. if (selection.ShowDialog() != true)
  602. {
  603. return;
  604. }
  605. var supplier = selection.Data().Rows.First().ToObject<Supplier>();
  606. var orderingItem = LoadItem(row);
  607. var supplierProduct = new SupplierProduct();
  608. supplierProduct.Product.CopyFrom(orderingItem.Product);
  609. supplierProduct.Style.CopyFrom(orderingItem.Style);
  610. supplierProduct.Dimensions.CopyFrom(orderingItem.Dimensions);
  611. supplierProduct.SupplierLink.CopyFrom(supplier);
  612. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  613. {
  614. SupplierProducts.Add(supplierProduct);
  615. var newSuppliers = new SupplierLink[Suppliers.Length + 1];
  616. var newIdx = Suppliers.Length;
  617. for (int i = 0; i < Suppliers.Length; i++)
  618. {
  619. newSuppliers[i] = Suppliers[i];
  620. }
  621. newSuppliers[newIdx] = supplierProduct.SupplierLink;
  622. foreach (var (itemIdx, item) in Items.WithIndex())
  623. {
  624. var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length];
  625. for (int i = 0; i < Suppliers.Length; ++i)
  626. {
  627. quantities[i] = item.GetQuantity(i);
  628. }
  629. var newQty = CreateQuantity(itemIdx);
  630. quantities[newIdx] = newQty;
  631. if(OrderType == StockForecastOrderingType.StockOrder)
  632. {
  633. newQty.OrderTotal = 0;
  634. }
  635. else
  636. {
  637. newQty.OrderTotal = 0;
  638. foreach(var id in item.GetJobRequiredQuantities().Keys)
  639. {
  640. newQty.Breakups[id] = 0;
  641. }
  642. }
  643. item.SetQuantities(quantities);
  644. }
  645. Suppliers = newSuppliers;
  646. foreach (var item in Items)
  647. {
  648. CalculateSupplierProduct(item, newIdx);
  649. }
  650. _loadedColumns = false;
  651. Refresh(true, true);
  652. }
  653. });
  654. }
  655. private BitmapImage? Warning_Image(CoreRow? row)
  656. {
  657. if (row is null) return _warning;
  658. var item = LoadItem(row);
  659. if(item.GetTotalQuantity(OrderType) < item.RequiredQuantity)
  660. {
  661. return _warning;
  662. }
  663. else
  664. {
  665. return null;
  666. }
  667. }
  668. protected override void ConfigureColumnGroups()
  669. {
  670. for(int idx = 0; idx < Suppliers.Length; ++idx)
  671. {
  672. GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]);
  673. }
  674. }
  675. #region Job Data Cache
  676. private void LoadJobData(IEnumerable<Guid> ids)
  677. {
  678. var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray();
  679. if(neededIDs.Length > 0)
  680. {
  681. var details = Client.Query(
  682. new Filter<Job>(x => x.ID).InList(neededIDs),
  683. Columns.None<Job>().Add(x => x.ID)
  684. .Add(x => x.JobNumber)
  685. .Add(x => x.Name));
  686. foreach(var job in details.ToObjects<Job>())
  687. {
  688. JobDetails[job.ID] = job;
  689. }
  690. }
  691. }
  692. private void LoadJobRequiData(IEnumerable<Guid> ids)
  693. {
  694. var neededIDs = ids.Where(x => x != Guid.Empty && !JobRequiDetails.ContainsKey(x)).ToArray();
  695. if(neededIDs.Length > 0)
  696. {
  697. var details = Client.Query(
  698. new Filter<JobRequisitionItem>(x => x.ID).InList(neededIDs),
  699. Columns.None<JobRequisitionItem>().Add(x => x.ID)
  700. .Add(x => x.Requisition.Number)
  701. .Add(x => x.Requisition.Description));
  702. foreach(var requi in details.ToObjects<JobRequisitionItem>())
  703. {
  704. JobRequiDetails[requi.ID] = requi;
  705. }
  706. }
  707. }
  708. #endregion
  709. private class QuantityControl : ContentControl
  710. {
  711. private readonly StockOrderingItem Item;
  712. private readonly int SupplierIndex;
  713. private readonly StockForecastOrderingGrid Parent;
  714. public QuantityControl(StockForecastOrderingGrid parent, StockOrderingItem item, int supplierIndex, StockForecastOrderingType mode)
  715. {
  716. Parent = parent;
  717. Item = item;
  718. SupplierIndex = supplierIndex;
  719. UpdateControl(mode);
  720. }
  721. public void UpdateControl(StockForecastOrderingType mode)
  722. {
  723. // If no supplier product has been selected for this cell, we can't allow the user to select a quantity.
  724. var supplierProduct = Item.GetQuantity(SupplierIndex).SupplierProduct;
  725. if(supplierProduct is null)
  726. {
  727. Content = null;
  728. return;
  729. }
  730. if(mode == StockForecastOrderingType.StockOrder)
  731. {
  732. var editor = new DoubleTextBox
  733. {
  734. VerticalAlignment = VerticalAlignment.Stretch,
  735. HorizontalAlignment = HorizontalAlignment.Stretch,
  736. Background = new SolidColorBrush(Colors.LightYellow),
  737. BorderThickness = new Thickness(0.0),
  738. MinValue = 0.0,
  739. Value = Item.GetQuantity(SupplierIndex).OrderTotal
  740. };
  741. editor.ValueChanged += (o, e) =>
  742. {
  743. Item.GetQuantity(SupplierIndex).OrderTotal = editor.Value ?? default;
  744. };
  745. Content = editor;
  746. }
  747. else if(mode == StockForecastOrderingType.Breakup)
  748. {
  749. var grid = new Grid();
  750. grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
  751. grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
  752. var _breakups = Item.GetQuantity(SupplierIndex).Breakups.Sum(x => x.Value);
  753. var editor = new DoubleTextBox
  754. {
  755. VerticalAlignment = VerticalAlignment.Stretch,
  756. HorizontalAlignment = HorizontalAlignment.Stretch,
  757. VerticalContentAlignment = VerticalAlignment.Center,
  758. HorizontalContentAlignment = HorizontalAlignment.Center,
  759. Background = new SolidColorBrush(Colors.WhiteSmoke),
  760. BorderThickness = new Thickness(0.0),
  761. Value = Item.GetQuantity(SupplierIndex).OrderTotal,
  762. IsReadOnly = true,
  763. Focusable = false,
  764. };
  765. Grid.SetColumn(editor, 0);
  766. grid.Children.Add(editor);
  767. var btn = new Button
  768. {
  769. VerticalAlignment = VerticalAlignment.Stretch,
  770. VerticalContentAlignment = VerticalAlignment.Center,
  771. HorizontalAlignment = HorizontalAlignment.Stretch,
  772. Content = "..",
  773. Margin = new Thickness(1),
  774. Focusable = false
  775. };
  776. btn.SetValue(Grid.ColumnProperty, 1);
  777. btn.SetValue(Grid.RowProperty, 0);
  778. btn.Click += (o, e) =>
  779. {
  780. var qty = Item.GetQuantity(SupplierIndex);
  781. Parent.LoadJobData(qty.Breakups.Keys.Select(x => x.JobID));
  782. Parent.LoadJobRequiData(qty.Breakups.Keys.Select(x => x.RequiID));
  783. var items = qty.Breakups.Select(x =>
  784. {
  785. var item = new StockForecastOrderingJobItem
  786. {
  787. JobID = x.Key.JobID,
  788. JobRequiID = x.Key.RequiID,
  789. RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key),
  790. Quantity = x.Value
  791. };
  792. if(item.JobID == Guid.Empty)
  793. {
  794. item.Description = "General Stock";
  795. }
  796. else if(Parent.JobDetails.TryGetValue(item.JobID, out var job))
  797. {
  798. if(Parent.JobRequiDetails.TryGetValue(item.JobRequiID, out var requi))
  799. {
  800. item.Description = $"{job.JobNumber}: Requi #{requi.Requisition.Number} ({requi.Requisition.Description})";
  801. }
  802. else
  803. {
  804. item.Description = $"{job.JobNumber}: {job.Name}";
  805. }
  806. }
  807. return item;
  808. }).ToList();
  809. var genitem = items.FirstOrDefault(x =>
  810. x.JobID == Guid.Empty && x.JobRequiID == Guid.Empty);
  811. if (genitem == null)
  812. {
  813. genitem = new StockForecastOrderingJobItem
  814. {
  815. Description = "General Stock",
  816. JobID = Guid.Empty,
  817. JobRequiID = Guid.Empty
  818. };
  819. items.Insert(0, genitem);
  820. }
  821. else
  822. {
  823. items.Remove(genitem);
  824. items.Insert(0, genitem);
  825. }
  826. var window = new StockForecastOrderJobScreen();
  827. window.Items = items;
  828. if(window.ShowDialog() == true)
  829. {
  830. foreach(var item in items)
  831. {
  832. qty.Breakups[new(item.JobID, item.JobRequiID)] = item.Quantity;
  833. }
  834. qty.DoChanged();
  835. var req = Parent.GetRequiredQuantity(Item, supplierProduct);
  836. editor.Value = req;
  837. qty.OrderTotal = req;
  838. }
  839. };
  840. grid.Children.Add(btn);
  841. Content = grid;
  842. }
  843. }
  844. }
  845. private List<QuantityControl> QuantityControls = [];
  846. private void InitialiseSupplierColumn(int idx)
  847. {
  848. var contextMenuFunc = (CoreRow[]? rows) =>
  849. {
  850. var row = rows?.FirstOrDefault();
  851. if (row is null) return null;
  852. var item = LoadItem(row);
  853. var menu = new ContextMenu();
  854. menu.AddItem("Create Supplier Product", null, new Tuple<StockOrderingItem, int>(item, idx), CreateSupplierProduct_Click);
  855. return menu;
  856. };
  857. var qtyColumn = new Tuple<DynamicActionColumn, QuantityControl?>(null!, null);
  858. SupplierProductColumns[idx] = new DynamicTemplateColumn(row =>
  859. {
  860. var instance = LoadItem(row);
  861. var comboBox = new ComboBox();
  862. comboBox.Tag = idx;
  863. var supplierProducts = string.IsNullOrWhiteSpace(instance.Dimensions.Unit.Conversion)
  864. ? SupplierProducts.Where(x => x.Dimensions.Equals(instance.Dimensions))
  865. : SupplierProducts;
  866. var items = supplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID)
  867. .Select(x => new KeyValuePair<SupplierProduct?, string>(x, x.Job.ID == Guid.Empty ? x.Dimensions.UnitSize : $"Job {x.Job.JobNumber}: {x.Dimensions.UnitSize}"));
  868. if (items.Any())
  869. items = items.Prepend(new KeyValuePair<SupplierProduct?, string>(null, ""));
  870. comboBox.SelectedValuePath = "Key";
  871. comboBox.ItemsSource = items.ToArray();
  872. comboBox.DisplayMemberPath = "Value";
  873. var qty = instance.GetQuantity(idx);
  874. comboBox.Bind(ComboBox.SelectedValueProperty, qty, x => x.SupplierProduct);
  875. comboBox.SelectionChanged += (o, e) =>
  876. {
  877. var box = o as ComboBox;
  878. instance.CustomStrategy = true;
  879. var _item = LoadItem(row);
  880. var _product = ((o as ComboBox)?.SelectedValue as SupplierProduct ?? new SupplierProduct());
  881. qty.OrderTotal = GetRequiredQuantity(_item, _product);
  882. InvalidateRow(row);
  883. };
  884. comboBox.VerticalContentAlignment = VerticalAlignment.Center;
  885. comboBox.Margin = new Thickness(2);
  886. if(!items.Any())
  887. comboBox.IsEnabled = false;
  888. return comboBox;
  889. })
  890. {
  891. HeaderText = "U.O.M.",
  892. Width = 80
  893. };
  894. QuantityColumns[idx] = new DynamicTemplateColumn(row =>
  895. {
  896. var instance = LoadItem(row);
  897. var control = new QuantityControl(this, instance, idx, OrderType);
  898. QuantityControls.Add(control);
  899. return control;
  900. })
  901. {
  902. HeaderText = "Qty.",
  903. Width = 80,
  904. ContextMenu = contextMenuFunc
  905. };
  906. CostColumns[idx] = new DynamicTextColumn(row =>
  907. {
  908. if(row is null)
  909. {
  910. return "Cost";
  911. }
  912. var instance = LoadItem(row);
  913. var qty = instance.GetQuantity(idx);
  914. if(qty.SupplierProduct is not null)
  915. {
  916. return $"{qty.OrderTotal * qty.SupplierProduct.CostPrice:C2}";
  917. }
  918. else
  919. {
  920. return "";
  921. }
  922. })
  923. {
  924. HeaderText = "Cost",
  925. Width = 80,
  926. ContextMenu = contextMenuFunc,
  927. GetSummary = () =>
  928. {
  929. return new DynamicGridCustomSummary((rows) => Cost_Aggregate(idx, rows), "C2");
  930. }
  931. };
  932. ActionColumns.Add(SupplierProductColumns[idx]);
  933. ActionColumns.Add(QuantityColumns[idx]);
  934. ActionColumns.Add(CostColumns[idx]);
  935. }
  936. private double Cost_Aggregate(int supplierIdx, IEnumerable<CoreRow> rows)
  937. {
  938. return rows.Sum(row =>
  939. {
  940. var item = LoadItem(row);
  941. var qty = item.GetQuantity(supplierIdx);
  942. if(qty.SupplierProduct is not null)
  943. {
  944. return qty.OrderTotal * qty.SupplierProduct.CostPrice;
  945. }
  946. else
  947. {
  948. return 0;
  949. }
  950. });
  951. }
  952. private void CreateSupplierProduct_Click(Tuple<StockOrderingItem, int> tuple)
  953. {
  954. var (item, supplierIdx) = tuple;
  955. var supplierProduct = new SupplierProduct();
  956. supplierProduct.Product.CopyFrom(item.Product);
  957. supplierProduct.Style.CopyFrom(item.Style);
  958. supplierProduct.Dimensions.CopyFrom(item.Dimensions);
  959. supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]);
  960. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  961. {
  962. SupplierProducts.Add(supplierProduct);
  963. var qty = item.GetQuantity(supplierIdx);
  964. if(qty.SupplierProduct is null)
  965. {
  966. var hasSelected = item.Quantities.Any(x => x.SupplierProduct is not null);
  967. var sp = CalculateSupplierProduct(item, supplierIdx);
  968. if(!hasSelected && sp is not null)
  969. {
  970. SetSupplierProduct(item, sp);
  971. }
  972. }
  973. InvalidateGrid();
  974. }
  975. }
  976. }