mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 03:31:34 -05:00
Auto Nest's Stock Options gets a Load from ledger button that reads the nest
material's free sheets and drops from MaterialLedger's API-key-protected
GET /api/stock/opennest. Ledger rows are request-local: the engine sees them as
ordinary NestPlateStock rows with ledger:{n} ids and finite quantities, and they
are never written to Nest.PlateOptions.
- OpenNest.Data/MaterialLedger: settings file, read-only client (X-Api-Key,
5 s timeout, closed LedgerStockResult set) and LedgerStockMapper (bundles and
sheets one row each, same-size drops grouped with ordered item codes, unknown
shapes and too-small items skipped, inch/mm conversion).
- Options: MaterialLedger URL and API key (DPAPI, CurrentUser) with Test.
- Auto Nest grid: Use, Qty and Source columns for every row; PlateOption gains
request-local Quantity and Id, which NestStockBuilder passes to the engine.
Ledger rows score by area; hand-typed costs stay saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.0 KiB
C#
129 lines
5.0 KiB
C#
using OpenNest.Data.MaterialLedger;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.Data;
|
|
|
|
public class LedgerStockMapperTests
|
|
{
|
|
[Fact]
|
|
public void BundlesAndSheetsAreOneRowEachAndDropsOfOneSizeShareARow()
|
|
{
|
|
var mapping = LedgerStockMapper.Map(new[]
|
|
{
|
|
Item("B-1", LedgerItemKinds.FullSheetBundle, 48, 96, quantity: 3),
|
|
Item("S-1", LedgerItemKinds.FullSheet, 48, 96, quantity: 1),
|
|
Item("D-2", LedgerItemKinds.Drop, 24, 36, location: "R2"),
|
|
Item("D-9", LedgerItemKinds.Drop, 30, 40, location: "R1"),
|
|
Item("D-1", LedgerItemKinds.Drop, 36.0004m, 24, location: "R1"),
|
|
Item("D-3", LedgerItemKinds.Drop, 24, 36, location: "R1"),
|
|
}, Units.Inches);
|
|
|
|
Assert.Equal(4, mapping.Rows.Count);
|
|
var bundle = mapping.Rows[0];
|
|
Assert.Equal("ledger:1", bundle.Id);
|
|
Assert.False(bundle.IsDrop);
|
|
Assert.Equal(3, bundle.Quantity);
|
|
Assert.Equal(new[] { "B-1", "B-1", "B-1" }, bundle.ItemCodes);
|
|
Assert.Equal(new Size(48, 96), bundle.Size);
|
|
Assert.Equal(1, mapping.Rows[1].Quantity);
|
|
|
|
// Width is the short side; drops matching after ML's 3-place rounding group, ordered by location then code.
|
|
var drops = mapping.Rows[2];
|
|
Assert.True(drops.IsDrop);
|
|
Assert.Equal(new Size(24, 36), drops.Size);
|
|
Assert.Equal(3, drops.Quantity);
|
|
Assert.Equal(new[] { "D-1", "D-3", "D-2" }, drops.ItemCodes);
|
|
Assert.Equal(new[] { "R1", "R2" }, drops.Locations);
|
|
Assert.Equal(new[] { "D-9" }, mapping.Rows[3].ItemCodes);
|
|
|
|
Assert.Equal(2, mapping.SheetRows);
|
|
Assert.Equal(4, mapping.Drops);
|
|
Assert.Equal("Ledger: 2 sheet sizes, 4 drops", mapping.Summary);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnknownShapesAreSkippedAndCounted()
|
|
{
|
|
var mapping = LedgerStockMapper.Map(new[]
|
|
{
|
|
Item("U-1", LedgerItemKinds.Drop, 20, 30, shape: LedgerItemShapes.Unknown),
|
|
Item("U-2", LedgerItemKinds.Drop, 20, 30, shape: "SomeFutureShape"),
|
|
Item("S-1", LedgerItemKinds.FullSheet, 60, 120),
|
|
}, Units.Inches);
|
|
|
|
Assert.Equal("S-1", Assert.Single(Assert.Single(mapping.Rows).ItemCodes));
|
|
Assert.Equal(2, mapping.SkippedUnknownShape);
|
|
Assert.Equal("Ledger: 1 sheet size, 0 drops (2 skipped: 2 unknown shape)", mapping.Summary);
|
|
}
|
|
|
|
[Fact]
|
|
public void IrregularDropsAreMarkedAndKeptApartFromRectangularOnes()
|
|
{
|
|
var mapping = LedgerStockMapper.Map(new[]
|
|
{
|
|
Item("D-1", LedgerItemKinds.Drop, 20, 30),
|
|
Item("D-2", LedgerItemKinds.Drop, 20, 30, shape: LedgerItemShapes.IrregularConservativeRectangle),
|
|
}, Units.Inches);
|
|
|
|
Assert.Equal(2, mapping.Rows.Count);
|
|
Assert.False(mapping.Rows[0].IsIrregular);
|
|
Assert.True(mapping.Rows[1].IsIrregular);
|
|
}
|
|
|
|
[Fact]
|
|
public void ItemsNoPartFitsAreSkipped()
|
|
{
|
|
var mapping = LedgerStockMapper.Map(new[]
|
|
{
|
|
Item("D-1", LedgerItemKinds.Drop, 10, 50),
|
|
Item("D-2", LedgerItemKinds.Drop, 20, 25),
|
|
Item("D-3", LedgerItemKinds.Drop, 5, 5),
|
|
}, Units.Inches, new[] { new Size(22, 12), new Size(40, 30) });
|
|
|
|
Assert.Equal("D-2", Assert.Single(Assert.Single(mapping.Rows).ItemCodes));
|
|
Assert.Equal(2, mapping.SkippedTooSmall);
|
|
Assert.Contains("2 too small for any part", mapping.Summary);
|
|
}
|
|
|
|
[Fact]
|
|
public void HeldOutItemsAndUnknownKindsAreIgnored()
|
|
{
|
|
var mapping = LedgerStockMapper.Map(new[]
|
|
{
|
|
Item("S-1", LedgerItemKinds.FullSheet, 48, 96, quantity: 0),
|
|
Item("C-1", "Coil", 48, 96),
|
|
}, Units.Inches);
|
|
|
|
Assert.Empty(mapping.Rows);
|
|
}
|
|
|
|
[Fact]
|
|
public void MillimeterNestsGetMillimeterSizes()
|
|
{
|
|
var row = Assert.Single(LedgerStockMapper.Map(new[] { Item("S-1", LedgerItemKinds.FullSheet, 48, 96.5m) },
|
|
Units.Millimeters).Rows);
|
|
Assert.Equal(48 * 25.4, row.Size.Width, 9);
|
|
Assert.Equal(96.5 * 25.4, row.Size.Length, 9);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.25, Units.Inches, 0.25)]
|
|
[InlineData(0.18751, Units.Inches, 0.1875)]
|
|
[InlineData(6.35, Units.Millimeters, 0.25)]
|
|
[InlineData(3, Units.Millimeters, 0.1181)]
|
|
public void ThicknessQueryIsInchesToFourPlaces(double thickness, Units units, double expected) =>
|
|
Assert.Equal(expected, LedgerStockMapper.ThicknessInches(thickness, units));
|
|
|
|
[Fact]
|
|
public void LedgerIdsAreRecognized()
|
|
{
|
|
Assert.True(LedgerStockMapper.IsLedgerId("ledger:3"));
|
|
Assert.False(LedgerStockMapper.IsLedgerId("option-0"));
|
|
Assert.False(LedgerStockMapper.IsLedgerId(null));
|
|
}
|
|
|
|
private static LedgerStockItem Item(string code, string kind, decimal width, decimal length, int quantity = 1,
|
|
string shape = LedgerItemShapes.Rectangular, string location = "R1") =>
|
|
new(code, 7, kind, width, length, quantity, shape, location, location, null, null, DateTime.UtcNow);
|
|
}
|