Przeglądarka plików PDF dla ASP.NET: Jak wyświetlać pliki PDF w .NET 10
Problem z ręcznym generowaniem wycen
Każda godzina, którą przedstawiciel handlowy spędza kopiując pozycje z CRM do szablonu Word, to godzina, którą nie sprzedaje. W zespole dziesięciu przedstawicieli, którzy mają po dwadzieścia aktywnych możliwości każdy, ten czas się sumuje, a wynik i tak nie jest niezawodny. Poziomy cenowe są stosowane nieprawidłowo, pola rabatowe pozostają na stawkach z ostatniego kwartału, a język warunków z starej wersji szablonu prześlizguje się do dokumentu, który wychodzi do klienta bez przeglądu.
Problem czasu liniowego potęguje problem dokładności. Potencjalny klient, który prosi o wycenę we wtorek po południu i dostaje ją w środę wieczorem, spędził dzień zastanawiając się, czy umowa jest rzeczywista. Do czasu, gdy plik PDF do niego dotarł, jego uwaga może być już skierowana na konkurenta, który zareagował szybciej.
Istniejące narzędzia CPQ rozwiązują część tego problemu, ale przy cenach dla przedsiębiorstw i harmonogramach wdrożeń, które nie pasują do zespołów, które po prostu potrzebują czystego pliku PDF z danych już istniejącej możliwości. Wysyłanie e-mailem arkusza kalkulacyjnego lub listy cen w postaci zwykłego tekstu zamiast ustrukturyzowanej propozycji sygnalizuje potencjalnemu klientowi, że proces sprzedaży firmy jest improwizowany. A gdy potencjalny klient prosi o poprawki, zamieszanie w wersjach się mnoży, kilkukrotne iteracje unoszą się jako załączniki e-mailowe bez jednego źródła prawdy przytwierdzonego do rekordu w CRM.
Scenariusze się różnią: firma oprogramowania B2B pobierająca wyceny licencji z Salesforce, firma produkcyjna generująca propozycje części i cen z pozycji ERP, agencja tworząca dokumenty określenia zakresu projektu z wewnętrznych szacunków, hurtownik tworzący oferty zniżkowe dla detalistów. Podstawowe tarcia są takie same w każdym przypadku.
Rozwiązanie: Generowanie wycen bezpośrednio z danych możliwości
IronPDF to potężna biblioteka PDF dla .NET, która pozwala aplikacjom .NET generować markowe wyceny PDF i propozycje bezpośrednio z danych możliwości CRM. Gdy przedstawiciel oznacza transakcję jako gotową do wyceny lub kliknie przycisk w wewnętrznym narzędziu, aplikacja pobiera pozycje, ceny, dane klienta i warunki z CRM lub bazy danych, wypełnia szablon HTML lub ciąg zawartości HTML, a ChromePdfRenderer generuje C# PDF z tego HTML.
Wycena jest wysyłana e-mailem do potencjalnego klienta, a wysłany dokument jest automatycznie rejestrowany w rekordzie CRM, bez ręcznego formatowania, bez szablonów Word, bez opłat SaaS za dokument. Renderowanie odbywa się wewnątrz istniejącej aplikacji .NET jako pojedyncza paczka NuGet. Szablon jest własnością Twojego zespołu i jest wersjonowany, aktualizowany w jednym miejscu, co odzwierciedla się w każdej przyszłej wycenie bez rozsyłania plików do zespołu sprzedażowego.
Jak to działa w praktyce
1. Wyzwalacz inicjuje ofertę
Pipeline rozpoczyna się, gdy przedstawiciel kliknie "Generuj ofertę" w interfejsie CRM, możliwość automatycznie wchodzi w określony etap pipeline'u lub zewnętrzna platforma automatyzacji wykonuje wywołanie API do punktu końcowego tworzenia ofert. Wszystkie trzy punkty wejściowe zasilają ten sam przepływ generowania, mechanizm wyzwalacza nie zmienia tego, co dzieje się dalej.
Aplikacja zapytuje CRM lub bazę danych o wszystko, co wymaga oferta: nazwa firmy potencjalnego klienta, imię i nazwisko oraz e-mail kontaktu, pozycje z SKU, opisy, ilości, ceny jednostkowe, zastosowane procenty rabatów i sumy pozycji; łączna suma, obliczenie podatku i suma całkowita; warunki płatności, okres ważności oferty i ewentualne niestandardowe notatki dodane przez przedstawiciela do możliwości.
2. Szablon HTML określa układ oferty
Szablon obsługuje pełną wizualną strukturę oferty: logo firmy i nagłówek, blok "Przygotowany dla" z danymi potencjalnego klienta, tabela z liniami pozycji z kolumnami dla opisu, ilości, ceny jednostkowej, rabatu i sumy pozycji, podsumowanie sumy oraz warunki i stopka z imieniem, nazwiskiem, tytułem i bezpośrednim kontaktem przedstawiciela.
Tabela z liniami pozycji jest tworzona przez iterację nad pozycjami możliwości w handlerze C# i konstruowanie wierszy HTML przed przekazaniem kompletnego szablonu do IronPDF. To utrzymuje logikę w warstwie aplikacji, a nie w samym szablonie.
Przykładowy szablon pliku HTML

