mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 02:41:06 -04:00
Auto Nest on MaterialLedger stock now records which inventory item each
physical sheet uses, so ML can hold exactly those items instead of matching
by size.
- Core: Plate.StockItemCodes (entry i = sheet i of Quantity; empty = uncoded).
Core invalidates it: a resize clears it (turning the sheet keeps it), a lower
Quantity truncates it, a nest material/grade/thickness change clears every
plate's, ClearAll clears it and new plates never copy it.
- Desktop Auto Nest: after a kept proposal is committed, LedgerStockAssignment
deals each ledger row's codes to its plates in Nest.Plates order (bundles
repeat their code; clamp-removal variants use their base row). Rejected,
discarded or cancelled proposals never reach it. NestPipelineCommit.PairWithStock
pairs committed plates with their solved stock; the engine stays ledger-free.
- Plate list tooltip: "Stock: CODE (location)"; locations are display-only.
- nest.json: optional plate stockItemCodes, omitted when empty, read leniently.
- /api/nests/{id}/sheets: new plates[] { index, width, length, quantity,
stockItemCodes }; sheets[] unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.7 KiB
C#
85 lines
3.7 KiB
C#
namespace OpenNest.Data.MaterialLedger;
|
|
|
|
/// <summary>
|
|
/// Gives the plates Auto Nest committed on ledger stock their item codes. Runs only after the
|
|
/// proposal was validated, kept and committed, so a rejected or cancelled one never changes
|
|
/// codes. Codes are hand-off data for MaterialLedger holds, not inventory accounting.
|
|
/// </summary>
|
|
public static class LedgerStockAssignment
|
|
{
|
|
// ClampRemovalRecovery solves a sheet with clamps removed as "{stock id}::removed:{ids}".
|
|
private const string VariantSeparator = "::";
|
|
|
|
/// <param name="nestPlates">The nest's plates; codes are dealt out in this order.</param>
|
|
/// <param name="committed">Each plate the commit attached, with the engine stock id it was solved on.</param>
|
|
/// <param name="rows">The ledger rows offered to the run.</param>
|
|
/// <remarks>
|
|
/// Each row's codes are a queue: a plate takes the next <see cref="Plate.Quantity"/> codes of
|
|
/// its row (a bundle repeats its one code). A plate whose row runs out keeps the rest of its
|
|
/// sheets uncoded. A committed plate on hand-typed stock has no codes.
|
|
/// </remarks>
|
|
public static void Assign(IList<Plate> nestPlates, IEnumerable<(Plate Plate, string StockId)> committed,
|
|
IEnumerable<LedgerStockRow> rows)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(nestPlates);
|
|
ArgumentNullException.ThrowIfNull(committed);
|
|
ArgumentNullException.ThrowIfNull(rows);
|
|
var queues = new Dictionary<string, Queue<string>>(StringComparer.Ordinal);
|
|
foreach (var row in rows)
|
|
queues.TryAdd(row.Id, new Queue<string>(row.ItemCodes));
|
|
|
|
foreach (var (plate, stockId) in committed.OrderBy(c => IndexOf(nestPlates, c.Plate)))
|
|
{
|
|
var codes = new List<string>();
|
|
if (stockId != null && queues.TryGetValue(BaseStockId(stockId), out var queue))
|
|
{
|
|
while (codes.Count < plate.Quantity && queue.Count > 0)
|
|
codes.Add(queue.Dequeue());
|
|
}
|
|
plate.StockItemCodes = codes;
|
|
}
|
|
}
|
|
|
|
/// <summary>The offered stock id a solved sheet came from, without a clamp-removal suffix.</summary>
|
|
public static string BaseStockId(string stockId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(stockId);
|
|
var separator = stockId.IndexOf(VariantSeparator, StringComparison.Ordinal);
|
|
return separator < 0 ? stockId : stockId[..separator];
|
|
}
|
|
|
|
/// <summary>
|
|
/// "Stock: DROP-1182 (B3)" for display, with runs of one code counted ("SHEET-456 (A1) x3")
|
|
/// and uncoded sheets noted; null when no sheet is coded. Locations are display-only.
|
|
/// </summary>
|
|
public static string? Describe(Plate plate, IReadOnlyDictionary<string, string>? locations)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(plate);
|
|
var codes = plate.StockItemCodes;
|
|
if (codes.Count == 0)
|
|
return null;
|
|
var parts = new List<string>();
|
|
for (var i = 0; i < codes.Count;)
|
|
{
|
|
var run = 1;
|
|
while (i + run < codes.Count && codes[i + run] == codes[i])
|
|
run++;
|
|
var text = locations != null && locations.TryGetValue(codes[i], out var location)
|
|
? $"{codes[i]} ({location})"
|
|
: codes[i];
|
|
parts.Add(run > 1 ? $"{text} x{run}" : text);
|
|
i += run;
|
|
}
|
|
var uncoded = plate.Quantity - codes.Count;
|
|
if (uncoded > 0)
|
|
parts.Add($"{uncoded} uncoded");
|
|
return "Stock: " + string.Join(", ", parts);
|
|
}
|
|
|
|
private static int IndexOf(IList<Plate> plates, Plate plate)
|
|
{
|
|
var index = plates.IndexOf(plate);
|
|
return index < 0 ? int.MaxValue : index;
|
|
}
|
|
}
|