Files
OpenNest/OpenNest.IO.Tests/StockItemCodesPersistenceTests.cs
T
ajandClaude Opus 5.5 15750949dd feat(nest): plates name the ledger item each sheet is cut from
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>
2026-10-25 17:40:44 -04:00

80 lines
2.7 KiB
C#

using System.Text.Json.Nodes;
using OpenNest.Geometry;
using OpenNest.Shapes;
using static OpenNest.IO.Tests.NestArchiveJson;
namespace OpenNest.IO.Tests;
/// <summary>
/// A plate's stock item codes survive the .nest round trip as an optional
/// <c>stockItemCodes</c> array; files without it load with no codes.
/// </summary>
public class StockItemCodesPersistenceTests
{
private static Nest NestWithPlate(int quantity, params string[] codes)
{
var nest = new Nest("coded") { Material = new Material("Mild Steel", "A36"), Thickness = 0.25 };
var drawing = new RectangleShape { Name = "SYN", Length = 4, Width = 2 }.GetDrawing();
nest.Drawings.Add(drawing);
var plate = new Plate(30, 52.5) { Quantity = quantity };
plate.Parts.Add(new Part(drawing, new Vector(1, 1)));
nest.Plates.Add(plate);
plate.StockItemCodes = codes;
return nest;
}
private static byte[] Write(Nest nest)
{
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest).Write(stream));
return stream.ToArray();
}
[Fact]
public void CodesRoundTripInSheetOrder()
{
var (reopened, _) = RoundTrip(Write(NestWithPlate(3, "SHEET-456", "SHEET-456")));
var plate = Assert.Single(reopened.Plates);
Assert.Equal(3, plate.Quantity);
Assert.Equal(["SHEET-456", "SHEET-456"], plate.StockItemCodes);
Assert.Equal("Mild Steel", reopened.Material.Name);
}
[Fact]
public void UncodedPlate_WritesNoKey()
{
var json = NestJson(Write(NestWithPlate(1)));
Assert.False(json["plates"]![0]!.AsObject().ContainsKey("stockItemCodes"));
}
[Fact]
public void OldFileWithoutTheKey_LoadsWithNoCodes()
{
var archive = Write(NestWithPlate(1, "DROP-1182"));
var json = NestJson(archive);
Assert.True(json["plates"]![0]!.AsObject().Remove("stockItemCodes"));
var (reopened, _) = RoundTrip(ReplaceNestJson(archive, json));
Assert.Empty(Assert.Single(reopened.Plates).StockItemCodes);
}
[Theory]
[InlineData("\"DROP-1\"", new string[0])]
[InlineData("[\"DROP-1\", 7, \"DROP-2\"]", new[] { "DROP-1", "DROP-2" })]
[InlineData("[\"DROP-1\", \"\", \"DROP-2\"]", new[] { "DROP-1" })]
[InlineData("[\"A\", \"B\", \"C\", \"D\"]", new[] { "A", "B", "C" })]
public void MalformedCodes_ReadLeniently(string value, string[] expected)
{
var archive = Write(NestWithPlate(3, "X"));
var json = NestJson(archive);
json["plates"]![0]!["stockItemCodes"] = JsonNode.Parse(value);
var (reopened, _) = RoundTrip(ReplaceNestJson(archive, json));
Assert.Equal(expected, Assert.Single(reopened.Plates).StockItemCodes);
}
}