Add packing slip PDF to invoice details page

Generates a no-price packing slip (items, color, qty + signature line) via
QuestPDF. New DownloadPackingSlip action reuses existing invoice data pipeline;
Packing Slip button opens inline in a new tab same as Print/PDF.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 16:52:46 -04:00
parent 81dc34bab4
commit b241daf15e
6 changed files with 254 additions and 0 deletions
@@ -1782,6 +1782,59 @@ public class InvoicesController : Controller
}
}
// -----------------------------------------------------------------------
// GET: /Invoices/DownloadPackingSlip/5
// -----------------------------------------------------------------------
/// <summary>
/// Generates a no-price packing slip PDF for physical pickup/delivery paperwork.
/// Reuses the same company branding and invoice data pipeline as DownloadPdf but
/// delegates to GeneratePackingSlipPdfAsync which omits all pricing columns.
/// </summary>
public async Task<IActionResult> DownloadPackingSlip(int? id, bool inline = false)
{
if (id == null) return NotFound();
try
{
var invoice = await LoadInvoiceForViewAsync(id.Value);
if (invoice == null) return NotFound();
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var company = await _unitOfWork.Companies.GetByIdAsync(currentUser.CompanyId);
var companyInfo = new Application.DTOs.Company.CompanyInfoDto
{
CompanyName = company?.CompanyName ?? string.Empty,
Phone = company?.Phone,
Address = company?.Address,
City = company?.City,
State = company?.State,
ZipCode = company?.ZipCode,
PrimaryContactEmail = company?.PrimaryContactEmail
};
var (logoData, logoContentType) = await LoadCompanyLogoAsync(company);
var dto = await BuildInvoiceDtoAsync(invoice);
var pdfBytes = await _pdfService.GeneratePackingSlipPdfAsync(dto, logoData, logoContentType, companyInfo);
var fileName = $"PackingSlip-{invoice.InvoiceNumber}.pdf";
if (inline)
{
Response.Headers["Content-Disposition"] = $"inline; filename=\"{fileName}\"";
return File(pdfBytes, "application/pdf");
}
return File(pdfBytes, "application/pdf", fileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating packing slip for invoice {Id}", id);
TempData["ErrorPermanent"] = $"Packing slip generation failed: {ex.Message}";
return RedirectToAction(nameof(Details), new { id });
}
}
// -----------------------------------------------------------------------
// GET: /Invoices/ForJob/5 — redirect to existing or Create
// -----------------------------------------------------------------------
@@ -51,6 +51,10 @@
<a asp-action="DownloadPdf" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
<i class="bi bi-file-pdf me-2"></i>PDF
</a>
<a asp-action="DownloadPackingSlip" asp-route-id="@Model.Id" asp-route-inline="true"
class="btn btn-outline-secondary" target="_blank" rel="noopener" title="Print Packing Slip">
<i class="bi bi-box-seam me-2"></i>Packing Slip
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back
</a>