Complete Job modal: ask powder usage once per color, not per item/coat
The modal was showing one row per coat per item, so a job with 5 items each with 2 coats of the same powder produced 10 identical input rows. Now groups by unique InventoryItemId and shows one row per powder color for the whole job. The controller distributes the entered total across coats proportionally by their estimated PowderToOrder so per-coat reporting data is preserved. A single inventory transaction is created per powder (net of any pre-logged scan credit). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user