fix(desktop): count only active MaterialLedger holds and link the nest page

MaterialLedger's Release lets a hold go, so a Released hold no longer keeps
a sheet for the nest; only Proposed, Approved and AwaitingCloseout holds,
the statuses ML counts against free stock, are open. The Has Been Cut
note's link pointed at /Nests/{id}, which ML does not serve; its nest page
is /Nests/Details/{id}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-25 18:20:11 -04:00
1 parent c27188c722
commit 079dfaf9ea
6 files changed
+18 -15

No files matched your search

@@ -50,7 +50,7 @@ public sealed class LedgerHoldSummary
public int OpenHolds { get; }
/// <summary>Holds in any other status: closed, cancelled, exception or unknown.</summary>
/// <summary>Holds in any other status: released, closed, cancelled, exception or unknown.</summary>
public int ClosedHolds { get; }
/// <summary>Sheets without an open hold.</summary>
+3 -3
View File
@@ -30,13 +30,13 @@ public static class LedgerHoldStatuses
public const string Exception = "Exception";
/// <summary>
/// True for holds that still keep a sheet for the nest and need closing out. Closed,
/// cancelled, exception and unknown statuses are not open.
/// True for holds that still keep a sheet for the nest and need closing out: the statuses ML
/// counts against free stock. Released means the hold was let go, not sent to cutting, so it
/// is not open; nor are closed, cancelled, exception and unknown statuses.
/// </summary>
public static bool IsOpen(string? status) =>
string.Equals(status, Proposed, StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, Approved, StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, Released, StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, AwaitingCloseout, StringComparison.OrdinalIgnoreCase);
}
@@ -23,19 +23,20 @@ public class LedgerHoldSummaryTests
[Fact]
public void AllSheetsHeld()
{
var summary = Summarize(4, Hold("Approved"), Hold("Released"), Hold("Proposed"), Hold("AwaitingCloseout"));
var summary = Summarize(4, Hold("Approved"), Hold("Approved"), Hold("Proposed"), Hold("AwaitingCloseout"));
Assert.Equal(LedgerHoldState.Held, summary.State);
Assert.Equal("Ledger: 4 of 4 sheets held", summary.Text);
Assert.Equal(0, summary.ShortSheets);
}
// ML's Release lets a hold go: the sheet is free stock again, so the nest is short of it.
[Fact]
public void ClosedAndUnknownHoldsDoNotCountAsHeld()
public void ReleasedClosedAndUnknownHoldsDoNotCountAsHeld()
{
var summary = Summarize(4, Hold("Approved"), Hold("Released"), Hold("ClosedConsumed"),
var summary = Summarize(4, Hold("Approved"), Hold("Proposed"), Hold("Released"), Hold("ClosedConsumed"),
Hold("Cancelled"), Hold("Exception"), Hold("OnTruck"));
Assert.Equal(LedgerHoldState.Short, summary.State);
Assert.Equal((2, 4, 2), (summary.OpenHolds, summary.ClosedHolds, summary.ShortSheets));
Assert.Equal((2, 5, 2), (summary.OpenHolds, summary.ClosedHolds, summary.ShortSheets));
Assert.Equal("Ledger: 2 of 4 held, 2 short", summary.Text);
}
@@ -47,7 +47,7 @@ public class NestHoldStatusTests
var document = Bound(sheets: 4);
var changes = 0;
HttpRequestMessage seen = null;
var status = Status(document, databaseMode: true, request => { seen = request; return Holds("Approved", "Released"); });
var status = Status(document, databaseMode: true, request => { seen = request; return Holds("Approved", "Proposed", "Released"); });
status.Changed += (_, _) => changes++;
await status.RefreshAfterSaveAsync();
@@ -56,7 +56,7 @@ public class NestHoldStatusTests
Assert.False(status.IsRefreshing);
Assert.Equal("Ledger: 2 of 4 held, 2 short", status.Summary.Text);
Assert.Contains(document.RemoteId.ToString("D"), seen.RequestUri!.Query);
Assert.Equal($"http://ledger:5000/Nests/{document.RemoteId:D}", status.LedgerNestUrl);
Assert.Equal($"http://ledger:5000/Nests/Details/{document.RemoteId:D}", status.LedgerNestUrl);
Assert.Equal(2, changes); // Refreshing, then the answer.
// An on-demand refresh counts the sheets of the last save, not unsaved edits.
+1 -1
View File
@@ -60,7 +60,7 @@ namespace OpenNest.Forms
public bool IsVisible => IsApplicable && (Summary != null || IsRefreshing);
/// <summary>The nest's page in MaterialLedger, where its holds are closed out.</summary>
public string LedgerNestUrl => $"{ledgerSettings().BaseUrl.TrimEnd('/')}/Nests/{document.RemoteId:D}";
public string LedgerNestUrl => $"{ledgerSettings().BaseUrl.TrimEnd('/')}/Nests/Details/{document.RemoteId:D}";
/// <summary>
/// Rereads the holds after the nest was opened or saved, counting the sheets of the nest
+6 -4
View File
@@ -172,9 +172,11 @@ Each read uses `MaterialLedgerClient.GetHoldsAsync` (`GET /api/stock/holds?openN
**Summary.** `LedgerHoldSummary` (`OpenNest.Data/MaterialLedger/`) compares the
open holds with the sheets of the last open or save: the sum of plate quantities,
skipping the trailing empty plate as `/sheets` does. Proposed, Approved, Released
and AwaitingCloseout holds are open. Closed, cancelled, exception and unknown
statuses are listed but not counted; unknown status strings are kept as sent.
skipping the trailing empty plate as `/sheets` does. Proposed, Approved and
AwaitingCloseout holds are open, the statuses ML counts against free stock. Released
means the hold was let go, not sent to cutting, so it is listed but not counted, as
are closed, cancelled, exception and unknown statuses; unknown status strings are kept
as sent.
| Holds | Label |
|-------|-------|
@@ -189,7 +191,7 @@ nest's material label beside it.
**Has Been Cut.** When Nest Info changes the status to Has Been Cut, the desktop
rereads the holds. If any are still open, it shows a non-modal note, "Close out N holds
in MaterialLedger", with a link to `{ledgerUrl}/Nests/{id}`. Closeout happens there.
in MaterialLedger", with a link to `{ledgerUrl}/Nests/Details/{id}`. Closeout happens there.
The note never blocks the dialog, save or post, and posting is not gated on holds.
### Generated nest names