mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-11 05:14:13 -04:00
feat(ui): validate Auto Nest stock prices and report costs
This commit is contained in:
1 parent
c34ac2bc0e
commit
0c99455489
4 files changed
+123
-24
No files matched your search
@@ -10,6 +10,64 @@ namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class AutoNestFormStockOptionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("invalid", false)]
|
||||
[InlineData("-1", false)]
|
||||
[InlineData("NaN", false)]
|
||||
[InlineData("27", true)]
|
||||
public void PendingCostEditsAreValidatedBeforePublishing(string text, bool valid)
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var form = new AutoNestForm(new Nest());
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96, Cost = 5 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
((TabControl)form.Controls.Find("tabControl", true).Single()).SelectedIndex = 1;
|
||||
form.Show();
|
||||
grid.Focus();
|
||||
grid.CurrentCell = grid.Rows[0].Cells[1];
|
||||
Assert.True(grid.BeginEdit(false));
|
||||
grid.EditingControl.Text = text;
|
||||
Assert.Equal(valid, form.TryGetPlateOptions(out var options, out var error));
|
||||
if (valid)
|
||||
Assert.Equal(27, Assert.Single(options).Cost);
|
||||
else
|
||||
{
|
||||
Assert.Empty(options);
|
||||
Assert.Contains("cost", error);
|
||||
}
|
||||
grid.CancelEdit();
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0, true)]
|
||||
[InlineData(5, 8, true)]
|
||||
[InlineData(5, 0, false)]
|
||||
[InlineData(0, 5, false)]
|
||||
[InlineData(-1, -1, false)]
|
||||
[InlineData(double.NaN, 5, false)]
|
||||
[InlineData(double.PositiveInfinity, 5, false)]
|
||||
public void CostRowsRequireConsistentFiniteValues(double first, double second, bool valid)
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var form = new AutoNestForm(new Nest());
|
||||
form.LoadPlateOptions(new List<PlateOption>
|
||||
{
|
||||
new() { Width = 48, Length = 96, Cost = first },
|
||||
new() { Width = 60, Length = 120, Cost = second },
|
||||
}, 0.5);
|
||||
Assert.Equal(valid, form.TryGetPlateOptions(out var options, out var error));
|
||||
if (!valid)
|
||||
{
|
||||
Assert.Empty(options);
|
||||
Assert.Contains("cost", error);
|
||||
}
|
||||
AssertNewRow(GetGrid(form));
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
@@ -62,7 +120,7 @@ public class AutoNestFormStockOptionsTests
|
||||
Assert.True(rows.AllowNew);
|
||||
var added = rows.AddNew()!;
|
||||
SetValue(added, "Size", "84 x 168");
|
||||
SetValue(added, "Cost", 123.5);
|
||||
SetValue(added, "Cost", loadSavedOptions ? 123.5 : 0);
|
||||
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||
|
||||
var options = form.GetPlateOptions();
|
||||
@@ -70,7 +128,7 @@ public class AutoNestFormStockOptionsTests
|
||||
var option = options.Last();
|
||||
Assert.Equal(84, option.Width);
|
||||
Assert.Equal(168, option.Length);
|
||||
Assert.Equal(123.5, option.Cost);
|
||||
Assert.Equal(loadSavedOptions ? 123.5 : 0, option.Cost);
|
||||
AssertNewRow(grid);
|
||||
});
|
||||
}
|
||||
@@ -128,6 +186,7 @@ public class AutoNestFormStockOptionsTests
|
||||
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||
var row = rows.AddNew()!;
|
||||
SetValue(row, "Size", invalid);
|
||||
SetValue(row, "Cost", 25);
|
||||
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
|
||||
@@ -152,6 +152,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
DataPropertyName = "Cost",
|
||||
HeaderText = "Cost",
|
||||
ToolTipText = "Per physical sheet, in one common unit. Use zero on every row for area scoring, or positive costs on every row. Salvage credits cost proportionally; set salvage to zero for purchase totals.",
|
||||
Width = 70,
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
|
||||
}
|
||||
@@ -235,6 +236,15 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
if (plateGrid.IsCurrentCellInEditMode && plateGrid.CurrentCell != null
|
||||
&& plateGrid.Columns[plateGrid.CurrentCell.ColumnIndex].DataPropertyName == "Cost"
|
||||
&& plateGrid.EditingControl != null
|
||||
&& !ValidCost(plateGrid.EditingControl.Text))
|
||||
{
|
||||
error = $"Invalid stock cost in row {plateGrid.CurrentCell.RowIndex + 1}. Enter a finite positive cost, or zero on every row.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool committed;
|
||||
try
|
||||
{
|
||||
@@ -269,6 +279,14 @@ namespace OpenNest.Forms
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!double.IsFinite(item.Cost) || item.Cost < 0
|
||||
|| validated.Count > 0 && (validated[0].Cost == 0) != (item.Cost == 0))
|
||||
{
|
||||
error = $"Invalid stock cost in row {index + 1}. Use finite positive costs on every active row, or zero on every row.";
|
||||
if (index < plateGrid.Rows.Count)
|
||||
plateGrid.Rows[index].ErrorText = error;
|
||||
return false;
|
||||
}
|
||||
if (index < plateGrid.Rows.Count)
|
||||
plateGrid.Rows[index].ErrorText = "";
|
||||
validated.Add(new PlateOption { Width = width, Length = length, Cost = item.Cost });
|
||||
@@ -378,8 +396,17 @@ namespace OpenNest.Forms
|
||||
UpdateSummary();
|
||||
}
|
||||
|
||||
private static bool ValidCost(string text) =>
|
||||
double.TryParse(text, out var cost) && double.IsFinite(cost) && cost >= 0;
|
||||
|
||||
private void PlateGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
|
||||
{
|
||||
if (plateGrid.Columns[e.ColumnIndex].DataPropertyName == "Cost")
|
||||
{
|
||||
e.Cancel = !plateGrid.Rows[e.RowIndex].IsNewRow && !ValidCost(e.FormattedValue?.ToString());
|
||||
plateGrid.Rows[e.RowIndex].ErrorText = e.Cancel ? "Enter a finite positive cost, or zero on every row." : "";
|
||||
return;
|
||||
}
|
||||
if (plateGrid.Columns[e.ColumnIndex].DataPropertyName != "Size")
|
||||
return;
|
||||
|
||||
|
||||
@@ -1494,6 +1494,7 @@ namespace OpenNest.Forms
|
||||
allowInvalid = true;
|
||||
}
|
||||
|
||||
var costs = result.IsValid && result.CanKeep ? NestCostSummary.FromAccepted(result) : null;
|
||||
var applied = NestPipelineCommit.ApplyToEmptyPlates(result, target.PlateManager,
|
||||
allowInvalid, cts.Token);
|
||||
if (applied.Count > 0)
|
||||
@@ -1509,6 +1510,9 @@ namespace OpenNest.Forms
|
||||
target.OverlapDisplay = OverlapDisplayMode.Both;
|
||||
await target.CheckOverlapsAsync();
|
||||
}
|
||||
if (costs?.Basis == "supplied-cost")
|
||||
MessageBox.Show(this, $"Gross sheet cost: {costs.GrossTotal:G}\nEstimated salvage credit: {costs.SalvageCredit:G}\nNet score: {costs.NetScore:G}",
|
||||
"Auto Nest Cost", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
if (result.Status != NestJobStatus.Complete)
|
||||
MessageBox.Show(this, $"{engineName} could not place every part ({result.StopReason}).",
|
||||
"Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
|
||||
+31
-22
@@ -101,29 +101,38 @@ Response archive schema 3 persists validation status and violations. Older archi
|
||||
|
||||
## Stock costs
|
||||
|
||||
API `NestRequestPlate.Cost` is an optional per-physical-sheet cost in caller-consistent
|
||||
units. Omit every cost for area scoring, or supply strictly positive finite costs
|
||||
for every available row. Explicit zero and mixed priced/unpriced available stock
|
||||
are rejected before import. Quantity-zero rows do not select the cost mode.
|
||||
|
||||
Validated proposals expose `Costs` (`Basis`, `Stock`, `GrossTotal`, `SalvageCredit`,
|
||||
`NetScore`) and persist them in quote archives. `area` identifies fallback scoring;
|
||||
`supplied-cost` identifies explicit prices. API salvage is disabled, so two sheets
|
||||
costing 25 each report gross and net totals of 50 with zero salvage credit. Totals
|
||||
exclude penalties for unplaced demand. Invalid proposals have no cost summary;
|
||||
overflow fails instead of becoming a score.
|
||||
|
||||
Returned nests preserve offered stock prices. These are cost-aware heuristics,
|
||||
not global cost optimizers; validation and fulfillment remain required.
|
||||
|
||||
Auto Nest Stock Options accepts a generic **per physical sheet** Cost. Enter positive
|
||||
finite costs on every active row in one common unit (purchase price, area, or another
|
||||
consistent scalar). All-zero rows retain legacy area scoring: zero means unspecified,
|
||||
not free stock. Negative/nonfinite costs and mixed zero/positive rows are rejected
|
||||
before solving. The blank add row is ignored.
|
||||
|
||||
MCP `autonest_job` accepts optional `cost` on each `sheets` row, alongside `width`,
|
||||
`length` and positive finite `quantity`. Supply positive finite costs on every row
|
||||
or omit all costs. MCP continues to reject duplicate sizes and disables salvage.
|
||||
Its result reports the cost basis, used physical counts, supplied costs, gross
|
||||
subtotals, gross total, estimated salvage credit and net score for the proposal.
|
||||
`length` and positive finite `quantity`. API `NestRequestPlate.Cost` uses the same
|
||||
nullable contract: omit every cost for area scoring, or supply strictly positive
|
||||
finite values for every available row. Explicit zero is invalid in these requests.
|
||||
Engine stock with quantity zero is unavailable and does not select the cost mode.
|
||||
MCP continues to reject duplicate sizes; API/engine stock IDs keep independent inventory.
|
||||
|
||||
With prices, gross sheet cost is the supplied value. Salvage is still computed from
|
||||
the largest qualifying material-edge offcut; net cost is
|
||||
`Cost * (NetSheetArea / SheetArea)`. Set desktop salvage rate to zero for purchase-price
|
||||
comparisons. Entering prices preserves the selected salvage settings. MCP and API
|
||||
currently disable salvage. Without prices, scores retain their existing area units
|
||||
and tie ordering; fixed strategies retain their legacy no-salvage area tie-break.
|
||||
|
||||
Desktop reports gross cost, estimated salvage credit and net score. MCP reports
|
||||
`basis`, each stock's used physical count, supplied cost and gross subtotal, plus
|
||||
gross total, estimated salvage credit and net score. API exposes these under `Costs`
|
||||
(`Basis`, `Stock`, `GrossTotal`, `SalvageCredit`, `NetScore`) and persists them in
|
||||
quote archives. For example, two sheets costing 25 each report gross total 50;
|
||||
with salvage disabled, credit is 0 and net score is 50. `area` identifies fallback
|
||||
scoring; `supplied-cost` identifies explicit prices. Totals cover the returned proposal,
|
||||
not previously occupied session sheets, and exclude penalties for unplaced demand.
|
||||
Invalid API proposals have no cost summary. Overflow fails instead of becoming a score.
|
||||
|
||||
An accepted MCP solve stores requested drawing quantities and offered stock prices;
|
||||
save/reopen preserves them and existing nest metadata, plate defaults and salvage
|
||||
settings. Rejected/cancelled proposals change neither requirements nor options.
|
||||
Inventory remains request-local; saved options are offers, not a purchase ledger.
|
||||
save/reopen preserves them and existing nest metadata. Rejected/cancelled proposals
|
||||
change neither requirements nor options. Inventory remains request-local; saved
|
||||
options are offers, not a per-sheet purchase ledger. These are cost-aware heuristics,
|
||||
not global cost optimizers; validation and fulfillment remain required.
|
||||
Reference in new issue
Block a user