Files
OpenNest/OpenNest.Data/MaterialLedger/LedgerCodeFilter.cs
T
ajandClaude Opus 5.5 8776eb4734 feat(ledger): keep MaterialLedger item codes from being double-booked
Auto Nest leaves out ledger item codes already on other open server nests,
and save and post re-check a nest's codes before going on.

- Server: GET /api/nests/stock-item-codes?exclude= lists the codes on
  non-deleted Draft, Quote and To Be Cut nests. Codes are stored in
  nest_stock_codes (schema v14) at every archive write; the upgrade reads
  each current archive once.
- Data: INestRepository.GetStockCodesInUseAsync, LedgerCodesInUse (5 s,
  failures become an unknown answer), the pure LedgerCodeFilter for ledger
  rows (reusable by release-time nesting) and LedgerCodeCheck for the
  re-check.
- Desktop: the Auto Nest ledger summary counts "(N on other nests)" or says
  other nests were not checked. Save, Save As and post open Ledger Stock
  Changed when codes are taken: proceed after a per-attempt
  acknowledgement, remove the codes (undoable), or cancel. A ledger that
  cannot be read skips the check with a status note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:43:08 -04:00

71 lines
3.0 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);
removed = 0;
var kept = new List<LedgerStockRow>();
foreach (var row in rows)
{
if (row == null)
continue;
var codes = row.ItemCodes.Where(code => !exclude.Contains(code)).ToList();
var dropped = row.ItemCodes.Count - codes.Count;
if (dropped == 0)
{
kept.Add(row);
continue;
}
removed += dropped;
if (codes.Count == 0)
continue;
var itemLocations = row.ItemLocations
.Where(pair => !exclude.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 };
}
}