mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 13:41:37 -05:00
feat(release): nest released groups on MaterialLedger stock first
NestRenest gains a staged run: a nest is nested on finite first stock alone, then whatever is left on its plate defaults and plate-size options. Each stage keeps only a valid layout, and the result lists the stock id each committed plate was solved on. ReleaseLedgerStock is the ledger stock of one Make Nests run: each nest is offered the free sheets and drops Auto Nest's ledger load would tick for its material, grade and thickness (substitutes included), minus codes on other open server nests (read once per run) and codes earlier nests of the same run took, which are not on the server until the release uploads them. Committed ledger plates get their codes as Auto Nest gives them. An unusable ledger nests on the plate defaults and the note says why the ledger was not used.
This commit is contained in:
1 parent
072e96c08e
commit
2939fe0802
3 files changed
+445
-17
No files matched your search
@@ -0,0 +1,159 @@
|
||||
using System.Globalization;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Data.MaterialLedger;
|
||||
|
||||
/// <summary>
|
||||
/// The MaterialLedger stock one released nest is offered: the rows Auto Nest's ledger load
|
||||
/// would tick (substitutes included). An empty offer nests on the plate defaults alone, and
|
||||
/// <see cref="NotUsed"/> says why the ledger was not used.
|
||||
/// </summary>
|
||||
public sealed record ReleaseLedgerOffer(IReadOnlyList<LedgerStockRow> Rows, string? NotUsed)
|
||||
{
|
||||
public static ReleaseLedgerOffer None(string reason) =>
|
||||
new(Array.Empty<LedgerStockRow>(), reason);
|
||||
|
||||
/// <summary>Why codes on other nests were not left out of the rows, or null.</summary>
|
||||
public string? OtherNestsUnchecked { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The rows as engine stock: finite, under their <c>ledger:{n}</c> ids, no costs.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PlateOption> Options => Rows
|
||||
.Select(row => new PlateOption
|
||||
{
|
||||
Width = row.Size.Width,
|
||||
Length = row.Size.Length,
|
||||
Quantity = row.Quantity,
|
||||
Id = row.Id,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MaterialLedger stock for one Make Nests run. Each nest is offered the free sheets and
|
||||
/// drops of its material, grade and thickness, minus codes on other open server nests (read
|
||||
/// once per run) and codes nests earlier in the same run took: those are not on the server
|
||||
/// until the release uploads them. Nothing here throws for an unusable ledger; the offer
|
||||
/// says so.
|
||||
/// </summary>
|
||||
public sealed class ReleaseLedgerStock
|
||||
{
|
||||
/// <summary>Reads the free stock for a material, grade and thickness in inches.</summary>
|
||||
public delegate Task<LedgerStockResult> StockReader(string material, string grade,
|
||||
double thicknessIn, CancellationToken cancellationToken);
|
||||
|
||||
private readonly StockReader _readStock;
|
||||
private readonly Func<CancellationToken, Task<LedgerCodesInUse>> _readCodesInUse;
|
||||
private readonly HashSet<string> _taken = new(StringComparer.Ordinal);
|
||||
private LedgerCodesInUse? _codesInUse;
|
||||
|
||||
public ReleaseLedgerStock(StockReader readStock,
|
||||
Func<CancellationToken, Task<LedgerCodesInUse>> readCodesInUse)
|
||||
{
|
||||
_readStock = readStock ?? throw new ArgumentNullException(nameof(readStock));
|
||||
_readCodesInUse = readCodesInUse
|
||||
?? throw new ArgumentNullException(nameof(readCodesInUse));
|
||||
}
|
||||
|
||||
/// <summary>The codes nests of this run have taken so far.</summary>
|
||||
public IReadOnlyCollection<string> Taken => _taken;
|
||||
|
||||
/// <summary>
|
||||
/// The stock to offer <paramref name="nest"/>, whose drawings carry their release
|
||||
/// quantities.
|
||||
/// </summary>
|
||||
public async Task<ReleaseLedgerOffer> OfferAsync(Nest nest, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
var material = nest.Material?.Name?.Trim() ?? "";
|
||||
if (material.Length == 0 || !(nest.Thickness > 0))
|
||||
return ReleaseLedgerOffer.None("the nest has no material or thickness");
|
||||
|
||||
var grade = nest.Material?.Grade?.Trim() ?? "";
|
||||
var thicknessIn = LedgerStockMapper.ThicknessInches(nest.Thickness, nest.Units);
|
||||
var result = await _readStock(material, grade, thicknessIn, cancellationToken);
|
||||
switch (result)
|
||||
{
|
||||
case LedgerStockResult.Ok ok:
|
||||
var mapping = LedgerStockMapper.Map(ok.Items, nest.Units, PartEnvelopes(nest),
|
||||
ok.Specs);
|
||||
_codesInUse ??= await _readCodesInUse(cancellationToken);
|
||||
if (_codesInUse.IsKnown)
|
||||
mapping = LedgerCodeFilter.Exclude(mapping, _codesInUse.Codes);
|
||||
var rows = LedgerCodeFilter.Exclude(mapping.Rows, _taken, out _);
|
||||
if (rows.Count == 0)
|
||||
return ReleaseLedgerOffer.None(
|
||||
$"no free stock of {Describe(nest, material, grade)}");
|
||||
return new ReleaseLedgerOffer(rows, null)
|
||||
{
|
||||
OtherNestsUnchecked = _codesInUse.Unavailable,
|
||||
};
|
||||
case LedgerStockResult.Unmapped:
|
||||
return ReleaseLedgerOffer.None($"no mapping for {Describe(nest, material, grade)}");
|
||||
case LedgerStockResult.Unauthorized:
|
||||
return ReleaseLedgerOffer.None("it rejected the API key (Tools > Options)");
|
||||
case LedgerStockResult.Unreachable unreachable:
|
||||
return ReleaseLedgerOffer.None($"unreachable: {unreachable.Reason}");
|
||||
default:
|
||||
return ReleaseLedgerOffer.None("not set up on this PC (Tools > Options)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the plates the release committed on <paramref name="offer"/>'s rows their item
|
||||
/// codes, as Auto Nest does, keeps those codes from later nests of this run, and returns
|
||||
/// the note for the release: how many sheets came from the ledger, or why it was not used.
|
||||
/// </summary>
|
||||
/// <param name="committed">
|
||||
/// Each committed plate with the engine stock id it was solved on.
|
||||
/// </param>
|
||||
public string Assign(Nest nest, ReleaseLedgerOffer offer,
|
||||
IReadOnlyList<(Plate Plate, string StockId)> committed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(offer);
|
||||
ArgumentNullException.ThrowIfNull(committed);
|
||||
if (offer.NotUsed != null)
|
||||
return $"MaterialLedger not used: {offer.NotUsed}.";
|
||||
|
||||
LedgerStockAssignment.Assign(nest.Plates, committed, offer.Rows);
|
||||
var sheets = 0;
|
||||
foreach (var (plate, stockId) in committed)
|
||||
{
|
||||
_taken.UnionWith(plate.StockItemCodes);
|
||||
if (LedgerStockMapper.IsLedgerId(stockId))
|
||||
sheets += plate.Quantity;
|
||||
}
|
||||
|
||||
var note = sheets == 0
|
||||
? "No MaterialLedger stock used."
|
||||
: string.Format(CultureInfo.CurrentCulture, "{0} sheet{1} from MaterialLedger stock.",
|
||||
sheets, sheets == 1 ? "" : "s");
|
||||
return offer.OtherNestsUnchecked == null
|
||||
? note
|
||||
: $"{note} Codes on other nests were not checked: {offer.OtherNestsUnchecked}.";
|
||||
}
|
||||
|
||||
private static List<Size> PartEnvelopes(Nest nest)
|
||||
{
|
||||
var envelopes = new List<Size>();
|
||||
var drawings = nest.Drawings
|
||||
.Where(d => !d.IsCutOff && d.Quantity.Required > 0 && d.Program != null);
|
||||
foreach (var drawing in drawings)
|
||||
{
|
||||
var box = drawing.Program.BoundingBox();
|
||||
if (double.IsFinite(box.Width) && double.IsFinite(box.Length)
|
||||
&& box.Width > 0 && box.Length > 0)
|
||||
envelopes.Add(new Size(box.Width, box.Length));
|
||||
}
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
private static string Describe(Nest nest, string material, string grade)
|
||||
{
|
||||
var unit = nest.Units == Units.Millimeters ? "mm" : "in";
|
||||
return FormattableString.Invariant(
|
||||
$"{material}{(grade.Length == 0 ? "" : " " + grade)} {nest.Thickness:G} {unit}");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenNest.Engine.Jobs;
|
||||
|
||||
@@ -16,7 +17,14 @@ public sealed record NestRenestResult(
|
||||
double Utilization,
|
||||
IReadOnlyList<NestUnplacedPart> Unplaced,
|
||||
string Note
|
||||
);
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Each committed plate with the engine stock id it was solved on, in commit order.
|
||||
/// </summary>
|
||||
public IReadOnlyList<(Plate Plate, string StockId)> Committed { get; init; } =
|
||||
Array.Empty<(Plate Plate, string StockId)>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto Nest without the dialog, for a nest no one is looking at yet (a released drawing
|
||||
@@ -25,8 +33,8 @@ public sealed record NestRenestResult(
|
||||
/// <see cref="NestPipeline"/>. A layout is committed only when it is valid and keepable;
|
||||
/// otherwise the nest is left without plates for the operator to run Auto Nest. Copies the
|
||||
/// layout leaves unplaced stay as remaining quantity. The caller owns the nest throughout.
|
||||
/// A caller showing the nest uses <see cref="CreateRequest"/> and <see cref="Keep"/> to solve
|
||||
/// off its thread and commit on it.
|
||||
/// A caller showing the nest uses <see cref="RunStagedAsync"/> (or <see cref="CreateRequest"/>
|
||||
/// and <see cref="Keep"/>) to solve off its thread and commit on it.
|
||||
/// </summary>
|
||||
public static class NestRenest
|
||||
{
|
||||
@@ -48,7 +56,17 @@ public static class NestRenest
|
||||
/// Smallest offcut credited when the nest offers plate-size options, in the nest's units.
|
||||
/// </param>
|
||||
public static NestRenestResult Run(Nest nest, INestingEngine engine, string engineName,
|
||||
double minimumSalvageDimension = 0, IProgress<NestJobProgress> progress = null, CancellationToken token = default)
|
||||
double minimumSalvageDimension = 0, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default) =>
|
||||
Run(nest, engine, engineName, null, minimumSalvageDimension, progress, token);
|
||||
|
||||
/// <param name="firstStock">
|
||||
/// Sheets offered before the plate defaults (see <see cref="RunStagedAsync"/>); null or
|
||||
/// empty nests on the plate defaults alone.
|
||||
/// </param>
|
||||
public static NestRenestResult Run(Nest nest, INestingEngine engine, string engineName,
|
||||
IReadOnlyList<PlateOption> firstStock, double minimumSalvageDimension = 0,
|
||||
IProgress<NestJobProgress> progress = null, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(engine);
|
||||
@@ -56,14 +74,66 @@ public static class NestRenest
|
||||
|
||||
nest.Plates.Clear();
|
||||
nest.UpdateDrawingQuantities();
|
||||
var request = CreateRequest(nest, engineName, minimumSalvageDimension);
|
||||
if (request == null)
|
||||
return new NestRenestResult(false, 0, 0, Array.Empty<NestUnplacedPart>(), NothingToNestNote);
|
||||
// Solved synchronously: the returned task has already completed.
|
||||
return RunStagedAsync(nest, engineName, firstStock, minimumSalvageDimension,
|
||||
request => Task.FromResult(
|
||||
NestPipeline.Run(engine, engineName, request, progress, token)),
|
||||
kept => NestPipelineCommit.ApplyToPlates(kept, nest.CreatePlate, token: token),
|
||||
token)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var result = NestPipeline.Run(engine, engineName, request, progress, token);
|
||||
// Engines that ignore cancellation still have their proposal discarded.
|
||||
/// <summary>
|
||||
/// Nests every drawing's remaining quantity of <paramref name="nest"/>, which has no parts
|
||||
/// placed yet, in up to two kept layouts: first on <paramref name="firstStock"/> alone
|
||||
/// (finite sheets such as MaterialLedger stock, so they are used before anything else),
|
||||
/// then whatever is left on the plate defaults and plate-size options. A stage whose layout
|
||||
/// is not valid and keepable commits nothing, so its copies fall to the next stage or stay
|
||||
/// remaining. <paramref name="solve"/> runs a request; <paramref name="commit"/> attaches a
|
||||
/// kept result's sheets to the nest on the nest's own thread and returns them.
|
||||
/// </summary>
|
||||
public static async Task<NestRenestResult> RunStagedAsync(Nest nest, string engineName,
|
||||
IReadOnlyList<PlateOption> firstStock, double minimumSalvageDimension,
|
||||
Func<NestPipelineRequest, Task<NestPipelineResult>> solve,
|
||||
Func<NestPipelineResult, IReadOnlyList<Plate>> commit, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(solve);
|
||||
ArgumentNullException.ThrowIfNull(commit);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return Keep(result, kept => NestPipelineCommit.ApplyToPlates(kept, nest.CreatePlate, token: token));
|
||||
|
||||
var applied = new List<Plate>();
|
||||
var committed = new List<(Plate Plate, string StockId)>();
|
||||
IReadOnlyList<NestUnplacedPart> unplaced = Array.Empty<NestUnplacedPart>();
|
||||
var anyRequest = false;
|
||||
var stages = firstStock is { Count: > 0 }
|
||||
? new[] { firstStock, null }
|
||||
: new IReadOnlyList<PlateOption>[] { null };
|
||||
foreach (var stock in stages)
|
||||
{
|
||||
var request = CreateRequest(nest, engineName, minimumSalvageDimension, stock,
|
||||
remainingOnly: true);
|
||||
if (request == null)
|
||||
break;
|
||||
anyRequest = true;
|
||||
var result = await solve(request);
|
||||
// Engines that ignore cancellation still have their proposal discarded.
|
||||
token.ThrowIfCancellationRequested();
|
||||
unplaced = result.Unplaced;
|
||||
if (!IsKeepable(result))
|
||||
continue;
|
||||
// Attaching parts counts them as nested, so the next stage asks only for the rest.
|
||||
var plates = commit(result);
|
||||
committed.AddRange(StockIds(result, plates));
|
||||
applied.AddRange(plates);
|
||||
}
|
||||
|
||||
if (!anyRequest)
|
||||
return new NestRenestResult(false, 0, 0, Array.Empty<NestUnplacedPart>(),
|
||||
NothingToNestNote);
|
||||
if (applied.Count == 0)
|
||||
return new NestRenestResult(false, 0, 0, unplaced, NotKeptNote);
|
||||
return Summarize(applied, unplaced) with { Committed = committed };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -71,15 +141,23 @@ public static class NestRenest
|
||||
/// sheets from its plate defaults and plate-size options; null when nothing has a quantity.
|
||||
/// The nest is not changed, so a caller showing it can solve this off its thread.
|
||||
/// </summary>
|
||||
public static NestPipelineRequest CreateRequest(Nest nest, string engineName, double minimumSalvageDimension = 0)
|
||||
public static NestPipelineRequest CreateRequest(Nest nest, string engineName,
|
||||
double minimumSalvageDimension = 0) =>
|
||||
CreateRequest(nest, engineName, minimumSalvageDimension, null, remainingOnly: false);
|
||||
|
||||
// stock: offered instead of the nest's plate-size options; remainingOnly: copies already
|
||||
// placed are not asked for again.
|
||||
private static NestPipelineRequest CreateRequest(Nest nest, string engineName,
|
||||
double minimumSalvageDimension, IReadOnlyList<PlateOption> stock, bool remainingOnly)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
int Wanted(Drawing d) => remainingOnly ? d.Quantity.Remaining : d.Quantity.Required;
|
||||
var items = nest.Drawings
|
||||
.Where(d => !d.IsCutOff && d.Quantity.Required > 0)
|
||||
.Where(d => !d.IsCutOff && Wanted(d) > 0)
|
||||
.Select(d => new NestItem
|
||||
{
|
||||
Drawing = d,
|
||||
Quantity = d.Quantity.Required,
|
||||
Quantity = Wanted(d),
|
||||
Priority = d.Priority,
|
||||
RotationStart = d.Constraints.StartAngle,
|
||||
RotationEnd = d.Constraints.EndAngle,
|
||||
@@ -89,7 +167,7 @@ public static class NestRenest
|
||||
if (items.Count == 0)
|
||||
return null;
|
||||
|
||||
var options = nest.PlateOptions;
|
||||
var options = stock ?? nest.PlateOptions;
|
||||
return new NestPipelineRequest(engineName, items,
|
||||
NestStockBuilder.FromTemplate(nest.PlateDefaults.CreateNew(), options),
|
||||
new NestJobOptions(maxPlates: MaxPlates,
|
||||
@@ -106,14 +184,32 @@ public static class NestRenest
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
ArgumentNullException.ThrowIfNull(commit);
|
||||
if (!result.IsValid || !result.CanKeep || result.Plates.All(p => p.Parts.Count == 0))
|
||||
if (!IsKeepable(result))
|
||||
return new NestRenestResult(false, 0, 0, result.Unplaced, NotKeptNote);
|
||||
|
||||
var applied = commit(result);
|
||||
return Summarize(applied, result.Unplaced) with { Committed = StockIds(result, applied) };
|
||||
}
|
||||
|
||||
private static List<(Plate Plate, string StockId)> StockIds(NestPipelineResult result,
|
||||
IReadOnlyList<Plate> applied) =>
|
||||
NestPipelineCommit.PairWithStock(result, applied)
|
||||
.Select(p => (p.Plate, p.Stock.Id))
|
||||
.ToList();
|
||||
|
||||
private static bool IsKeepable(NestPipelineResult result) =>
|
||||
result.IsValid && result.CanKeep && result.Plates.Any(p => p.Parts.Count > 0);
|
||||
|
||||
private static NestRenestResult Summarize(IReadOnlyList<Plate> applied,
|
||||
IReadOnlyList<NestUnplacedPart> unplaced)
|
||||
{
|
||||
var sheets = applied.Sum(p => p.Quantity);
|
||||
var area = applied.Sum(p => p.Area() * p.Quantity);
|
||||
var utilization = area > 0 ? applied.Sum(p => p.Utilization() * p.Area() * p.Quantity) / area : 0;
|
||||
return new NestRenestResult(true, sheets, utilization, result.Unplaced, Note(sheets, utilization, result.Unplaced));
|
||||
var utilization = area > 0
|
||||
? applied.Sum(p => p.Utilization() * p.Area() * p.Quantity) / area
|
||||
: 0;
|
||||
return new NestRenestResult(true, sheets, utilization, unplaced,
|
||||
Note(sheets, utilization, unplaced));
|
||||
}
|
||||
|
||||
private static string Note(int sheets, double utilization, IReadOnlyList<NestUnplacedPart> unplaced)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Data.MaterialLedger;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class ReleaseLedgerStockTests
|
||||
{
|
||||
private const string Material = "Synthetic Steel";
|
||||
|
||||
private static Nest NestOf(double partSize, int quantity, string grade = "SYN-1")
|
||||
{
|
||||
var nest = new Nest("N26-0001")
|
||||
{
|
||||
Material = new Material(Material, grade),
|
||||
Thickness = 0.25,
|
||||
};
|
||||
nest.PlateDefaults.Size = new Size(48, 96);
|
||||
var drawing = new RectangleShape { Name = "SYN-A", Length = partSize, Width = partSize }
|
||||
.GetDrawing();
|
||||
drawing.Quantity.Required = quantity;
|
||||
nest.Drawings.Add(drawing);
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static LedgerStockItem Sheet(string code, decimal width, decimal length) =>
|
||||
new(code, 1, LedgerItemKinds.FullSheet, width, length, 1, LedgerItemShapes.Rectangular,
|
||||
"R1", "Rack 1", null, null, DateTime.UnixEpoch);
|
||||
|
||||
private static LedgerStockResult Stock(params LedgerStockItem[] items) =>
|
||||
new LedgerStockResult.Ok(Array.Empty<LedgerStockSpec>(), items);
|
||||
|
||||
private sealed class FakeLedger
|
||||
{
|
||||
public LedgerStockResult Stock { get; init; } = Stock();
|
||||
public LedgerCodesInUse InUse { get; init; } =
|
||||
LedgerCodesInUse.Known(Array.Empty<NestStockCodeUse>());
|
||||
public List<(string Material, string Grade, double Thickness)> StockReads { get; } = new();
|
||||
public int InUseReads { get; private set; }
|
||||
|
||||
public ReleaseLedgerStock Create() => new(
|
||||
(material, grade, thickness, _) =>
|
||||
{
|
||||
StockReads.Add((material, grade, thickness));
|
||||
return Task.FromResult(Stock);
|
||||
},
|
||||
_ =>
|
||||
{
|
||||
InUseReads++;
|
||||
return Task.FromResult(InUse);
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<(NestRenestResult Renest, string Note)> Release(
|
||||
ReleaseLedgerStock ledger, Nest nest)
|
||||
{
|
||||
var offer = await ledger.OfferAsync(nest, CancellationToken.None);
|
||||
var renest = NestRenest.Run(nest, NestingEngineRegistry.Create("Rectangles"), "Rectangles",
|
||||
offer.Options);
|
||||
return (renest, ledger.Assign(nest, offer, renest.Committed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LedgerStockIsUsedFirst_EvenWhenTheDefaultSheetWouldBeSmaller()
|
||||
{
|
||||
// One 60x120 ledger sheet holds both parts; a single pass scored by area would take
|
||||
// the smaller 48x96 Nest Defaults sheet instead.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var (renest, note) = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
Assert.Equal(new Size(60, 120), plate.Size);
|
||||
Assert.Equal(["S-1"], plate.StockItemCodes);
|
||||
Assert.Equal(0, nest.Drawings.First().Quantity.Remaining);
|
||||
Assert.Equal("1 sheet from MaterialLedger stock.", note);
|
||||
Assert.Equal((Material, "SYN-1", 0.25), Assert.Single(ledger.StockReads));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopiesTheLedgerCannotHold_TopUpOnTheNestDefaultsSheet_WithoutACode()
|
||||
{
|
||||
// Two free 40x40 sheets each hold one 30x30 part; the third copy needs a default sheet.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 40, 40), Sheet("S-2", 40, 40)) };
|
||||
var nest = NestOf(30, 3);
|
||||
|
||||
var (renest, note) = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
Assert.Empty(renest.Unplaced);
|
||||
Assert.Equal(0, nest.Drawings.First().Quantity.Remaining);
|
||||
var coded = nest.Plates.Where(p => p.StockItemCodes.Count > 0).ToList();
|
||||
Assert.Equal(["S-1", "S-2"], coded.SelectMany(p => p.StockItemCodes).Order());
|
||||
Assert.All(coded, p => Assert.Equal(new Size(40, 40), p.Size));
|
||||
var topUp = Assert.Single(nest.Plates.Where(p => p.StockItemCodes.Count == 0));
|
||||
Assert.Equal(new Size(48, 96), topUp.Size);
|
||||
Assert.Equal(3, renest.Sheets);
|
||||
Assert.Equal("2 sheets from MaterialLedger stock.", note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ALaterNestOfTheSameRun_DoesNotGetCodesAnEarlierNestTook()
|
||||
{
|
||||
// The release uploads every nest only after all are nested, so the server cannot
|
||||
// report the first nest's codes yet; a substitute row can list the same sheet twice.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var run = ledger.Create();
|
||||
await Release(run, NestOf(20, 2));
|
||||
|
||||
var second = NestOf(20, 2, grade: "SYN-2");
|
||||
var (renest, note) = await Release(run, second);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
var plate = Assert.Single(second.Plates);
|
||||
Assert.Empty(plate.StockItemCodes);
|
||||
Assert.Equal(new Size(48, 96), plate.Size);
|
||||
Assert.Equal("MaterialLedger not used: no free stock of Synthetic Steel SYN-2 0.25 in.",
|
||||
note);
|
||||
Assert.Equal(["S-1"], run.Taken);
|
||||
Assert.Equal(1, ledger.InUseReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CodesOnOtherServerNests_AreLeftOut()
|
||||
{
|
||||
var ledger = new FakeLedger
|
||||
{
|
||||
Stock = Stock(Sheet("S-1", 60, 120), Sheet("S-2", 60, 120)),
|
||||
InUse = LedgerCodesInUse.Known(
|
||||
[new NestStockCodeUse { ItemCode = "S-1", NestName = "N26-0099" }]),
|
||||
};
|
||||
|
||||
var offer = await ledger.Create().OfferAsync(NestOf(20, 2), CancellationToken.None);
|
||||
|
||||
Assert.Null(offer.NotUsed);
|
||||
Assert.Equal(["S-2"], Assert.Single(offer.Rows).ItemCodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnreachableLedger_NestsOnTheNestDefaultsSheet_AndSaysSo()
|
||||
{
|
||||
var ledger = new FakeLedger { Stock = new LedgerStockResult.Unreachable("timed out") };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var (renest, note) = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
Assert.Equal(new Size(48, 96), plate.Size);
|
||||
Assert.Empty(plate.StockItemCodes);
|
||||
Assert.Equal("MaterialLedger not used: unreachable: timed out.", note);
|
||||
Assert.Equal(0, ledger.InUseReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UncheckedOtherNests_AreNamedInTheNote()
|
||||
{
|
||||
var ledger = new FakeLedger
|
||||
{
|
||||
Stock = Stock(Sheet("S-1", 60, 120)),
|
||||
InUse = LedgerCodesInUse.Unknown("the nest server did not answer"),
|
||||
};
|
||||
|
||||
var (_, note) = await Release(ledger.Create(), NestOf(20, 2));
|
||||
|
||||
Assert.Equal("1 sheet from MaterialLedger stock. Codes on other nests were not checked: "
|
||||
+ "the nest server did not answer.", note);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user