Fix two production billing bugs: invoice missing oven cost, quote stuck in Draft after send
Bug 1 — Invoice total didn't match job total for direct jobs: - Root cause: all three item-save paths in JobsController passed null for ovenCostId, so FinalPrice/ShopSuppliesAmount were stored without oven cost while the Details page recalculated live with OvenCostId and showed higher. - Add OvenBatchCost stored field to Job entity (migration AddJobOvenBatchCost, default 0 for existing rows). - Fix Create, Edit, and UpdateItems to pass job.OvenCostId and save OvenBatchCost. - Fix InvoicesController.Create GET for direct jobs to use stored OvenBatchCost and ShopSuppliesAmount as separate labeled lines instead of recalculating shop supplies from scratch (which excluded the oven cost base). Bug 2 — Quote status stayed Draft after "Send Quote via Email": - ResendQuote advanced the approval token and sent the email but never updated the status. Added Draft → Sent advancement (same guard used by the SMS send path) so the status updates on successful email send. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -458,21 +458,36 @@ public class InvoicesController : Controller
|
||||
dto.TaxPercent = sourceQuote.TaxPercent;
|
||||
dto.DiscountAmount = sourceQuote.DiscountAmount;
|
||||
}
|
||||
else if (hadJobItems && costs?.ShopSuppliesRate > 0)
|
||||
else if (hadJobItems)
|
||||
{
|
||||
// Direct job — no source quote. Derive shop supplies from the items subtotal
|
||||
// using the current company rate. (Quote-sourced jobs read the pre-agreed amount
|
||||
// from the quote snapshot instead; this path only fires when there is no quote.)
|
||||
var itemsSubtotal = dto.InvoiceItems.Sum(i => i.TotalPrice);
|
||||
var shopSuppliesAmount = Math.Round(itemsSubtotal * (costs.ShopSuppliesRate / 100m), 2);
|
||||
if (shopSuppliesAmount > 0.01m)
|
||||
// Direct job — no source quote. Use the stored job-level fees rather than
|
||||
// recalculating, so the invoice always matches the total shown on the job page.
|
||||
// OvenBatchCost and ShopSuppliesAmount are saved by the pricing engine (with
|
||||
// OvenCostId) when job items are created or updated.
|
||||
if (job.OvenBatchCost > 0.01m)
|
||||
{
|
||||
dto.InvoiceItems.Add(new CreateInvoiceItemDto
|
||||
{
|
||||
Description = $"Shop Supplies ({costs.ShopSuppliesRate:0.##}%)",
|
||||
Description = $"Oven Processing Fee",
|
||||
Quantity = 1,
|
||||
UnitPrice = shopSuppliesAmount,
|
||||
TotalPrice = shopSuppliesAmount,
|
||||
UnitPrice = Math.Round(job.OvenBatchCost, 2),
|
||||
TotalPrice = Math.Round(job.OvenBatchCost, 2),
|
||||
DisplayOrder = order++,
|
||||
RevenueAccountId = defaultRevenueAccount?.Id
|
||||
});
|
||||
}
|
||||
|
||||
if (job.ShopSuppliesAmount > 0.01m)
|
||||
{
|
||||
var suppliesDesc = job.ShopSuppliesPercent > 0
|
||||
? $"Shop Supplies ({job.ShopSuppliesPercent:0.##}%)"
|
||||
: "Shop Supplies";
|
||||
dto.InvoiceItems.Add(new CreateInvoiceItemDto
|
||||
{
|
||||
Description = suppliesDesc,
|
||||
Quantity = 1,
|
||||
UnitPrice = Math.Round(job.ShopSuppliesAmount, 2),
|
||||
TotalPrice = Math.Round(job.ShopSuppliesAmount, 2),
|
||||
DisplayOrder = order,
|
||||
RevenueAccountId = defaultRevenueAccount?.Id
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user