Add IsIncoming inventory flag and catalog-to-incoming powder flow in item wizard
- InventoryItem.IsIncoming: marks powder ordered but not yet received; enables QR code printing on work orders while the shipment is in transit - InventoryController.CreateIncomingFromCatalog: POST endpoint creates a 0-balance inventory record from a PowderCatalogItem and returns it in wizard-compatible shape - item-wizard.js: custom coat tab now searches the platform powder catalog as a fallback; catalog results show an 'Add as Incoming Order' option; createIncomingFromCatalog POSTs to server and selects the new item without a page refresh - QuoteItemCoatDto: CatalogItemId + AddAsIncoming fields so the wizard can signal server-side incoming-item creation during quote save - Inventory Create/Edit/Index views: IsIncoming badge and field - IInventoryAiLookupService: minor interface update - Migration: AddInventoryIsIncoming Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -708,7 +708,6 @@ public class InventoryController : Controller
|
||||
return Json(new { success = false, errorMessage = "No product URL provided." });
|
||||
|
||||
var result = await _aiLookupService.LookupByUrlAsync(productUrl, colorName);
|
||||
if (result.Success) await ApplyTdsCureFallbackAsync(result, colorName);
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
@@ -801,17 +800,15 @@ public class InventoryController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If cure temperature or cure time is still missing after the primary lookup but a TDS URL
|
||||
/// was returned, fetches that page and asks Claude to extract only the cure schedule.
|
||||
/// Mutates <paramref name="result"/> in place; silently no-ops on failure so callers
|
||||
/// can always return the result even if the TDS fetch does not help.
|
||||
/// When cure specs are still missing after a primary AI lookup (LookupAsync or ScanLabelAsync),
|
||||
/// fetches the TDS URL that Claude returned and asks it to extract only the cure schedule.
|
||||
/// Not used by AiAugmentFromUrl — that path uses LookupByUrlAsync which has TDS fallback built in.
|
||||
/// </summary>
|
||||
private async Task ApplyTdsCureFallbackAsync(InventoryAiLookupResult result, string? colorName)
|
||||
{
|
||||
if ((result.CureTemperatureF == null || result.CureTimeMinutes == null)
|
||||
&& !string.IsNullOrEmpty(result.TdsUrl))
|
||||
{
|
||||
_logger.LogInformation("Cure specs missing after lookup; trying TDS at {Url}", result.TdsUrl);
|
||||
var tds = await _aiLookupService.FetchTdsCureSpecsAsync(result.TdsUrl, colorName);
|
||||
if (tds.Success)
|
||||
{
|
||||
@@ -1118,6 +1115,109 @@ public class InventoryController : Controller
|
||||
return Json(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 0-balance inventory item from a PowderCatalogItem record and marks it IsIncoming=true.
|
||||
/// Called by the item wizard when a staff member needs to quote a powder that has been ordered
|
||||
/// but not yet received — the inventory record enables QR code printing on the work order.
|
||||
/// Returns the new item's data in the same shape as the inventoryPowdersData list so the wizard
|
||||
/// can add it to powderData and select it immediately without a page refresh.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CreateIncomingFromCatalog(int catalogItemId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var catalogItem = await _unitOfWork.PowderCatalog.GetByIdAsync(catalogItemId);
|
||||
if (catalogItem == null)
|
||||
return Json(new { success = false, error = "Catalog item not found." });
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
// Find the default coating category to assign
|
||||
var categories = await _unitOfWork.InventoryCategoryLookups.GetAllAsync();
|
||||
var coatingCategory = categories
|
||||
.Where(c => c.IsActive && c.IsCoating)
|
||||
.OrderBy(c => c.DisplayOrder)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (coatingCategory == null)
|
||||
return Json(new { success = false, error = "No active coating category found. Please configure inventory categories first." });
|
||||
|
||||
// Generate a unique SKU following the same pattern as GenerateSku: {CODE}-{YYMM}-{####}
|
||||
var code = coatingCategory.CategoryCode.Length >= 4
|
||||
? coatingCategory.CategoryCode[..4].ToUpperInvariant()
|
||||
: coatingCategory.CategoryCode.ToUpperInvariant().PadRight(4, 'X');
|
||||
var yearMonth = DateTime.Now.ToString("yyMM");
|
||||
var prefix = $"{code}-{yearMonth}-";
|
||||
var allItems = await _unitOfWork.InventoryItems.GetAllAsync(ignoreQueryFilters: true);
|
||||
var maxSeq = allItems
|
||||
.Where(i => i.SKU.StartsWith(prefix))
|
||||
.Select(i => int.TryParse(i.SKU[prefix.Length..], out var n) ? n : 0)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
var sku = $"{prefix}{(maxSeq + 1):D4}";
|
||||
|
||||
var item = new InventoryItem
|
||||
{
|
||||
SKU = sku,
|
||||
Name = ToTitleCase($"{catalogItem.VendorName} {catalogItem.ColorName}"),
|
||||
ColorName = catalogItem.ColorName,
|
||||
Manufacturer = catalogItem.VendorName,
|
||||
ManufacturerPartNumber= catalogItem.Sku,
|
||||
Finish = catalogItem.Finish,
|
||||
ColorFamilies = catalogItem.ColorFamilies,
|
||||
RequiresClearCoat = catalogItem.RequiresClearCoat ?? false,
|
||||
CoverageSqFtPerLb = catalogItem.CoverageSqFtPerLb ?? 30m,
|
||||
TransferEfficiency = GetEffectiveTransferEfficiency(catalogItem.TransferEfficiency),
|
||||
CureTemperatureF = catalogItem.CureTemperatureF,
|
||||
CureTimeMinutes = catalogItem.CureTimeMinutes,
|
||||
SpecificGravity = catalogItem.SpecificGravity,
|
||||
SpecPageUrl = catalogItem.ProductUrl,
|
||||
ImageUrl = catalogItem.ImageUrl,
|
||||
SdsUrl = catalogItem.SdsUrl,
|
||||
TdsUrl = catalogItem.TdsUrl,
|
||||
UnitCost = catalogItem.UnitPrice,
|
||||
AverageCost = catalogItem.UnitPrice,
|
||||
LastPurchasePrice = catalogItem.UnitPrice,
|
||||
QuantityOnHand = 0,
|
||||
UnitOfMeasure = "lbs",
|
||||
InventoryCategoryId = coatingCategory.Id,
|
||||
Category = coatingCategory.DisplayName,
|
||||
IsActive = true,
|
||||
IsIncoming = true,
|
||||
CompanyId = companyId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await _unitOfWork.InventoryItems.AddAsync(item);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Created incoming inventory item {ItemId} ({ItemName}) from catalog item {CatalogId} for company {CompanyId}",
|
||||
item.Id, item.Name, catalogItemId, companyId);
|
||||
|
||||
return Json(new
|
||||
{
|
||||
success = true,
|
||||
value = item.Id.ToString(),
|
||||
text = $"[INCOMING] {coatingCategory.DisplayName} - {item.Manufacturer ?? "Generic"} - {item.ColorName ?? item.Name} - {item.ManufacturerPartNumber ?? "N/A"} ({item.UnitCost:C4}/unit)",
|
||||
coverage = item.CoverageSqFtPerLb ?? 30m,
|
||||
efficiency = item.TransferEfficiency ?? 65m,
|
||||
unitOfMeasure= item.UnitOfMeasure,
|
||||
categoryName = coatingCategory.DisplayName,
|
||||
costPerLb = item.UnitCost,
|
||||
colorName = item.ColorName ?? item.Name,
|
||||
colorCode = "",
|
||||
isIncoming = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create incoming inventory item from catalog {CatalogItemId}", catalogItemId);
|
||||
return Json(new { success = false, error = "Failed to create inventory item. Please try again." });
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal GetEffectiveTransferEfficiency(decimal? transferEfficiency)
|
||||
{
|
||||
return transferEfficiency ?? DefaultTransferEfficiency;
|
||||
|
||||
Reference in New Issue
Block a user