namespace OpenNest.Data.MaterialLedger; /// A plate's item code that is no longer the nest's to use, and why. /// The plate's 1-based position in the nest. public sealed record LedgerCodeConflict(int PlateNumber, string ItemCode, string Reason); /// /// The save and post re-check of a nest's : finds codes the /// ledger no longer has free for this nest or that other open nests are coded with, and takes /// them off the plates when the programmer chooses to. Pure; the caller fetches the inputs. /// public static class LedgerCodeCheck { /// True when any plate has a coded sheet. public static bool HasCodes(IEnumerable plates) => (plates ?? throw new ArgumentNullException(nameof(plates))).Any(plate => plate.StockItemCodes.Count > 0); /// The nest's plates in order. /// The ledger's free stock for the nest's material, after every hold. /// /// The ledger's holds for this nest: an open one keeps its sheet for this nest, so the /// sheet counts as available here although the ledger no longer lists it as free. /// /// Other open nests' codes; an unknown answer checks the ledger alone. /// One conflict per plate and code, in plate order then the plate's code order. public static IReadOnlyList Find(IList plates, IEnumerable freeItems, IEnumerable? ownHolds, LedgerCodesInUse inUse) { ArgumentNullException.ThrowIfNull(plates); ArgumentNullException.ThrowIfNull(freeItems); ArgumentNullException.ThrowIfNull(inUse); var available = new Dictionary(StringComparer.Ordinal); foreach (var item in freeItems.Where(item => item != null && item.Quantity > 0)) available[item.ItemCode] = available.GetValueOrDefault(item.ItemCode) + item.Quantity; foreach (var hold in (ownHolds ?? Array.Empty()).Where(hold => hold != null && LedgerHoldStatuses.IsOpen(hold.Status))) available[hold.ItemCode] = available.GetValueOrDefault(hold.ItemCode) + 1; // A bundle's code repeats once per sheet across every plate it is used on. var needed = new Dictionary(StringComparer.Ordinal); foreach (var code in plates.SelectMany(plate => plate.StockItemCodes)) needed[code] = needed.GetValueOrDefault(code) + 1; var reasons = new Dictionary(StringComparer.Ordinal); foreach (var (code, count) in needed) { var parts = new List(); var free = available.GetValueOrDefault(code); if (free == 0) parts.Add("not free in MaterialLedger"); else if (free < count) parts.Add($"only {free} of the {count} sheets needed free in MaterialLedger"); var nests = inUse.NestsUsing(code); if (nests.Count > 0) parts.Add((nests.Count == 1 ? "on nest " : "on nests ") + string.Join(", ", nests)); if (parts.Count > 0) reasons[code] = Capitalize(string.Join("; ", parts)); } var conflicts = new List(); for (var i = 0; i < plates.Count; i++) { foreach (var code in plates[i].StockItemCodes.Distinct(StringComparer.Ordinal)) { if (reasons.TryGetValue(code, out var reason)) conflicts.Add(new LedgerCodeConflict(i + 1, code, reason)); } } return conflicts; } /// /// Takes every copy of off the plates, keeping the layout and the /// other codes in order; the sheets left uncoded are matched by size in ML. Returns how /// many sheets lost their code. /// public static int Remove(IEnumerable plates, IEnumerable codes) { ArgumentNullException.ThrowIfNull(plates); ArgumentNullException.ThrowIfNull(codes); var remove = codes.ToHashSet(StringComparer.Ordinal); var removed = 0; foreach (var plate in plates) { var kept = plate.StockItemCodes.Where(code => !remove.Contains(code)).ToList(); if (kept.Count == plate.StockItemCodes.Count) continue; removed += plate.StockItemCodes.Count - kept.Count; plate.StockItemCodes = kept; } return removed; } private static string Capitalize(string text) => text.Length == 0 ? text : char.ToUpperInvariant(text[0]) + text[1..]; }