Files
OpenNest/OpenNest.Data/MaterialLedger/LedgerCodeFilter.cs
T
aj c5b6e680cb fix(release): keep ledger codes in the undo step and share bundles per sheet
Review fixes for ledger-first Make Nests:

- The release renest (offer, staged run, codes, note) moves into
  OpenNest.Desktop.ReleaseRenest, shared by the headless panel, the
  live main window and the tests. Codes are given inside each commit, so
  the live tab's Auto Nest undo step records them; before, undo and redo
  dropped every ledger code.
- A bundle repeats one code per sheet: a later nest of the run lost the
  whole bundle once an earlier nest took one sheet. Taken sheets are now
  counted per code (LedgerCodeFilter.ExcludeCopies).
- A stage whose layout is not kept lists every copy it asked for as not
  placed (the proposal cannot be kept), and a rejected ledger layout
  says "MaterialLedger not used: Auto Nest could not keep a layout on
  its stock" instead of a bare "No MaterialLedger stock used".
- "no free stock" is said only when the ledger had none; otherwise the
  note counts what was left out (on other nests, taken by earlier nests
  of this release, too small, unknown shape).
- The ledger is not read for a nest with nothing to nest, the live read
  runs under the tab's stop token, a reason's own full stop is not
  doubled, and the sheet count is the codes given.
2026-11-03 17:53:26 -05:00

100 lines
4.3 KiB
C#

namespace OpenNest.Data.MaterialLedger;
/// <summary>
/// Takes item codes already on other open nests out of ledger stock rows, so two nests are
/// not planned on the same sheet before ML holds exist for either. Pure: rows are rebuilt,
/// never changed.
/// </summary>
public static class LedgerCodeFilter
{
/// <summary>
/// <paramref name="rows"/> without <paramref name="codesInUse"/>: every copy of a code
/// leaves the row's <see cref="LedgerStockRow.ItemCodes"/> and
/// <see cref="LedgerStockRow.ItemLocations"/>, its quantity drops by the copies removed, and
/// a row left with none is dropped. Row ids and order are kept. <paramref name="removed"/>
/// counts the sheets and drops taken out.
/// </summary>
public static IReadOnlyList<LedgerStockRow> Exclude(IEnumerable<LedgerStockRow> rows,
IEnumerable<string> codesInUse, out int removed)
{
ArgumentNullException.ThrowIfNull(rows);
ArgumentNullException.ThrowIfNull(codesInUse);
var exclude = codesInUse as IReadOnlySet<string> ?? codesInUse.ToHashSet(StringComparer.Ordinal);
return Rebuild(rows, exclude.Contains, out removed);
}
/// <summary>
/// <paramref name="rows"/> with only <paramref name="copies"/> of each code taken out, in
/// row and ledger order: a bundle whose one code repeats per sheet loses as many sheets
/// as were taken, not the whole bundle. Otherwise as
/// <see cref="Exclude(IEnumerable{LedgerStockRow}, IEnumerable{string}, out int)"/>.
/// </summary>
public static IReadOnlyList<LedgerStockRow> ExcludeCopies(IEnumerable<LedgerStockRow> rows,
IReadOnlyDictionary<string, int> copies, out int removed)
{
ArgumentNullException.ThrowIfNull(rows);
ArgumentNullException.ThrowIfNull(copies);
var left = new Dictionary<string, int>(copies, StringComparer.Ordinal);
return Rebuild(rows, code =>
{
if (!left.TryGetValue(code, out var count) || count <= 0)
return false;
left[code] = count - 1;
return true;
}, out removed);
}
// takeOut is asked once per copy, in row order and the ledger order within a row.
private static IReadOnlyList<LedgerStockRow> Rebuild(IEnumerable<LedgerStockRow> rows,
Func<string, bool> takeOut, out int removed)
{
removed = 0;
var kept = new List<LedgerStockRow>();
foreach (var row in rows)
{
if (row == null)
continue;
var codes = row.ItemCodes.Where(code => !takeOut(code)).ToList();
var dropped = row.ItemCodes.Count - codes.Count;
if (dropped == 0)
{
kept.Add(row);
continue;
}
removed += dropped;
if (codes.Count == 0)
continue;
var left = codes.ToHashSet(StringComparer.Ordinal);
var itemLocations = row.ItemLocations
.Where(pair => left.Contains(pair.Key) || !row.ItemCodes.Contains(pair.Key))
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
// A location stays listed while an item left in the row is there, or when the row
// never said which item it held.
var locations = row.Locations
.Where(location => itemLocations.ContainsValue(location)
|| !row.ItemLocations.Values.Contains(location, StringComparer.Ordinal))
.ToList();
kept.Add(row with
{
Quantity = codes.Count,
ItemCodes = codes,
Locations = locations,
ItemLocations = itemLocations,
});
}
return kept;
}
/// <summary>
/// <paramref name="mapping"/> with <paramref name="codesInUse"/> taken out of its rows and
/// the sheets removed counted in <see cref="LedgerStockMapping.OnOtherNests"/>.
/// </summary>
public static LedgerStockMapping Exclude(LedgerStockMapping mapping, IEnumerable<string> codesInUse)
{
ArgumentNullException.ThrowIfNull(mapping);
var rows = Exclude(mapping.Rows, codesInUse, out var removed);
return mapping with { Rows = rows, OnOtherNests = mapping.OnOtherNests + removed };
}
}