3. ChromePdfRenderer produkuje dokument PDF oferty
Mocny silnik renderujący IronPDF umożliwia prostą konwersję HTML na PDF, zazwyczaj wykonaną w zaledwie kilku liniach kodu.
using IronPdf;
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;
var lineItemRows = string.Concat(opportunity.LineItems.Select(item => $@"
<tr>
<td>{item.Description}</td>
<td style='text-align:center'>{item.Quantity}</td>
<td style='text-align:right'>{item.UnitPrice:C}</td>
<td style='text-align:center'>{item.DiscountPct}%</td>
<td style='text-align:right'>{item.LineTotal:C}</td>
</tr>"));
string html = $@"
<h1>Quote — {opportunity.ProspectCompany}</h1>
<p>Prepared for: {opportunity.ContactName} <{opportunity.ContactEmail}></p>
<p>Valid until: {opportunity.QuoteExpiry:MMMM d, yyyy} | Terms: {opportunity.PaymentTerms}</p>
<table border='1' cellpadding='6' width='100%'>
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Discount</th><th>Total</th></tr></thead>
<tbody>{lineItemRows}</tbody>
</table>
<p style='text-align:right'><strong>Subtotal:</strong> {opportunity.Subtotal:C}</p>
<p style='text-align:right'><strong>Tax ({opportunity.TaxRatePct}%):</strong> {opportunity.TaxAmount:C}</p>
<p style='text-align:right'><strong>Grand Total:</strong> {opportunity.GrandTotal:C}</p>
<hr/><p style='font-size:11px'>{opportunity.TermsAndConditions}</p>";
PdfDocument quote = renderer.RenderHtmlAsPdf(html);
using IronPdf;
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;
var lineItemRows = string.Concat(opportunity.LineItems.Select(item => $@"
<tr>
<td>{item.Description}</td>
<td style='text-align:center'>{item.Quantity}</td>
<td style='text-align:right'>{item.UnitPrice:C}</td>
<td style='text-align:center'>{item.DiscountPct}%</td>
<td style='text-align:right'>{item.LineTotal:C}</td>
</tr>"));
string html = $@"
<h1>Quote — {opportunity.ProspectCompany}</h1>
<p>Prepared for: {opportunity.ContactName} <{opportunity.ContactEmail}></p>
<p>Valid until: {opportunity.QuoteExpiry:MMMM d, yyyy} | Terms: {opportunity.PaymentTerms}</p>
<table border='1' cellpadding='6' width='100%'>
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Discount</th><th>Total</th></tr></thead>
<tbody>{lineItemRows}</tbody>
</table>
<p style='text-align:right'><strong>Subtotal:</strong> {opportunity.Subtotal:C}</p>
<p style='text-align:right'><strong>Tax ({opportunity.TaxRatePct}%):</strong> {opportunity.TaxAmount:C}</p>
<p style='text-align:right'><strong>Grand Total:</strong> {opportunity.GrandTotal:C}</p>
<hr/><p style='font-size:11px'>{opportunity.TermsAndConditions}</p>";
PdfDocument quote = renderer.RenderHtmlAsPdf(html);
Imports IronPdf
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.MarginBottom = 20
Dim lineItemRows As String = String.Concat(opportunity.LineItems.Select(Function(item) $"
<tr>
<td>{item.Description}</td>
<td style='text-align:center'>{item.Quantity}</td>
<td style='text-align:right'>{item.UnitPrice:C}</td>
<td style='text-align:center'>{item.DiscountPct}%</td>
<td style='text-align:right'>{item.LineTotal:C}</td>
</tr>"))
Dim html As String = $"
<h1>Quote — {opportunity.ProspectCompany}</h1>
<p>Prepared for: {opportunity.ContactName} <{opportunity.ContactEmail}></p>
<p>Valid until: {opportunity.QuoteExpiry:MMMM d, yyyy} | Terms: {opportunity.PaymentTerms}</p>
<table border='1' cellpadding='6' width='100%'>
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Discount</th><th>Total</th></tr></thead>
<tbody>{lineItemRows}</tbody>
</table>
<p style='text-align:right'><strong>Subtotal:</strong> {opportunity.Subtotal:C}</p>
<p style='text-align:right'><strong>Tax ({opportunity.TaxRatePct}%):</strong> {opportunity.TaxAmount:C}</p>
<p style='text-align:right'><strong>Grand Total:</strong> {opportunity.GrandTotal:C}</p>
<hr/><p style='font-size:11px'>{opportunity.TermsAndConditions}</p>"
Dim quote As PdfDocument = renderer.RenderHtmlAsPdf(html)
Wygenerowany dokument PDF
4. Dostarczenie i zarejestrowanie oferty
using System.Net.Mail;
using System.IO;
// Save versioned copy to document storage
string outputPath = $"quotes/{opportunity.Id}/v{opportunity.QuoteVersion}.pdf";
quote.SaveAs(outputPath);
// Email to prospect
using var stream = new MemoryStream(quote.BinaryData);
using var attachment = new Attachment(stream,
$"Quote-{opportunity.ProspectCompany}-v{opportunity.QuoteVersion}.pdf",
"application/pdf");
var message = new MailMessage("sales@yourcompany.com", opportunity.ContactEmail)
{
Subject = $"Your Quote from {opportunity.RepName} — {opportunity.ProspectCompany}",
Body = $"Hi {opportunity.ContactName},\n\nPlease find your quote attached. It is valid until {opportunity.QuoteExpiry:MMMM d, yyyy}.\n\n{opportunity.RepName}"
};
message.Attachments.Add(attachment);
using var smtp = new SmtpClient("smtp.yourprovider.com");
await smtp.SendMailAsync(message);
// Log reference back to CRM
await _crmService.LogQuoteSentAsync(opportunity.Id, outputPath, DateTime.UtcNow);
using System.Net.Mail;
using System.IO;
// Save versioned copy to document storage
string outputPath = $"quotes/{opportunity.Id}/v{opportunity.QuoteVersion}.pdf";
quote.SaveAs(outputPath);
// Email to prospect
using var stream = new MemoryStream(quote.BinaryData);
using var attachment = new Attachment(stream,
$"Quote-{opportunity.ProspectCompany}-v{opportunity.QuoteVersion}.pdf",
"application/pdf");
var message = new MailMessage("sales@yourcompany.com", opportunity.ContactEmail)
{
Subject = $"Your Quote from {opportunity.RepName} — {opportunity.ProspectCompany}",
Body = $"Hi {opportunity.ContactName},\n\nPlease find your quote attached. It is valid until {opportunity.QuoteExpiry:MMMM d, yyyy}.\n\n{opportunity.RepName}"
};
message.Attachments.Add(attachment);
using var smtp = new SmtpClient("smtp.yourprovider.com");
await smtp.SendMailAsync(message);
// Log reference back to CRM
await _crmService.LogQuoteSentAsync(opportunity.Id, outputPath, DateTime.UtcNow);
Imports System.Net.Mail
Imports System.IO
' Save versioned copy to document storage
Dim outputPath As String = $"quotes/{opportunity.Id}/v{opportunity.QuoteVersion}.pdf"
quote.SaveAs(outputPath)
' Email to prospect
Using stream As New MemoryStream(quote.BinaryData)
Using attachment As New Attachment(stream, $"Quote-{opportunity.ProspectCompany}-v{opportunity.QuoteVersion}.pdf", "application/pdf")
Dim message As New MailMessage("sales@yourcompany.com", opportunity.ContactEmail) With {
.Subject = $"Your Quote from {opportunity.RepName} — {opportunity.ProspectCompany}",
.Body = $"Hi {opportunity.ContactName}," & vbCrLf & vbCrLf & $"Please find your quote attached. It is valid until {opportunity.QuoteExpiry:MMMM d, yyyy}." & vbCrLf & vbCrLf & $"{opportunity.RepName}"
}
message.Attachments.Add(attachment)
Using smtp As New SmtpClient("smtp.yourprovider.com")
Await smtp.SendMailAsync(message)
End Using
End Using
End Using
' Log reference back to CRM
Await _crmService.LogQuoteSentAsync(opportunity.Id, outputPath, DateTime.UtcNow)
Przykład wysłanego e-maila z załączonym PDF
Numer wersji zarówno w ścieżce pliku, jak i w nazwie pliku, daje każdej rewizji własny trwały zapis. Gdy potencjalny klient żąda zmian, przedstawiciel aktualizuje możliwość w CRM, aplikacja regeneruje się z inkrementowaną wersją, a nowa oferta zostaje wysłana, z pełną historią zachowaną w magazynie dokumentów i zarejestrowana w rekordzie CRM.
Korzyści w praktyce
Szybkość do potencjalnego klienta. Oferta generowana jest i wysyłana e-mailem w ciągu kilku sekund po jej zainicjowaniu przez przedstawiciela. Brak opóźnienia w formatowaniu, brak dokumentów siedzących w folderze wersji roboczych i brak opóźnienia między decyzją przedstawiciela o ofercie a jej otrzymaniem przez potencjalnego klienta.
Dokładność cen. Pozycje, poziomy rabatów i sumy całkowite są pobierane bezpośrednio z możliwości CRM. Nie ma kroku ręcznego przepisywania i nie ma ryzyka, że przedstawiciel zastosuje cennik z zeszłego kwartału lub zły harmonogram rabatów.
Spójność marki. Każda oferta wysyłana przez firmę zawiera to samo logo, układ, język warunków i stopkę, bez względu na to, który przedstawiciel ją wygenerował lub z jakiego etapu pipeline'u. Nie ma niekontrolowanych szablonów Word unoszących się na poszczególnych laptopach.
Śledzenie wersji. Każda wygenerowana oferta jest przechowywana z ID możliwości, numerem wersji i znacznikiem czasu, kiedy została wysłana. Rejestr CRM daje menedżerom sprzedaży pełną widoczność na to, co było ofertowane, komu i kiedy, bez przeszukiwania wątków e-mailowych.
Ponowne wykorzystanie szablonów. Gdy zespół prawny aktualizuje język warunków lub zespół marki odświeża logo, jeden plik HTML zmienia się i każda przyszła oferta to odzwierciedla. Nie ma ponownej dystrybucji szablonów Word do zespołu sprzedaży.
Brak kosztów za dokument. Renderowanie odbywa się w procesie wewnątrz aplikacji .NET. Nie ma subskrypcji CPQ, nie ma mierzenia API generowania dokumentów i nie ma modelu kosztów, który rośnie wraz z wolumenem transakcji.
Zakończenie
Luka między danymi możliwości CRM a profesjonalnym PDF w skrzynce odbiorczej potencjalnego klienta jest mniejsza, niż się wydaje, to szablon, wywołanie renderowania i wysłanie e-maila, wszystko wywołane jedną akcją w przepływie pracy sprzedaży. Usunięcie kroku ręcznego formatowania nie tylko oszczędza czas; usuwa kategorię błędów, które mogą kosztować transakcje.
Ten pipeline jest prosty do zbudowania i utrzymania w istniejącej aplikacji .NET. IronPDF obsługuje pełny cykl życia generowania PDF w C# na stronie ironpdf.com, od renderowania szablonów HTML po zapisywanie, strumieniowanie i manipulowanie dokumentami. Darmowy 30-dniowy okres próbny daje wystarczająco dużo czasu na zbudowanie i przetestowanie pełnego pipeline'u generowania ofert z wykorzystaniem własnych danych CRM i przepływu pracy sprzedaży.




