diff --git a/src/PowderCoating.Application/DTOs/Job/JobDtos.cs b/src/PowderCoating.Application/DTOs/Job/JobDtos.cs index 8ed6ac6..233649c 100644 --- a/src/PowderCoating.Application/DTOs/Job/JobDtos.cs +++ b/src/PowderCoating.Application/DTOs/Job/JobDtos.cs @@ -389,7 +389,7 @@ public class CompleteJobDto { public int JobId { get; set; } public decimal? ActualTimeSpentHours { get; set; } - public List CoatUsages { get; set; } = new(); + public List PowderUsages { get; set; } = new(); public bool SendEmailToCustomer { get; set; } = false; } @@ -400,10 +400,10 @@ public class SendJobSmsRequest public string Message { get; set; } = string.Empty; } -// DTO for tracking actual powder usage per coat -public class JobItemCoatUsageDto +// DTO for tracking actual powder usage per inventory item (color) for the whole job +public class JobPowderUsageDto { - public int JobItemCoatId { get; set; } + public int InventoryItemId { get; set; } public decimal? ActualPowderUsedLbs { get; set; } } diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs index d1bcaf3..eaa39e4 100644 --- a/src/PowderCoating.Web/Controllers/JobsController.cs +++ b/src/PowderCoating.Web/Controllers/JobsController.cs @@ -2655,78 +2655,80 @@ public class JobsController : Controller .GroupBy(t => t.InventoryItemId) .ToDictionary(g => g.Key, g => Math.Abs(g.Sum(t => t.Quantity))); - // Update actual powder usage for each coat - foreach (var coatUsage in dto.CoatUsages) + // Process powder usage submitted per inventory item (color) for the whole job. + // Distribute entered lbs across coats sharing that InventoryItemId proportionally + // by estimated PowderToOrder so per-coat reporting stays meaningful. + // One inventory deduction per powder (net of pre-logged credit). + if (dto.PowderUsages.Any()) { - var jobItemCoat = await _unitOfWork.JobItemCoats.GetByIdAsync( - coatUsage.JobItemCoatId, - false, - jic => jic.InventoryItem); + // Load all coats for the job with their inventory items + var allCoats = (await _unitOfWork.JobItemCoats.FindAsync( + jic => jic.JobItem != null && jic.JobItem.JobId == dto.JobId, + false, jic => jic.InventoryItem, jic => jic.JobItem)) + .ToList(); - if (jobItemCoat != null) + foreach (var powderUsage in dto.PowderUsages) { - jobItemCoat.ActualPowderUsedLbs = coatUsage.ActualPowderUsedLbs; - await _unitOfWork.JobItemCoats.UpdateAsync(jobItemCoat); + if (!powderUsage.ActualPowderUsedLbs.HasValue || powderUsage.ActualPowderUsedLbs.Value <= 0) + continue; - _logger.LogInformation("Updated JobItemCoat {CoatId} with {Lbs} lbs actual powder used", - coatUsage.JobItemCoatId, coatUsage.ActualPowderUsedLbs); + var invItemId = powderUsage.InventoryItemId; + var totalActualLbs = powderUsage.ActualPowderUsedLbs.Value; - // Deduct powder from inventory if using stock powder - if (jobItemCoat.InventoryItemId.HasValue && - coatUsage.ActualPowderUsedLbs.HasValue && - coatUsage.ActualPowderUsedLbs.Value > 0) + // Distribute across coats using this powder proportionally by estimated lbs + var coatsForPowder = allCoats.Where(c => c.InventoryItemId == invItemId).ToList(); + if (coatsForPowder.Any()) { - var invItemId = jobItemCoat.InventoryItemId.Value; - var actualLbs = coatUsage.ActualPowderUsedLbs.Value; - - // Apply available pre-logged credit so we don't double-deduct - var credit = preLoggedCredit.GetValueOrDefault(invItemId, 0m); - var deductNow = Math.Max(0m, actualLbs - credit); - // Consume credit (other coats sharing the same powder get whatever remains) - preLoggedCredit[invItemId] = Math.Max(0m, credit - actualLbs); - - if (deductNow > 0) + var totalEstimated = coatsForPowder.Sum(c => c.PowderToOrder ?? 0m); + foreach (var coat in coatsForPowder) { - var inventoryItem = await _unitOfWork.InventoryItems.GetByIdAsync(invItemId); - if (inventoryItem != null) - { - var transaction = new InventoryTransaction - { - InventoryItemId = inventoryItem.Id, - TransactionType = InventoryTransactionType.JobUsage, - Quantity = -deductNow, - UnitCost = inventoryItem.UnitCost, - TotalCost = inventoryItem.UnitCost * deductNow, - TransactionDate = DateTime.UtcNow, - JobId = job.Id, - Reference = job.JobNumber, - Notes = $"Powder used for Job {job.JobNumber} - {jobItemCoat.CoatName} ({jobItemCoat.ColorName ?? "N/A"}) by {currentUser!.FirstName} {currentUser.LastName}", - BalanceAfter = inventoryItem.QuantityOnHand - deductNow, - CompanyId = job.CompanyId - }; - - await _unitOfWork.InventoryTransactions.AddAsync(transaction); - inventoryItem.QuantityOnHand -= deductNow; - await _unitOfWork.InventoryItems.UpdateAsync(inventoryItem); - - // GL: DR COGS, CR Inventory Asset (accrual) — no-op if accounts not configured - if (inventoryItem.CogsAccountId.HasValue && inventoryItem.InventoryAccountId.HasValue) - { - var cost = deductNow * (inventoryItem.AverageCost > 0 ? inventoryItem.AverageCost : inventoryItem.UnitCost); - await _accountBalanceService.DebitAsync(inventoryItem.CogsAccountId, cost); - await _accountBalanceService.CreditAsync(inventoryItem.InventoryAccountId, cost); - } - - _logger.LogInformation( - "Deducted {Lbs} lbs (net of pre-logged) of {Item} from inventory for Job {JobNumber}. New quantity: {NewQty}", - deductNow, inventoryItem.Name, job.JobNumber, inventoryItem.QuantityOnHand); - } + var share = totalEstimated > 0 + ? totalActualLbs * ((coat.PowderToOrder ?? 0m) / totalEstimated) + : totalActualLbs / coatsForPowder.Count; + coat.ActualPowderUsedLbs = Math.Round(share, 4); + await _unitOfWork.JobItemCoats.UpdateAsync(coat); } - else + } + + // Single inventory deduction for the whole powder, net of pre-logged credit + var credit = preLoggedCredit.GetValueOrDefault(invItemId, 0m); + var deductNow = Math.Max(0m, totalActualLbs - credit); + preLoggedCredit[invItemId] = 0m; + + if (deductNow > 0) + { + var inventoryItem = await _unitOfWork.InventoryItems.GetByIdAsync(invItemId); + if (inventoryItem != null) { + inventoryItem.QuantityOnHand -= deductNow; + await _unitOfWork.InventoryItems.UpdateAsync(inventoryItem); + + var transaction = new InventoryTransaction + { + InventoryItemId = inventoryItem.Id, + TransactionType = InventoryTransactionType.JobUsage, + Quantity = -deductNow, + UnitCost = inventoryItem.UnitCost, + TotalCost = inventoryItem.UnitCost * deductNow, + TransactionDate = DateTime.UtcNow, + JobId = job.Id, + Reference = job.JobNumber, + Notes = $"Powder used for Job {job.JobNumber} by {currentUser!.FirstName} {currentUser.LastName}", + BalanceAfter = inventoryItem.QuantityOnHand, + CompanyId = job.CompanyId + }; + await _unitOfWork.InventoryTransactions.AddAsync(transaction); + + if (inventoryItem.CogsAccountId.HasValue && inventoryItem.InventoryAccountId.HasValue) + { + var cost = deductNow * (inventoryItem.AverageCost > 0 ? inventoryItem.AverageCost : inventoryItem.UnitCost); + await _accountBalanceService.DebitAsync(inventoryItem.CogsAccountId, cost); + await _accountBalanceService.CreditAsync(inventoryItem.InventoryAccountId, cost); + } + _logger.LogInformation( - "Skipped inventory deduction for JobItemCoat {CoatId} — {Lbs} lbs already pre-logged for inventory item {InvItemId}", - coatUsage.JobItemCoatId, actualLbs, invItemId); + "Deducted {Lbs} lbs (net of pre-logged) of {Item} from inventory for Job {JobNumber}. New quantity: {NewQty}", + deductNow, inventoryItem.Name, job.JobNumber, inventoryItem.QuantityOnHand); } } } diff --git a/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml b/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml index 30b75af..9a41d9b 100644 --- a/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml @@ -2,8 +2,21 @@ @{ var emailDefault = ViewBag.EmailDefaultOnComplete == true; var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary ?? new Dictionary(); - // Track remaining credit per InventoryItemId as we allocate it across coat rows - var remainingCredit = preLoggedPowder.ToDictionary(kv => kv.Key, kv => kv.Value); + + // Group all coats by inventory item so we ask once per powder color, not once per item/coat + var powderGroups = (Model.Items ?? new List()) + .SelectMany(i => i.Coats ?? new List()) + .Where(c => c.InventoryItemId.HasValue) + .GroupBy(c => c.InventoryItemId!.Value) + .Select(g => new { + InventoryItemId = g.Key, + ColorName = g.First().ColorName, + ColorCode = g.First().ColorCode, + TotalEstimatedLbs = g.Sum(c => c.PowderToOrder ?? 0m), + PreLogged = preLoggedPowder.GetValueOrDefault(g.Key, 0m) + }) + .OrderBy(g => g.ColorName) + .ToList(); }