Compare commits
12
Commits
7b210f518b
...
ec52834cd4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec52834cd4 | ||
|
|
d8656cc454 | ||
|
|
0333942cb3 | ||
|
|
49a539d7e5 | ||
|
|
6bdbdf4969 | ||
|
|
383fa19cf2 | ||
|
|
843ba24788 | ||
|
|
41283987b9 | ||
|
|
41dda11062 | ||
|
|
f71cf8ab0a | ||
|
|
fd6e8d632c | ||
|
|
69c993fcb2 |
@@ -120,13 +120,10 @@ Stdio-transport MCP server (`ModelContextProtocol` SDK) exposing CutList.Web's R
|
|||||||
Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its own standalone table (`DimAngle`, `DimChannel`, `DimFlatBar`, `DimIBeam`, `DimPipe`, `DimRectangularTube`, `DimRoundBar`, `DimRoundTube`, `DimSquareBar`, `DimSquareTube`) with no base table. Each table has its own `Id` (shared sequence) and `MaterialId` FK. Each generates its own `SizeString` and `SortOrder`.
|
Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its own standalone table (`DimAngle`, `DimChannel`, `DimFlatBar`, `DimIBeam`, `DimPipe`, `DimRectangularTube`, `DimRoundBar`, `DimRoundTube`, `DimSquareBar`, `DimSquareTube`) with no base table. Each table has its own `Id` (shared sequence) and `MaterialId` FK. Each generates its own `SizeString` and `SortOrder`.
|
||||||
|
|
||||||
### StockItem
|
### StockItem
|
||||||
- `MaterialId`, `LengthInches` (decimal), `QuantityOnHand` (int), `IsActive`
|
- `MaterialId`, `LengthInches` (decimal), `IsActive`
|
||||||
- **Unique constraint**: (MaterialId, LengthInches)
|
- **Unique constraint**: (MaterialId, LengthInches)
|
||||||
- **Relationships**: `Material`, `Transactions` (1:many StockTransaction)
|
- **Relationships**: `Material`
|
||||||
|
- No quantity tracking — a StockItem represents a length of material you can cut from, not a counted inventory record
|
||||||
### StockTransaction
|
|
||||||
- `StockItemId`, `Quantity` (signed delta), `Type` (Received/Used/Adjustment/Scrapped/Returned)
|
|
||||||
- Optional: `JobId`
|
|
||||||
|
|
||||||
### CuttingTool
|
### CuttingTool
|
||||||
- `Name`, `KerfInches` (decimal), `IsDefault` (bool), `IsActive`
|
- `Name`, `KerfInches` (decimal), `IsDefault` (bool), `IsActive`
|
||||||
@@ -154,8 +151,6 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
|
|
||||||
### StockItemService
|
### StockItemService
|
||||||
- CRUD with soft delete
|
- CRUD with soft delete
|
||||||
- Stock transactions: `AddStockAsync`, `UseStockAsync`, `AdjustStockAsync`, `ScrapStockAsync`
|
|
||||||
- `GetTransactionHistoryAsync`, `RecalculateQuantityAsync`
|
|
||||||
|
|
||||||
### JobService
|
### JobService
|
||||||
- Job CRUD: `CreateAsync` (auto-generates JobNumber), `DuplicateAsync` (deep copy), `QuickCreateAsync`
|
- Job CRUD: `CreateAsync` (auto-generates JobNumber), `DuplicateAsync` (deep copy), `QuickCreateAsync`
|
||||||
@@ -167,7 +162,7 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
|
|
||||||
### CutListPackingService
|
### CutListPackingService
|
||||||
- `PackAsync(parts, kerfInches, jobStock?)` — runs optimization per material group
|
- `PackAsync(parts, kerfInches, jobStock?)` — runs optimization per material group
|
||||||
- Separates results into `InStockBins` (from inventory) and `ToBePurchasedBins`
|
- Separates results into `InStockBins` (from catalog-sourced job stock) and `ToBePurchasedBins`
|
||||||
- `GetSummary(result)` — calculates total bins, pieces, waste, efficiency %
|
- `GetSummary(result)` — calculates total bins, pieces, waste, efficiency %
|
||||||
- `SerializeResult(result)` / `LoadSavedResult(json)` — JSON round-trip via DTO layer (`SavedOptimizationResult` etc.)
|
- `SerializeResult(result)` / `LoadSavedResult(json)` — JSON round-trip via DTO layer (`SavedOptimizationResult` etc.)
|
||||||
|
|
||||||
@@ -188,7 +183,7 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
| `/jobs/{Id}` | Jobs/Edit | Tabbed editor (Details, Parts, Stock, Results); locked jobs show banner + disable editing |
|
| `/jobs/{Id}` | Jobs/Edit | Tabbed editor (Details, Parts, Stock, Results); locked jobs show banner + disable editing |
|
||||||
| `/materials` | Materials/Index | Material list with MaterialFilter, pagination |
|
| `/materials` | Materials/Index | Material list with MaterialFilter, pagination |
|
||||||
| `/materials/new`, `/materials/{Id}` | Materials/Edit | Material + dimension form (varies by shape) |
|
| `/materials/new`, `/materials/{Id}` | Materials/Edit | Material + dimension form (varies by shape) |
|
||||||
| `/stock` | Stock/Index | Stock items with MaterialFilter, quantity badges |
|
| `/stock` | Stock/Index | Stock items with MaterialFilter, pagination |
|
||||||
| `/stock/new`, `/stock/{Id}` | Stock/Edit | Stock item form |
|
| `/stock/new`, `/stock/{Id}` | Stock/Edit | Stock item form |
|
||||||
| `/tools` | Tools/Index | Cutting tools CRUD |
|
| `/tools` | Tools/Index | Cutting tools CRUD |
|
||||||
| `/Error` | Error | Unhandled exception page (registered via `UseExceptionHandler`) |
|
| `/Error` | Error | Unhandled exception page (registered via `UseExceptionHandler`) |
|
||||||
@@ -211,7 +206,7 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
- **ConfirmDialog** — All destructive actions use the shared `ConfirmDialog` component
|
- **ConfirmDialog** — All destructive actions use the shared `ConfirmDialog` component
|
||||||
- **Material selection flow** — Shape dropdown -> Size dropdown -> Length input -> Quantity (conditional dropdowns)
|
- **Material selection flow** — Shape dropdown -> Size dropdown -> Length input -> Quantity (conditional dropdowns)
|
||||||
- **Stock priority** — Lower number = used first; `-1` quantity = unlimited
|
- **Stock priority** — Lower number = used first; `-1` quantity = unlimited
|
||||||
- **Job stock** — Jobs can use auto-discovered inventory OR define custom stock lengths
|
- **Job stock** — Jobs must have stock explicitly configured (catalog-sourced `StockItem` rows or custom-length rows); there is no fallback to auto-discovered inventory
|
||||||
- **Optimization persistence** — Results saved as JSON in `Job.OptimizationResultJson`; DTO layer (`SavedOptimizationResult` etc.) handles serialization since Core types use encapsulated collections; results auto-cleared when parts, stock, or cutting tool change
|
- **Optimization persistence** — Results saved as JSON in `Job.OptimizationResultJson`; DTO layer (`SavedOptimizationResult` etc.) handles serialization since Core types use encapsulated collections; results auto-cleared when parts, stock, or cutting tool change
|
||||||
- **Job lock flow** — Optimize job -> Lock Job (manual action, available whether or not purchases are needed) -> job becomes read-only until Unlock
|
- **Job lock flow** — Optimize job -> Lock Job (manual action, available whether or not purchases are needed) -> job becomes read-only until Unlock
|
||||||
- **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications
|
- **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications
|
||||||
|
|||||||
@@ -70,14 +70,13 @@ public class ApiClient
|
|||||||
return await _http.GetFromJsonAsync<List<ApiStockItemDto>>(url) ?? [];
|
return await _http.GetFromJsonAsync<List<ApiStockItemDto>>(url) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ApiStockItemDto?> CreateStockItemAsync(int materialId, string length, string? name, int quantityOnHand, string? notes)
|
public async Task<ApiStockItemDto?> CreateStockItemAsync(int materialId, string length, string? name, string? notes)
|
||||||
{
|
{
|
||||||
var body = new
|
var body = new
|
||||||
{
|
{
|
||||||
MaterialId = materialId,
|
MaterialId = materialId,
|
||||||
Length = length,
|
Length = length,
|
||||||
Name = name,
|
Name = name,
|
||||||
QuantityOnHand = quantityOnHand,
|
|
||||||
Notes = notes
|
Notes = notes
|
||||||
};
|
};
|
||||||
var response = await _http.PostAsJsonAsync("api/stock-items", body);
|
var response = await _http.PostAsJsonAsync("api/stock-items", body);
|
||||||
@@ -360,7 +359,6 @@ public class ApiStockItemDto
|
|||||||
public decimal LengthInches { get; set; }
|
public decimal LengthInches { get; set; }
|
||||||
public string LengthFormatted { get; set; } = string.Empty;
|
public string LengthFormatted { get; set; } = string.Empty;
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ public class InventoryTools
|
|||||||
LengthInches = s.LengthInches,
|
LengthInches = s.LengthInches,
|
||||||
LengthFormatted = s.LengthFormatted,
|
LengthFormatted = s.LengthFormatted,
|
||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
QuantityOnHand = s.QuantityOnHand,
|
|
||||||
Notes = s.Notes,
|
Notes = s.Notes,
|
||||||
IsActive = s.IsActive
|
IsActive = s.IsActive
|
||||||
}).ToList()
|
}).ToList()
|
||||||
@@ -194,14 +193,12 @@ public class InventoryTools
|
|||||||
string length,
|
string length,
|
||||||
[Description("Optional name/label for this stock item")]
|
[Description("Optional name/label for this stock item")]
|
||||||
string? name = null,
|
string? name = null,
|
||||||
[Description("Initial quantity on hand (default 0)")]
|
|
||||||
int quantityOnHand = 0,
|
|
||||||
[Description("Notes")]
|
[Description("Notes")]
|
||||||
string? notes = null)
|
string? notes = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var stockItem = await _api.CreateStockItemAsync(materialId, length, name, quantityOnHand, notes);
|
var stockItem = await _api.CreateStockItemAsync(materialId, length, name, notes);
|
||||||
|
|
||||||
if (stockItem == null)
|
if (stockItem == null)
|
||||||
return new StockItemResult { Success = false, Error = "Failed to create stock item" };
|
return new StockItemResult { Success = false, Error = "Failed to create stock item" };
|
||||||
@@ -217,7 +214,6 @@ public class InventoryTools
|
|||||||
LengthInches = stockItem.LengthInches,
|
LengthInches = stockItem.LengthInches,
|
||||||
LengthFormatted = stockItem.LengthFormatted,
|
LengthFormatted = stockItem.LengthFormatted,
|
||||||
Name = stockItem.Name,
|
Name = stockItem.Name,
|
||||||
QuantityOnHand = stockItem.QuantityOnHand,
|
|
||||||
Notes = stockItem.Notes,
|
Notes = stockItem.Notes,
|
||||||
IsActive = stockItem.IsActive
|
IsActive = stockItem.IsActive
|
||||||
}
|
}
|
||||||
@@ -237,7 +233,7 @@ public class InventoryTools
|
|||||||
|
|
||||||
#region Convenience
|
#region Convenience
|
||||||
|
|
||||||
[McpServerTool(Name = "add_stock"), Description("Convenience method: adds a material (if needed) and a stock item (if needed) with an initial quantity, all in one call.")]
|
[McpServerTool(Name = "add_stock"), Description("Convenience method: adds a material (if needed) and a stock item (if needed), all in one call.")]
|
||||||
public async Task<AddStockResult> AddStock(
|
public async Task<AddStockResult> AddStock(
|
||||||
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
|
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
|
||||||
string shape,
|
string shape,
|
||||||
@@ -245,8 +241,6 @@ public class InventoryTools
|
|||||||
string size,
|
string size,
|
||||||
[Description("Stock length (e.g., '20'', '240')")]
|
[Description("Stock length (e.g., '20'', '240')")]
|
||||||
string length,
|
string length,
|
||||||
[Description("Quantity on hand (default 0)")]
|
|
||||||
int quantityOnHand = 0,
|
|
||||||
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
|
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
|
||||||
string type = "Steel",
|
string type = "Steel",
|
||||||
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
|
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
|
||||||
@@ -315,7 +309,7 @@ public class InventoryTools
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, quantityOnHand, null);
|
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, null);
|
||||||
stockItemCreated = true;
|
stockItemCreated = true;
|
||||||
}
|
}
|
||||||
catch (ApiConflictException)
|
catch (ApiConflictException)
|
||||||
@@ -351,8 +345,7 @@ public class InventoryTools
|
|||||||
MaterialCreated = materialCreated,
|
MaterialCreated = materialCreated,
|
||||||
StockItemId = stockItem.Id,
|
StockItemId = stockItem.Id,
|
||||||
StockItemCreated = stockItemCreated,
|
StockItemCreated = stockItemCreated,
|
||||||
LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
|
LengthFormatted = ArchUnits.FormatFromInches(lengthInches)
|
||||||
QuantityOnHand = stockItem.QuantityOnHand
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +512,6 @@ public class StockItemDto
|
|||||||
public decimal LengthInches { get; set; }
|
public decimal LengthInches { get; set; }
|
||||||
public string LengthFormatted { get; set; } = string.Empty;
|
public string LengthFormatted { get; set; } = string.Empty;
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
@@ -548,7 +540,6 @@ public class AddStockResult
|
|||||||
public int StockItemId { get; set; }
|
public int StockItemId { get; set; }
|
||||||
public bool StockItemCreated { get; set; }
|
public bool StockItemCreated { get; set; }
|
||||||
public string LengthFormatted { get; set; } = string.Empty;
|
public string LengthFormatted { get; set; } = string.Empty;
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -257,9 +257,9 @@ public class JobTools
|
|||||||
string length,
|
string length,
|
||||||
[Description("Quantity available (-1 for unlimited, default -1)")]
|
[Description("Quantity available (-1 for unlimited, default -1)")]
|
||||||
int quantity = -1,
|
int quantity = -1,
|
||||||
[Description("Stock item ID from inventory (optional - links to tracked inventory)")]
|
[Description("Stock item ID from the stock catalog (optional - links to a specific catalog stock item)")]
|
||||||
int? stockItemId = null,
|
int? stockItemId = null,
|
||||||
[Description("True if this is a custom length not from inventory (default false)")]
|
[Description("True if this is a custom length not sourced from the stock catalog (default false)")]
|
||||||
bool isCustomLength = false,
|
bool isCustomLength = false,
|
||||||
[Description("Priority - lower number = used first (default 10)")]
|
[Description("Priority - lower number = used first (default 10)")]
|
||||||
int priority = 10)
|
int priority = 10)
|
||||||
@@ -304,7 +304,7 @@ public class JobTools
|
|||||||
|
|
||||||
#region Optimization
|
#region Optimization
|
||||||
|
|
||||||
[McpServerTool(Name = "optimize_job"), Description("Runs bin packing optimization on a job. The job must have parts defined. If stock is defined, it will be used; otherwise the optimizer uses available inventory. Returns optimized cut layouts per material with efficiency stats.")]
|
[McpServerTool(Name = "optimize_job"), Description("Runs bin packing optimization on a job. The job must have parts defined, and stock must be explicitly configured on the job (via add_job_stock) for each material used by its parts - there is no fallback to inventory; parts with no matching stock configured come back as items not placed. Returns optimized cut layouts per material with efficiency stats.")]
|
||||||
public async Task<OptimizeJobResult> OptimizeJob(
|
public async Task<OptimizeJobResult> OptimizeJob(
|
||||||
[Description("Job ID")]
|
[Description("Job ID")]
|
||||||
int jobId,
|
int jobId,
|
||||||
|
|||||||
@@ -276,7 +276,6 @@ else
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40px;"></th>
|
<th style="width: 40px;"></th>
|
||||||
<th>Length</th>
|
<th>Length</th>
|
||||||
<th>On Hand</th>
|
|
||||||
<th style="width: 120px;">Qty to Use</th>
|
<th style="width: 120px;">Qty to Use</th>
|
||||||
<th style="width: 100px;">Priority</th>
|
<th style="width: 100px;">Priority</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -289,7 +288,6 @@ else
|
|||||||
<input type="checkbox" class="form-check-input" @bind="candidate.Selected" />
|
<input type="checkbox" class="form-check-input" @bind="candidate.Selected" />
|
||||||
</td>
|
</td>
|
||||||
<td>@ArchUnits.FormatFromInches((double)candidate.StockItem.LengthInches)</td>
|
<td>@ArchUnits.FormatFromInches((double)candidate.StockItem.LengthInches)</td>
|
||||||
<td>@candidate.StockItem.QuantityOnHand</td>
|
|
||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm" @bind="candidate.Quantity"
|
<input type="number" class="form-control form-control-sm" @bind="candidate.Quantity"
|
||||||
min="-1" disabled="@(!candidate.Selected)" />
|
min="-1" disabled="@(!candidate.Selected)" />
|
||||||
@@ -810,13 +808,14 @@ else
|
|||||||
<option value="">-- Select --</option>
|
<option value="">-- Select --</option>
|
||||||
@foreach (var stock in availableStockItems)
|
@foreach (var stock in availableStockItems)
|
||||||
{
|
{
|
||||||
<option value="@stock.Id">@ArchUnits.FormatFromInches((double)stock.LengthInches) (@stock.QuantityOnHand available)</option>
|
<option value="@stock.Id">@ArchUnits.FormatFromInches((double)stock.LengthInches)</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Qty to Use</label>
|
<label class="form-label">Qty to Use</label>
|
||||||
<input type="number" class="form-control" @bind="newStock.Quantity" min="1" />
|
<input type="number" class="form-control" @bind="newStock.Quantity" min="-1" />
|
||||||
|
<small class="text-muted">-1 = unlimited</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3 mt-1">
|
<div class="row g-3 mt-1">
|
||||||
@@ -870,7 +869,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Quantity</label>
|
<label class="form-label">Quantity</label>
|
||||||
<input type="number" class="form-control" @bind="newStock.Quantity" min="1" />
|
<input type="number" class="form-control" @bind="newStock.Quantity" min="-1" />
|
||||||
<small class="text-muted">Use -1 for unlimited</small>
|
<small class="text-muted">Use -1 for unlimited</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1125,7 +1124,7 @@ else
|
|||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> —
|
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> —
|
||||||
No stock lengths available or parts too long.
|
not enough stock quantity entered for this job, no stock lengths configured, or parts too long.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,9 +1320,9 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newStock.Quantity < 1)
|
if (newStock.Quantity < -1 || newStock.Quantity == 0)
|
||||||
{
|
{
|
||||||
stockErrorMessage = "Quantity must be at least 1";
|
stockErrorMessage = "Quantity must be at least 1 (or -1 for unlimited)";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,94 +70,6 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!IsNew)
|
|
||||||
{
|
|
||||||
<div class="col-lg-6 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
Inventory
|
|
||||||
<span class="badge @(stockItem.QuantityOnHand > 0 ? "bg-success" : "bg-secondary") ms-2">@stockItem.QuantityOnHand on hand</span>
|
|
||||||
</h5>
|
|
||||||
<button class="btn btn-sm btn-primary" @onclick="ShowStockForm">Add/Adjust Stock</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if (showStockForm)
|
|
||||||
{
|
|
||||||
<div class="border rounded p-3 mb-3 bg-light">
|
|
||||||
<h6>Stock Transaction</h6>
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Type</label>
|
|
||||||
<select class="form-select" @bind="stockTransactionType">
|
|
||||||
<option value="add">Receive Stock</option>
|
|
||||||
<option value="adjust">Set Quantity</option>
|
|
||||||
<option value="scrap">Scrap/Waste</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">@(stockTransactionType == "adjust" ? "New Quantity" : "Quantity")</label>
|
|
||||||
<input type="number" class="form-control" @bind="stockQuantity" min="0" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Notes</label>
|
|
||||||
<InputText class="form-control" @bind-Value="stockNotes" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (!string.IsNullOrEmpty(stockFormErrorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger mt-2 mb-0">@stockFormErrorMessage</div>
|
|
||||||
}
|
|
||||||
<div class="mt-3 d-flex gap-2">
|
|
||||||
<button class="btn btn-primary btn-sm" @onclick="SaveStockTransactionAsync" disabled="@savingStockTransaction">
|
|
||||||
@if (savingStockTransaction)
|
|
||||||
{
|
|
||||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
|
||||||
}
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelStockForm">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (transactions.Count == 0)
|
|
||||||
{
|
|
||||||
<p class="text-muted">No transaction history yet.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Qty</th>
|
|
||||||
<th>Notes</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var txn in transactions)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@txn.CreatedAt.ToLocalTime().ToString("MM/dd/yy HH:mm")</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge @GetTransactionBadgeClass(txn.Type)">@txn.Type</span>
|
|
||||||
</td>
|
|
||||||
<td class="@(txn.Quantity >= 0 ? "text-success" : "text-danger")">
|
|
||||||
@(txn.Quantity >= 0 ? "+" : "")@txn.Quantity
|
|
||||||
</td>
|
|
||||||
<td>@(txn.Notes ?? "-")</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,19 +79,10 @@ else
|
|||||||
|
|
||||||
private StockItem stockItem = new();
|
private StockItem stockItem = new();
|
||||||
private List<Material> materials = new();
|
private List<Material> materials = new();
|
||||||
private List<StockTransaction> transactions = new();
|
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
private bool saving;
|
private bool saving;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
|
|
||||||
// Stock transaction form
|
|
||||||
private bool showStockForm;
|
|
||||||
private bool savingStockTransaction;
|
|
||||||
private string stockTransactionType = "add";
|
|
||||||
private int stockQuantity;
|
|
||||||
private string? stockNotes;
|
|
||||||
private string? stockFormErrorMessage;
|
|
||||||
|
|
||||||
private bool IsNew => !Id.HasValue;
|
private bool IsNew => !Id.HasValue;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -195,83 +98,10 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stockItem = existing;
|
stockItem = existing;
|
||||||
transactions = await StockItemService.GetTransactionHistoryAsync(Id.Value, 20);
|
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTransactionBadgeClass(StockTransactionType type) => type switch
|
|
||||||
{
|
|
||||||
StockTransactionType.Received => "bg-success",
|
|
||||||
StockTransactionType.Used => "bg-primary",
|
|
||||||
StockTransactionType.Adjustment => "bg-warning text-dark",
|
|
||||||
StockTransactionType.Scrapped => "bg-danger",
|
|
||||||
StockTransactionType.Returned => "bg-info",
|
|
||||||
_ => "bg-secondary"
|
|
||||||
};
|
|
||||||
|
|
||||||
private void ShowStockForm()
|
|
||||||
{
|
|
||||||
stockTransactionType = "add";
|
|
||||||
stockQuantity = 0;
|
|
||||||
stockNotes = null;
|
|
||||||
stockFormErrorMessage = null;
|
|
||||||
showStockForm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelStockForm()
|
|
||||||
{
|
|
||||||
showStockForm = false;
|
|
||||||
stockFormErrorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveStockTransactionAsync()
|
|
||||||
{
|
|
||||||
stockFormErrorMessage = null;
|
|
||||||
savingStockTransaction = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (stockQuantity <= 0 && stockTransactionType != "adjust")
|
|
||||||
{
|
|
||||||
stockFormErrorMessage = "Quantity must be greater than zero";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stockTransactionType == "adjust" && stockQuantity < 0)
|
|
||||||
{
|
|
||||||
stockFormErrorMessage = "Quantity cannot be negative";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (stockTransactionType)
|
|
||||||
{
|
|
||||||
case "add":
|
|
||||||
await StockItemService.AddStockAsync(Id!.Value, stockQuantity, stockNotes);
|
|
||||||
break;
|
|
||||||
case "adjust":
|
|
||||||
await StockItemService.AdjustStockAsync(Id!.Value, stockQuantity, stockNotes);
|
|
||||||
break;
|
|
||||||
case "scrap":
|
|
||||||
await StockItemService.ScrapStockAsync(Id!.Value, stockQuantity, stockNotes);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh
|
|
||||||
var updated = await StockItemService.GetByIdAsync(Id!.Value);
|
|
||||||
if (updated != null)
|
|
||||||
{
|
|
||||||
stockItem = updated;
|
|
||||||
}
|
|
||||||
transactions = await StockItemService.GetTransactionHistoryAsync(Id!.Value, 20);
|
|
||||||
showStockForm = false;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
savingStockTransaction = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveStockItemAsync()
|
private async Task SaveStockItemAsync()
|
||||||
{
|
{
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
Stock items represent the specific lengths of material you have available for cutting. Each stock item links
|
Stock items represent the specific lengths of material you can cut from. Add the lengths you typically
|
||||||
a material to a length and tracks how many pieces you have on hand.
|
work with here, then pick from them when adding stock to a job.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (loading)
|
@if (loading)
|
||||||
@@ -46,7 +46,6 @@ else
|
|||||||
<th>Grade</th>
|
<th>Grade</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>Length</th>
|
<th>Length</th>
|
||||||
<th>On Hand</th>
|
|
||||||
<th style="width: 100px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -59,16 +58,6 @@ else
|
|||||||
<td>@item.Material.Grade</td>
|
<td>@item.Material.Grade</td>
|
||||||
<td>@item.Material.Size</td>
|
<td>@item.Material.Size</td>
|
||||||
<td>@ArchUnits.FormatFromInches((double)item.LengthInches)</td>
|
<td>@ArchUnits.FormatFromInches((double)item.LengthInches)</td>
|
||||||
<td>
|
|
||||||
@if (item.QuantityOnHand > 0)
|
|
||||||
{
|
|
||||||
<span class="badge bg-success">@item.QuantityOnHand</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="badge bg-secondary">0</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex gap-1">
|
<div class="d-flex gap-1">
|
||||||
<a href="stock/@item.Id" class="btn btn-sm btn-outline-primary" title="Edit"><i class="bi bi-pencil"></i></a>
|
<a href="stock/@item.Id" class="btn btn-sm btn-outline-primary" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||||
|
|||||||
@@ -310,7 +310,6 @@ public class JobsController : ControllerBase
|
|||||||
LengthInches = s.LengthInches,
|
LengthInches = s.LengthInches,
|
||||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
QuantityOnHand = s.QuantityOnHand,
|
|
||||||
IsActive = s.IsActive
|
IsActive = s.IsActive
|
||||||
}).ToList());
|
}).ToList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ public class StockItemsController : ControllerBase
|
|||||||
MaterialId = dto.MaterialId,
|
MaterialId = dto.MaterialId,
|
||||||
LengthInches = (decimal)lengthInches,
|
LengthInches = (decimal)lengthInches,
|
||||||
Name = dto.Name,
|
Name = dto.Name,
|
||||||
QuantityOnHand = dto.QuantityOnHand,
|
|
||||||
Notes = dto.Notes
|
Notes = dto.Notes
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,87 +117,6 @@ public class StockItemsController : ControllerBase
|
|||||||
return Ok(items.Select(MapToDto).ToList());
|
return Ok(items.Select(MapToDto).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/transactions")]
|
|
||||||
public async Task<ActionResult<List<StockTransactionDto>>> GetTransactions(int id, [FromQuery] int? limit = null)
|
|
||||||
{
|
|
||||||
var item = await _stockItemService.GetByIdAsync(id);
|
|
||||||
if (item == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var transactions = await _stockItemService.GetTransactionHistoryAsync(id, limit);
|
|
||||||
return Ok(transactions.Select(MapTransactionToDto).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/receive")]
|
|
||||||
public async Task<ActionResult<StockTransactionDto>> ReceiveStock(int id, AddStockDto dto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var transaction = await _stockItemService.AddStockAsync(id, dto.Quantity, dto.Notes);
|
|
||||||
return Ok(MapTransactionToDto(transaction));
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/use")]
|
|
||||||
public async Task<ActionResult<StockTransactionDto>> UseStock(int id, UseStockDto dto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var transaction = await _stockItemService.UseStockAsync(id, dto.Quantity, dto.JobId, dto.Notes);
|
|
||||||
return Ok(MapTransactionToDto(transaction));
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/adjust")]
|
|
||||||
public async Task<ActionResult<StockTransactionDto>> AdjustStock(int id, AdjustStockDto dto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var transaction = await _stockItemService.AdjustStockAsync(id, dto.NewQuantity, dto.Notes);
|
|
||||||
return Ok(MapTransactionToDto(transaction));
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/scrap")]
|
|
||||||
public async Task<ActionResult<StockTransactionDto>> ScrapStock(int id, ScrapStockDto dto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var transaction = await _stockItemService.ScrapStockAsync(id, dto.Quantity, dto.Notes);
|
|
||||||
return Ok(MapTransactionToDto(transaction));
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/recalculate")]
|
|
||||||
public async Task<ActionResult<object>> RecalculateStock(int id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newQuantity = await _stockItemService.RecalculateQuantityAsync(id);
|
|
||||||
return Ok(new { QuantityOnHand = newQuantity });
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static StockItemDto MapToDto(StockItem s) => new()
|
private static StockItemDto MapToDto(StockItem s) => new()
|
||||||
{
|
{
|
||||||
Id = s.Id,
|
Id = s.Id,
|
||||||
@@ -207,20 +125,7 @@ public class StockItemsController : ControllerBase
|
|||||||
LengthInches = s.LengthInches,
|
LengthInches = s.LengthInches,
|
||||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
QuantityOnHand = s.QuantityOnHand,
|
|
||||||
Notes = s.Notes,
|
Notes = s.Notes,
|
||||||
IsActive = s.IsActive
|
IsActive = s.IsActive
|
||||||
};
|
};
|
||||||
|
|
||||||
private static StockTransactionDto MapTransactionToDto(StockTransaction t) => new()
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
StockItemId = t.StockItemId,
|
|
||||||
Quantity = t.Quantity,
|
|
||||||
Type = t.Type.ToString(),
|
|
||||||
JobId = t.JobId,
|
|
||||||
JobNumber = t.Job?.JobNumber,
|
|
||||||
Notes = t.Notes,
|
|
||||||
CreatedAt = t.CreatedAt
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ public class CatalogStockItemDto
|
|||||||
{
|
{
|
||||||
public decimal LengthInches { get; set; }
|
public decimal LengthInches { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ public class StockItemDto
|
|||||||
public decimal LengthInches { get; set; }
|
public decimal LengthInches { get; set; }
|
||||||
public string LengthFormatted { get; set; } = string.Empty;
|
public string LengthFormatted { get; set; } = string.Empty;
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
@@ -18,7 +17,6 @@ public class CreateStockItemDto
|
|||||||
public int MaterialId { get; set; }
|
public int MaterialId { get; set; }
|
||||||
public string Length { get; set; } = string.Empty;
|
public string Length { get; set; } = string.Empty;
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,40 +26,3 @@ public class UpdateStockItemDto
|
|||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StockTransactionDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public string Type { get; set; } = string.Empty;
|
|
||||||
public int? JobId { get; set; }
|
|
||||||
public string? JobNumber { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AddStockDto
|
|
||||||
{
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UseStockDto
|
|
||||||
{
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public int? JobId { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AdjustStockDto
|
|
||||||
{
|
|
||||||
public int NewQuantity { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScrapStockDto
|
|
||||||
{
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ public class ApplicationDbContext : DbContext
|
|||||||
public DbSet<Material> Materials => Set<Material>();
|
public DbSet<Material> Materials => Set<Material>();
|
||||||
public DbSet<MaterialDimensions> MaterialDimensions => Set<MaterialDimensions>();
|
public DbSet<MaterialDimensions> MaterialDimensions => Set<MaterialDimensions>();
|
||||||
public DbSet<StockItem> StockItems => Set<StockItem>();
|
public DbSet<StockItem> StockItems => Set<StockItem>();
|
||||||
public DbSet<StockTransaction> StockTransactions => Set<StockTransaction>();
|
|
||||||
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
|
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
|
||||||
public DbSet<Job> Jobs => Set<Job>();
|
public DbSet<Job> Jobs => Set<Job>();
|
||||||
public DbSet<JobPart> JobParts => Set<JobPart>();
|
public DbSet<JobPart> JobParts => Set<JobPart>();
|
||||||
@@ -157,24 +156,6 @@ public class ApplicationDbContext : DbContext
|
|||||||
entity.HasIndex(e => new { e.MaterialId, e.LengthInches }).IsUnique();
|
entity.HasIndex(e => new { e.MaterialId, e.LengthInches }).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
// StockTransaction
|
|
||||||
modelBuilder.Entity<StockTransaction>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
entity.Property(e => e.Notes).HasMaxLength(500);
|
|
||||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
|
||||||
|
|
||||||
entity.HasOne(e => e.StockItem)
|
|
||||||
.WithMany(s => s.Transactions)
|
|
||||||
.HasForeignKey(e => e.StockItemId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.Job)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(e => e.JobId)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
// CuttingTool
|
// CuttingTool
|
||||||
modelBuilder.Entity<CuttingTool>(entity =>
|
modelBuilder.Entity<CuttingTool>(entity =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ public class StockItem
|
|||||||
public int MaterialId { get; set; }
|
public int MaterialId { get; set; }
|
||||||
public decimal LengthInches { get; set; }
|
public decimal LengthInches { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; } = 0;
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
public Material Material { get; set; } = null!;
|
public Material Material { get; set; } = null!;
|
||||||
public ICollection<StockTransaction> Transactions { get; set; } = new List<StockTransaction>();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public class StockTransaction
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public StockTransactionType Type { get; set; }
|
|
||||||
public int? JobId { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
public StockItem StockItem { get; set; } = null!;
|
|
||||||
public Job? Job { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum StockTransactionType
|
|
||||||
{
|
|
||||||
Received,
|
|
||||||
Used,
|
|
||||||
Adjustment,
|
|
||||||
Scrapped,
|
|
||||||
Returned
|
|
||||||
}
|
|
||||||
+620
@@ -0,0 +1,620 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CutList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260801145943_RemoveInventoryQuantityTracking")]
|
||||||
|
partial class RemoveInventoryQuantityTracking
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.HasSequence("MaterialDimensionsSequence");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("KerfInches")
|
||||||
|
.HasPrecision(6, 4)
|
||||||
|
.HasColumnType("decimal(6,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("CuttingTools");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = true,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Bandsaw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.125m,
|
||||||
|
Name = "Chop Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Cold Cut Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Hacksaw"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Customer")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("CuttingToolId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("JobNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LockedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("OptimizationResultJson")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("OptimizedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CuttingToolId");
|
||||||
|
|
||||||
|
b.HasIndex("JobNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.ToTable("JobParts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsCustomLength")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.ToTable("JobStocks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Grade")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Shape")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Size")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Materials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable((string)null);
|
||||||
|
|
||||||
|
b.UseTpcMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId", "LengthInches")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Leg1")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Leg2")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Thickness")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Leg1");
|
||||||
|
|
||||||
|
b.ToTable("DimAngle", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Flange")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Web")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.ToTable("DimChannel", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Thickness")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.ToTable("DimFlatBar", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WeightPerFoot")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.ToTable("DimIBeam", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("NominalSize")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Schedule")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("NominalSize");
|
||||||
|
|
||||||
|
b.ToTable("DimPipe", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.ToTable("DimRectangularTube", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Diameter")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Diameter");
|
||||||
|
|
||||||
|
b.ToTable("DimRoundBar", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("OuterDiameter")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("OuterDiameter");
|
||||||
|
|
||||||
|
b.ToTable("DimRoundTube", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.ToTable("DimSquareBar", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.ToTable("DimSquareTube", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||||
|
.WithMany("Jobs")
|
||||||
|
.HasForeignKey("CuttingToolId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("CuttingTool");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Parts")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("JobParts")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Stock")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithOne("Dimensions")
|
||||||
|
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("StockItems")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Parts");
|
||||||
|
|
||||||
|
b.Navigation("Stock");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Dimensions");
|
||||||
|
|
||||||
|
b.Navigation("JobParts");
|
||||||
|
|
||||||
|
b.Navigation("StockItems");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveInventoryQuantityTracking : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "StockTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "QuantityOnHand",
|
||||||
|
table: "StockItems");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "QuantityOnHand",
|
||||||
|
table: "StockItems",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "StockTransactions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
JobId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
StockItemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Type = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_StockTransactions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_StockTransactions_Jobs_JobId",
|
||||||
|
column: x => x.JobId,
|
||||||
|
principalTable: "Jobs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_StockTransactions_StockItems_StockItemId",
|
||||||
|
column: x => x.StockItemId,
|
||||||
|
principalTable: "StockItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_StockTransactions_JobId",
|
||||||
|
table: "StockTransactions",
|
||||||
|
column: "JobId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_StockTransactions_StockItemId",
|
||||||
|
table: "StockTransactions",
|
||||||
|
column: "StockItemId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -325,9 +325,6 @@ namespace CutList.Web.Migrations
|
|||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
.HasColumnType("nvarchar(255)");
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
b.Property<int>("QuantityOnHand")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
@@ -339,44 +336,6 @@ namespace CutList.Web.Migrations
|
|||||||
b.ToTable("StockItems");
|
b.ToTable("StockItems");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasDefaultValueSql("GETUTCDATE()");
|
|
||||||
|
|
||||||
b.Property<int?>("JobId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("nvarchar(500)");
|
|
||||||
|
|
||||||
b.Property<int>("Quantity")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("StockItemId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("Type")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("StockItemId");
|
|
||||||
|
|
||||||
b.ToTable("StockTransactions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
@@ -632,24 +591,6 @@ namespace CutList.Web.Migrations
|
|||||||
b.Navigation("Material");
|
b.Navigation("Material");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
|
||||||
.WithMany("Transactions")
|
|
||||||
.HasForeignKey("StockItemId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
|
|
||||||
b.Navigation("StockItem");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Jobs");
|
b.Navigation("Jobs");
|
||||||
@@ -670,11 +611,6 @@ namespace CutList.Web.Migrations
|
|||||||
|
|
||||||
b.Navigation("StockItems");
|
b.Navigation("StockItems");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Transactions");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,7 +362,6 @@ public class CatalogService
|
|||||||
MaterialId = material.Id,
|
MaterialId = material.Id,
|
||||||
LengthInches = dto.LengthInches,
|
LengthInches = dto.LengthInches,
|
||||||
Name = dto.Name,
|
Name = dto.Name,
|
||||||
QuantityOnHand = dto.QuantityOnHand,
|
|
||||||
Notes = dto.Notes,
|
Notes = dto.Notes,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
@@ -388,7 +387,6 @@ public class CatalogService
|
|||||||
{
|
{
|
||||||
LengthInches = s.LengthInches,
|
LengthInches = s.LengthInches,
|
||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
QuantityOnHand = s.QuantityOnHand,
|
|
||||||
Notes = s.Notes
|
Notes = s.Notes
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ public class CutListPackingService
|
|||||||
// Build stock bins
|
// Build stock bins
|
||||||
var stockBins = new List<StockBinSource>();
|
var stockBins = new List<StockBinSource>();
|
||||||
|
|
||||||
// Check if job has specific stock configured for this material
|
// Use job-specific stock configuration. Jobs must have stock explicitly configured -
|
||||||
if (jobStockByMaterial.TryGetValue(materialId, out var materialJobStock) && materialJobStock.Count > 0)
|
// there is no fallback to "whatever's in inventory."
|
||||||
|
if (jobStockByMaterial.TryGetValue(materialId, out var materialJobStock))
|
||||||
{
|
{
|
||||||
// Use job-specific stock configuration
|
|
||||||
foreach (var stock in materialJobStock.OrderBy(s => s.Priority))
|
foreach (var stock in materialJobStock.OrderBy(s => s.Priority))
|
||||||
{
|
{
|
||||||
stockBins.Add(new StockBinSource
|
stockBins.Add(new StockBinSource
|
||||||
@@ -61,37 +61,6 @@ public class CutListPackingService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// No job-specific stock - use all available stock items for this material
|
|
||||||
var stockItems = await context.StockItems
|
|
||||||
.Where(s => s.MaterialId == materialId && s.IsActive)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var stock in stockItems)
|
|
||||||
{
|
|
||||||
if (stock.QuantityOnHand > 0)
|
|
||||||
{
|
|
||||||
// In-stock with finite quantity
|
|
||||||
stockBins.Add(new StockBinSource
|
|
||||||
{
|
|
||||||
LengthInches = stock.LengthInches,
|
|
||||||
Quantity = stock.QuantityOnHand,
|
|
||||||
Priority = 1,
|
|
||||||
IsInStock = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always add as purchasable (unlimited) - algorithm will use in-stock first due to priority
|
|
||||||
stockBins.Add(new StockBinSource
|
|
||||||
{
|
|
||||||
LengthInches = stock.LengthInches,
|
|
||||||
Quantity = -1, // unlimited
|
|
||||||
Priority = 2,
|
|
||||||
IsInStock = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stockBins.Count == 0)
|
if (stockBins.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -139,7 +108,14 @@ public class CutListPackingService
|
|||||||
var inStockBins = new List<Bin>();
|
var inStockBins = new List<Bin>();
|
||||||
var toBePurchasedBins = new List<Bin>();
|
var toBePurchasedBins = new List<Bin>();
|
||||||
|
|
||||||
// Track remaining in-stock quantities from the stock bins we configured
|
// Catalog-sourced stock rows with an unlimited (-1) quantity are always in-stock,
|
||||||
|
// regardless of how many bins of that length get packed.
|
||||||
|
var unlimitedInStockLengths = stockBins
|
||||||
|
.Where(s => s.IsInStock && s.Quantity == -1)
|
||||||
|
.Select(s => s.LengthInches)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
// Track remaining in-stock quantities from the finite-quantity catalog stock bins we configured
|
||||||
var remainingStock = stockBins
|
var remainingStock = stockBins
|
||||||
.Where(s => s.IsInStock && s.Quantity > 0)
|
.Where(s => s.IsInStock && s.Quantity > 0)
|
||||||
.GroupBy(s => s.LengthInches)
|
.GroupBy(s => s.LengthInches)
|
||||||
@@ -149,9 +125,14 @@ public class CutListPackingService
|
|||||||
{
|
{
|
||||||
var binLength = (decimal)bin.Length;
|
var binLength = (decimal)bin.Length;
|
||||||
|
|
||||||
// Check if this can come from in-stock
|
if (unlimitedInStockLengths.Contains(binLength))
|
||||||
if (remainingStock.TryGetValue(binLength, out var remaining) && remaining > 0)
|
|
||||||
{
|
{
|
||||||
|
// Unlimited catalog-sourced stock - always in-stock
|
||||||
|
inStockBins.Add(bin);
|
||||||
|
}
|
||||||
|
else if (remainingStock.TryGetValue(binLength, out var remaining) && remaining > 0)
|
||||||
|
{
|
||||||
|
// Check if this can come from in-stock
|
||||||
inStockBins.Add(bin);
|
inStockBins.Add(bin);
|
||||||
remainingStock[binLength] = remaining - 1;
|
remainingStock[binLength] = remaining - 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,147 +108,4 @@ public class StockItemService
|
|||||||
|
|
||||||
return await query.AnyAsync();
|
return await query.AnyAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stock transaction methods
|
|
||||||
public async Task<StockTransaction> AddStockAsync(int stockItemId, int quantity, string? notes = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var stockItem = await context.StockItems.FindAsync(stockItemId)
|
|
||||||
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
|
|
||||||
|
|
||||||
var transaction = new StockTransaction
|
|
||||||
{
|
|
||||||
StockItemId = stockItemId,
|
|
||||||
Quantity = quantity,
|
|
||||||
Type = StockTransactionType.Received,
|
|
||||||
Notes = notes,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
stockItem.QuantityOnHand += quantity;
|
|
||||||
stockItem.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
context.StockTransactions.Add(transaction);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StockTransaction> UseStockAsync(int stockItemId, int quantity, int? jobId = null, string? notes = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var stockItem = await context.StockItems.FindAsync(stockItemId)
|
|
||||||
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
|
|
||||||
|
|
||||||
var transaction = new StockTransaction
|
|
||||||
{
|
|
||||||
StockItemId = stockItemId,
|
|
||||||
Quantity = -quantity,
|
|
||||||
Type = StockTransactionType.Used,
|
|
||||||
JobId = jobId,
|
|
||||||
Notes = notes,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
stockItem.QuantityOnHand -= quantity;
|
|
||||||
stockItem.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
context.StockTransactions.Add(transaction);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StockTransaction> AdjustStockAsync(int stockItemId, int newQuantity, string? notes = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var stockItem = await context.StockItems.FindAsync(stockItemId)
|
|
||||||
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
|
|
||||||
|
|
||||||
var difference = newQuantity - stockItem.QuantityOnHand;
|
|
||||||
|
|
||||||
var transaction = new StockTransaction
|
|
||||||
{
|
|
||||||
StockItemId = stockItemId,
|
|
||||||
Quantity = difference,
|
|
||||||
Type = StockTransactionType.Adjustment,
|
|
||||||
Notes = notes ?? "Manual adjustment",
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
stockItem.QuantityOnHand = newQuantity;
|
|
||||||
stockItem.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
context.StockTransactions.Add(transaction);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StockTransaction> ScrapStockAsync(int stockItemId, int quantity, string? notes = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var stockItem = await context.StockItems.FindAsync(stockItemId)
|
|
||||||
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
|
|
||||||
|
|
||||||
var transaction = new StockTransaction
|
|
||||||
{
|
|
||||||
StockItemId = stockItemId,
|
|
||||||
Quantity = -quantity,
|
|
||||||
Type = StockTransactionType.Scrapped,
|
|
||||||
Notes = notes,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
stockItem.QuantityOnHand -= quantity;
|
|
||||||
stockItem.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
context.StockTransactions.Add(transaction);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<StockTransaction>> GetTransactionHistoryAsync(int stockItemId, int? limit = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var query = context.StockTransactions
|
|
||||||
.Include(t => t.Job)
|
|
||||||
.Where(t => t.StockItemId == stockItemId)
|
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
if (limit.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Take(limit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> RecalculateQuantityAsync(int stockItemId)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var stockItem = await context.StockItems.FindAsync(stockItemId)
|
|
||||||
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
|
|
||||||
|
|
||||||
var calculatedQuantity = await context.StockTransactions
|
|
||||||
.Where(t => t.StockItemId == stockItemId)
|
|
||||||
.SumAsync(t => t.Quantity);
|
|
||||||
|
|
||||||
if (stockItem.QuantityOnHand != calculatedQuantity)
|
|
||||||
{
|
|
||||||
stockItem.QuantityOnHand = calculatedQuantity;
|
|
||||||
stockItem.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return calculatedQuantity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Remove inventory quantity tracking from CutList
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
`StockItem.QuantityOnHand` and its `StockTransaction` ledger (Received/Used/Adjustment/Scrapped/Returned) exist today, but nothing in the job workflow reads or writes them automatically:
|
||||||
|
|
||||||
|
- Adding a stock item to a job (`JobStock`) never checks or reserves `QuantityOnHand` — a user can put `Quantity = 50` on a `JobStock` row pointing at a `StockItem` with 3 on hand and nothing complains.
|
||||||
|
- Locking a job (`JobService.LockAsync`) only stamps `LockedAt` — no transaction is created, `QuantityOnHand` doesn't move.
|
||||||
|
- The only place `QuantityOnHand` has any real effect is `CutListPackingService.PackAsync`'s auto-discovery fallback: when a job has *no* `JobStock` rows configured for a material, the packer pulls all active `StockItem`s for that material, treats the first `QuantityOnHand` bars as free ("in stock"), and always adds an *additional unlimited* bin on top for "to be purchased" — i.e. it silently assumes more can always be bought.
|
||||||
|
- The only way `QuantityOnHand` actually changes is a fully separate, fully manual flow (`StockItemsController` receive/use/adjust/scrap, driven from the `/stock/{id}` page) that nothing in the Jobs UI ever triggers.
|
||||||
|
|
||||||
|
Net effect: inventory quantity is a second, disconnected bookkeeping system that the job workflow neither respects nor maintains, and the auto-discovery fallback actively assumes unlimited purchasing beyond whatever quantity happens to be tracked. Decision: remove quantity tracking entirely. A job's stock should be exactly what the user explicitly enters for that job — nothing assumed, nothing silently topped up — and the user should never need to think about a separate "inventory" system to enter parts, stock, and get results.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Data model
|
||||||
|
|
||||||
|
- `StockItem` drops `QuantityOnHand`. Keeps `MaterialId`, `LengthInches`, `Name`, `Notes`, `IsActive`, `CreatedAt`/`UpdatedAt` — it becomes a pure catalog of known lengths per material (still used to populate the "pick a standard length" dropdown when adding stock to a job).
|
||||||
|
- `StockTransaction` entity and `StockTransactionType` enum: **deleted entirely**, along with the `ApplicationDbContext.StockTransactions` `DbSet` and its EF configuration.
|
||||||
|
- `JobStock` is unchanged in shape. Its `Quantity` becomes the sole source of truth for how many bars are available to a job everywhere it's used: a finite number is a hard ceiling (parts beyond it land in `ItemsNotPlaced`, nothing is silently treated as purchasable), and `-1` (unlimited) only applies when the user explicitly picks it. This "unlimited" option is extended to catalog-sourced `JobStock` rows, which today only allow it for custom-length rows.
|
||||||
|
|
||||||
|
### Services / API
|
||||||
|
|
||||||
|
- `StockItemService` drops `AddStockAsync`, `UseStockAsync`, `AdjustStockAsync`, `ScrapStockAsync`, `GetTransactionHistoryAsync`, `RecalculateQuantityAsync`. Keeps `GetAllAsync`, `GetByMaterialAsync`, `GetByIdAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync`, `ExistsAsync`.
|
||||||
|
- `StockItemsController` drops `POST /{id}/receive`, `POST /{id}/use`, `POST /{id}/adjust`, `POST /{id}/scrap`, `POST /{id}/recalculate`, `GET /{id}/transactions`. Keeps list/get/create/update/delete/by-material.
|
||||||
|
- `StockItemDto` drops `QuantityOnHand`; `CreateStockItemDto` drops `QuantityOnHand`. `StockTransactionDto`, `AddStockDto`, `UseStockDto`, `AdjustStockDto`, `ScrapStockDto` are deleted.
|
||||||
|
- `CutListPackingService.PackAsync` drops the auto-discovery fallback branch entirely (the "no job-specific stock configured" path that reads `QuantityOnHand` and adds an unlimited purchasable bin). A material with no `JobStock` rows configured packs zero stock bins, so all of its parts land in `ItemsNotPlaced` — same code path as today's "no stock available" case. The `IsInStock` bin classification (`!IsCustomLength && StockItemId.HasValue`) is untouched, since it already doesn't depend on quantity.
|
||||||
|
- `CatalogService`: `CatalogStockItemDto` drops `QuantityOnHand`; `ImportStockItemsAsync`/`MapStockItems` stop reading/writing it. Existing seed JSON (`alro-catalog.json`, `oneals-catalog.json`) keeps the field in the file for now — it's simply ignored on import.
|
||||||
|
- `CutList.Mcp/InventoryTools.cs`: its own `StockItemDto` drops `QuantityOnHand`. `add_stock_item` drops the `quantityOnHand` parameter. `add_stock` convenience tool drops `quantityOnHand` and the `QuantityOnHand` field on `AddStockResult` — it becomes purely "ensure this material and stock length exist." `ApiClient` methods that pass `quantityOnHand` are updated to match. Republished to `~/.claude/mcp/CutList.Mcp/` per the standard MCP publishing workflow.
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
- `/stock` (Index): drop the "On Hand" column/badge. Intro copy changes from "tracks how many pieces you have on hand" to describing stock items as the lengths of material available to cut from.
|
||||||
|
- `/stock/{id}` (Edit): drop the entire right-hand "Inventory" card (quantity badge, Add/Adjust Stock transaction form, transaction history table). Left-hand details form (Material, Length, Name, Notes) is unchanged.
|
||||||
|
- Job Edit → Stock tab: `SaveStockFromInventoryAsync`'s quantity validation changes from `Quantity < 1` to `Quantity < -1 || Quantity == 0` (matching custom-stock validation), and the form gains an "Unlimited" option for catalog-sourced rows.
|
||||||
|
- Job Edit → Results tab: the existing "Items Not Placed" warning stays as-is structurally; its copy changes from "No stock lengths available or parts too long" to also cover insufficient configured quantity, since a finite `JobStock.Quantity` is now the only thing that can produce unplaced items for a material that does have stock configured.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
One new EF Core migration:
|
||||||
|
- Drops the `QuantityOnHand` column from `StockItems`.
|
||||||
|
- Drops the `StockTransactions` table.
|
||||||
|
|
||||||
|
This is destructive to any `QuantityOnHand`/`StockTransaction` data currently on forge (confirmed acceptable — nothing reads it automatically today, and it isn't otherwise relied upon). Applied immediately after `dotnet ef migrations add`, per the standard EF workflow, no separate confirmation gate beyond this design doc.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Any change to the packing algorithm itself (`CutList.Core`) — untouched, since bin selection already only consumed `StockBinSource.Quantity`/`IsInStock`, not `QuantityOnHand` directly.
|
||||||
|
- Reworking the "Purchase List" / `ToBePurchasedBins` reporting concept — it stays as a classification of catalog-sourced vs. custom-length bins for the print report, which doesn't depend on quantity tracking.
|
||||||
|
- Cleaning up `QuantityOnHand` values already present in `alro-catalog.json`/`oneals-catalog.json` seed files — harmless once the import path ignores the field.
|
||||||
Reference in New Issue
Block a user