972123c7a2
Three bugs fixed: 1. Wrong timing — inventory items with IsIncoming=true were auto-created during quote save (in QuotePricingAssemblyService). Now deferred to quote approval so inventory only reflects powders the shop is actually going to process. 2. Duplicate records — same powder on multiple items in one quote created multiple inventory records. Now grouped by PowderCatalogItemId: one record per unique catalog powder, all matching coats linked to the same record. 3. Wrong category — category resolution used first IsCoating=true by DisplayOrder, which could land items in Cerakote or other unintended categories. Now prefers CategoryCode==POWDER explicitly, with DisplayOrder fallback. Changes: - QuoteItemCoat: add PowderCatalogItemId int? — persists catalog reference at quote save time so the approval path knows what to create - QuotePricingAssemblyService.BuildQuoteItemCoatsAsync: store PowderCatalogItemId on coat instead of calling CreateIncomingInventoryItemAsync immediately - QuotePricingAssemblyService.CreateIncomingInventoryItemAsync: signature changed from (coatDto, companyId) to (catalogItemId, companyId); category lookup prefers POWDER code; no longer clears PowderCostPerLb on the DTO - QuotePricingAssemblyService.EnsureIncomingInventoryForApprovedQuoteAsync: new public method called at approval — loads pending coats, groups by catalog ID, creates one inventory item per group, links all coats in each group - IQuotePricingAssemblyService: exposes EnsureIncomingInventoryForApprovedQuoteAsync - QuotesController.ApproveQuote: calls EnsureIncomingInventory after save - QuotesController.ChangeQuoteStatus: calls EnsureIncomingInventory on Approved - QuoteApprovalController: injects IQuotePricingAssemblyService; calls EnsureIncomingInventory in ApproveInternal (customer-facing portal path) - InventoryController.CreateIncomingFromCatalog: same category fix (prefers POWDER) - Migration: AddPowderCatalogItemIdToCoat (nullable int on QuoteItemCoats) - Tests: updated AddAsIncoming test to verify deferred behavior; new deduplication test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
58 lines
2.4 KiB
C#
58 lines
2.4 KiB
C#
namespace PowderCoating.Core.Entities;
|
|
|
|
/// <summary>
|
|
/// Represents a single coating layer applied to a quote item.
|
|
/// Supports multi-coat configurations (primer, base coat, top coat, clear coat, etc.)
|
|
/// </summary>
|
|
public class QuoteItemCoat : BaseEntity
|
|
{
|
|
// Parent relationship
|
|
public int QuoteItemId { get; set; }
|
|
|
|
// Coat identification (user-defined)
|
|
public string CoatName { get; set; } = string.Empty; // "Primer", "Base Coat", "Top Coat", etc.
|
|
public int Sequence { get; set; } // 1, 2, 3... for ordering
|
|
|
|
// Powder selection (same pattern as current QuoteItem)
|
|
public int? InventoryItemId { get; set; } // In-stock powder
|
|
/// <summary>
|
|
/// Platform powder catalog item that this coat was sourced from.
|
|
/// Persisted so that at quote-approval time the system can create exactly one
|
|
/// IsIncoming InventoryItem per unique catalog powder (deduplication), rather
|
|
/// than creating during quote-save when the job may never be approved.
|
|
/// </summary>
|
|
public int? PowderCatalogItemId { get; set; }
|
|
public string? ColorName { get; set; } // Color name
|
|
public int? VendorId { get; set; } // Vendor for custom powder
|
|
public string? ColorCode { get; set; } // RAL code, etc.
|
|
public string? Finish { get; set; } // Gloss, Matte, Textured, etc.
|
|
|
|
// Coverage parameters (defaults from inventory or user-specified)
|
|
public decimal CoverageSqFtPerLb { get; set; } = 30m;
|
|
public decimal TransferEfficiency { get; set; } = 65m;
|
|
|
|
// Cost override for custom powder
|
|
public decimal? PowderCostPerLb { get; set; } // $/lb for custom orders
|
|
public decimal? PowderToOrder { get; set; } // Pounds to order (rounded up from needed)
|
|
|
|
// Calculated costs (stored for audit trail)
|
|
public decimal CoatMaterialCost { get; set; }
|
|
public decimal CoatLaborCost { get; set; }
|
|
public decimal CoatTotalCost { get; set; }
|
|
|
|
// Pricing flags
|
|
/// <summary>
|
|
/// When true, the additional layer labor charge is not applied for this coat even if it is
|
|
/// not the first coat in the sequence. Used for clear coats, sealers, etc.
|
|
/// </summary>
|
|
public bool NoExtraLayerCharge { get; set; }
|
|
|
|
// Notes
|
|
public string? Notes { get; set; }
|
|
|
|
// Navigation properties
|
|
public virtual QuoteItem QuoteItem { get; set; } = null!;
|
|
public virtual InventoryItem? InventoryItem { get; set; }
|
|
public virtual Vendor? Vendor { get; set; }
|
|
}
|