Native Salesforce Quoting Built for Manufacturing Sales Teams

Stop pasting spreadsheet quotes into emails. SilkQuote generates branded, buyer-ready manufacturing quotes directly from your Salesforce Opportunities — part numbers, lead times, freight terms, and payment conditions included. No CPQ. No external tools.

Manufacturing quote template in Salesforce Lightning
Manufacturing quote template — full size

How It Works

  1. Install and set up the SilkQuote appfollow the setup guide →
  2. Create the Apex Class and Visualforce Page — copy the code below and paste each file into your Salesforce org via Setup
  3. Create a SilkQuote Template record — in the SilkQuote app, create a new template and set the Visualforce Page name to SilkQuote_Manufacturing

If you need any assistance, reach out to support.

Requirements

Before you install, confirm you have:

  • Salesforce edition — Professional, Enterprise, or Unlimited
  • System Administrator profile — required for package installation

Install SilkQuote — free, installs in minutes →

Apex Class ManufacturingPDFController.cls
// Apex Controller Class
/**
 * ManufacturingPDFController
 *
 * Unmanaged Apex controller for SilkQuote_Manufacturing.page.
 * Customers have full ownership of this class and can modify it freely.
 *
 * This controller is a pure data provider — it loads Salesforce records and
 * exposes simple string/boolean properties. All layout and HTML lives in
 * SilkQuote_Manufacturing.page, which is the only file you need to edit to
 * change how the document looks.
 *
 * Uses "without sharing" so it works in guest/site contexts (e.g. the
 * Collaboration Site iframe for quote review). Change to "with sharing"
 * if you only use this template in authenticated contexts.
 *
 * ============================================================
 * CUSTOMIZATION GUIDE
 * ============================================================
 * Accent color:  Change ACCENT_COLOR constant below.
 * Logo:          Set LOGO_ID to a ContentVersion or Document Id.
 * Company name:  Set COMPANY_NAME (shown when no logo is set).
 * Tax:           In loadLineItems(), replace taxVal = 0 with
 *                your own tax logic.
 * Layout:        Edit SilkQuote_Manufacturing.page — no Apex changes needed.
 * ============================================================
 */
public without sharing class ManufacturingPDFController {

    // ============================================================
    // CUSTOMIZE THESE
    // ============================================================
    private static final String ACCENT_COLOR = '#1B3A5C';
    private static final String LOGO_ID      = '';   // ContentVersion or Document Id
    private static final String COMPANY_NAME = '';   // Shown when no logo is set
    // ============================================================

    // Styling
    public String  accentColor { get; private set; }
    public String  logoId      { get; private set; }
    public String  companyName { get; private set; }

    // Quote metadata
    public String  quoteName     { get; private set; }
    public String  formattedDate { get; private set; }
    public String  validUntil    { get; private set; }

    // Rep / Sales contact
    public String  repName  { get; private set; }
    public String  repPhone { get; private set; }

    // Bill To / Ship To address (uses billing address for both)
    public String  accName         { get; private set; }
    public String  accStreet       { get; private set; }
    public String  accCityStateZip { get; private set; }
    public String  accCountry      { get; private set; }
    public String  accPhone        { get; private set; }

    // Line items
    public List<LineItemRow> lineItems { get; private set; }
    public Boolean hasLineItems { get; private set; }

    // Totals
    public String  formattedSubtotal { get; private set; }
    public String  formattedDiscount { get; private set; }
    public String  formattedTax      { get; private set; }
    public String  formattedTotal    { get; private set; }
    public Boolean hasDiscount { get; private set; }
    public Boolean hasTax      { get; private set; }

    // Terms — raw HTML from the managed package, rendered with escape="false"
    public String  termsHtml { get; private set; }
    public Boolean hasTerms  { get; private set; }

    // ============================================================
    // LineItemRow — one row of the products table.
    // ============================================================
    public class LineItemRow {
        public Boolean isEven        { get; set; }
        public String  sku           { get; set; }
        public String  name          { get; set; }
        public String  description   { get; set; }
        public Boolean hasDescription { get; set; }
        public String  qty           { get; set; }
        public String  listPrice     { get; set; }
        public String  discount      { get; set; }
        public Boolean hasDiscount   { get; set; }
        public String  unitPrice     { get; set; }
        public String  lineTotal     { get; set; }
    }

    // ============================================================
    // Constructor
    // ============================================================
    public ManufacturingPDFController() {
        accentColor  = ACCENT_COLOR;
        logoId       = LOGO_ID;
        companyName  = COMPANY_NAME;
        formattedDate = formatDate(Date.today());
        lineItems    = new List<LineItemRow>();
        hasLineItems = false;
        hasTerms     = false;
        hasDiscount  = false;
        hasTax       = false;
        termsHtml    = '';
        quoteName = repName = repPhone = '';
        accName = accStreet = accCityStateZip = accCountry = accPhone = '';
        validUntil = '';
        formattedSubtotal = formattedDiscount = formattedTax = formattedTotal = '$0.00';

        Map<String, String> params = ApexPages.currentPage().getParameters();
        quoteName = safe(params.get('quoteName'));
        String oppId = sanitizeId(params.get('opportunityId'));

        if (String.isNotBlank(oppId)) {
            loadOpportunity(oppId);
            loadLineItems(oppId);
        }
        String termsId = sanitizeId(params.get('selectedTerms'));
        if (String.isNotBlank(termsId)) {
            loadTerms(termsId);
        }
    }

    // ============================================================
    // Data Loading
    // ============================================================
    private void loadOpportunity(String oppId) {
        List<Opportunity> opps = [
            SELECT Id, Name, CloseDate,
                   Account.Name,
                   Account.BillingStreet,
                   Account.BillingCity,
                   Account.BillingState,
                   Account.BillingPostalCode,
                   Account.BillingCountry,
                   Account.Phone,
                   Owner.Name,
                   Owner.Phone
            FROM Opportunity
            WHERE Id = :oppId
            LIMIT 1
        ];
        if (opps.isEmpty()) return;
        Opportunity opp = opps[0];

        validUntil = opp.CloseDate != null ? formatDate(opp.CloseDate) : '';

        repName  = safe(opp.Owner.Name);
        repPhone = safe(opp.Owner.Phone);

        Account a = opp.Account;
        accName   = safe(a.Name);
        accStreet = safe(a.BillingStreet);
        String city = safe(a.BillingCity);
        String state = safe(a.BillingState);
        String zip   = safe(a.BillingPostalCode);
        accCityStateZip = city
            + (String.isNotBlank(state) ? ', ' + state : '')
            + (String.isNotBlank(zip)   ? ' '  + zip   : '');
        accCountry = safe(a.BillingCountry);
        accPhone   = safe(a.Phone);
    }

    private void loadLineItems(String oppId) {
        List<OpportunityLineItem> raw = [
            SELECT Id, Name, ProductCode, Description,
                   Quantity, ListPrice, UnitPrice, TotalPrice, Discount,
                   silkquote__Hide_On_PDF__c
            FROM OpportunityLineItem
            WHERE OpportunityId = :oppId
            ORDER BY SortOrder ASC NULLS LAST, Name ASC
        ];
        Decimal sub = 0, disc = 0;
        Integer n = 0;
        for (OpportunityLineItem item : raw) {
            if (item.silkquote__Hide_On_PDF__c == true) continue;
            n++;
            Decimal lineTotal = item.TotalPrice != null ? item.TotalPrice : 0;
            Decimal listTotal = (item.ListPrice != null ? item.ListPrice : 0)
                              * (item.Quantity  != null ? item.Quantity  : 0);
            sub  += lineTotal;
            disc += (listTotal - lineTotal);

            LineItemRow row = new LineItemRow();
            row.isEven        = Math.mod(n, 2) == 0;
            row.sku           = safe(item.ProductCode);
            row.name          = safe(item.Name);
            row.description   = safe(item.Description);
            row.hasDescription = String.isNotBlank(item.Description);
            row.qty           = formatQty(item.Quantity);
            row.listPrice     = formatCurrency(item.ListPrice);
            row.hasDiscount   = item.Discount != null && item.Discount > 0;
            row.discount      = row.hasDiscount ? formatQty(item.Discount) + '%' : '';
            row.unitPrice     = formatCurrency(item.UnitPrice);
            row.lineTotal     = formatCurrency(item.TotalPrice);
            lineItems.add(row);
        }
        hasLineItems = !lineItems.isEmpty();

        Decimal subtotalVal = sub.setScale(2, RoundingMode.HALF_UP);
        Decimal discountVal = (disc < 0 ? 0 : disc).setScale(2, RoundingMode.HALF_UP);
        Decimal taxVal      = 0; // ✏️ Add tax logic here if needed
        Decimal totalVal    = (subtotalVal - discountVal + taxVal).setScale(2, RoundingMode.HALF_UP);

        formattedSubtotal = formatCurrency(subtotalVal);
        formattedDiscount = formatCurrency(discountVal);
        formattedTax      = formatCurrency(taxVal);
        formattedTotal    = formatCurrency(totalVal);
        hasDiscount = discountVal > 0;
        hasTax      = taxVal > 0;
    }

    private void loadTerms(String termsId) {
        List<silkquote__Terms__c> termsList = [
            SELECT silkquote__Terms_Text_Body__c
            FROM silkquote__Terms__c
            WHERE Id = :termsId
            LIMIT 1
        ];
        if (!termsList.isEmpty() && String.isNotBlank(termsList[0].silkquote__Terms_Text_Body__c)) {
            hasTerms  = true;
            termsHtml = termsList[0].silkquote__Terms_Text_Body__c;
        }
    }

    // ============================================================
    // Utilities
    // ============================================================
    private static String safe(String val) {
        return val != null ? val : '';
    }

    private static String formatCurrency(Decimal val) {
        if (val == null) return '$0.00';
        Decimal v = val.setScale(2, RoundingMode.HALF_UP);
        String raw = String.valueOf(v.abs());
        Integer dot = raw.indexOf('.');
        String intPart = dot >= 0 ? raw.substring(0, dot) : raw;
        String decPart = dot >= 0 ? raw.substring(dot + 1) : '00';
        while (decPart.length() < 2) decPart += '0';
        if (decPart.length() > 2)   decPart = decPart.substring(0, 2);
        String result = '';
        Integer count = 0;
        for (Integer i = intPart.length() - 1; i >= 0; i--) {
            if (count > 0 && Math.mod(count, 3) == 0) result = ',' + result;
            result = intPart.substring(i, i + 1) + result;
            count++;
        }
        return (v < 0 ? '-' : '') + '$' + result + '.' + decPart;
    }

    private static String formatQty(Decimal val) {
        if (val == null) return '';
        Long whole = val.longValue();
        return (val == Decimal.valueOf(whole))
            ? String.valueOf(whole)
            : String.valueOf(val.setScale(2, RoundingMode.HALF_UP));
    }

    private static String formatDate(Date d) {
        if (d == null) return '';
        String[] months = new String[]{
            'January','February','March','April','May','June',
            'July','August','September','October','November','December'
        };
        return months[d.month() - 1] + ' ' + d.day() + ', ' + d.year();
    }

    private static String sanitizeId(String raw) {
        if (String.isBlank(raw)) return null;
        String clean = raw.trim().replaceAll('[^a-zA-Z0-9]', '');
        return (clean.length() == 15 || clean.length() == 18) ? clean : null;
    }
}

Visualforce Page SilkQuote_Manufacturing.page
<apex:page controller="ManufacturingPDFController"
           renderAs="pdf"
           showHeader="false"
           sidebar="false"
           standardStylesheets="false"
           applyBodyTag="false"
           applyHtmlTag="false">
<html>
<head>
  <meta charset="UTF-8"/>
  <style>

    @page {
      size: letter;
      margin: 0 0.35in 0.6in 0;
    }

    body {
      font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
      font-size: 11px;
      line-height: 18px;
      color: #1a1a1a;
      margin: 0;
      padding: 0;
    }

    /* ── Header band ─────────────────────────────────────────── */
    .doc-header {
      width: 100%;
      padding: 16pt 20pt;
      box-sizing: border-box;
    }

    .doc-header table {
      width: 100%;
      border-collapse: collapse;
    }

    .doc-header .doc-title {
      font-size: 15pt;
      font-weight: bold;
      letter-spacing: 1.5pt;
      text-transform: uppercase;
      color: #1B3A5C;
      text-align: right;
      line-height: 1.2;
    }

    .doc-header .doc-sub {
      font-size: 8pt;
      color: rgba(255,255,255,0.6);
      text-align: right;
      margin-top: 2pt;
      letter-spacing: 0.5pt;
    }

    /* ── Content area ────────────────────────────────────────── */
    .doc-body {
      padding: 14pt 4pt 20pt 20pt;
      box-sizing: border-box;
    }

    .section {
      margin-bottom: 14pt;
    }

    /* ── Address cards — Bill To / Ship To ───────────────────── */
    .addr-table {
      width: 100%;
      border-collapse: collapse;
    }

    .addr-box {
      border: 1pt solid #b0bec8;
      vertical-align: top;
    }

    .addr-box-header {
      background-color: #1B3A5C;
      color: #ffffff;
      font-size: 7.5pt;
      font-weight: bold;
      text-transform: uppercase;
      letter-spacing: 1pt;
      padding: 4pt 10pt;
    }

    .addr-box-body {
      padding: 8pt 10pt;
      font-size: 10pt;
      line-height: 17px;
      color: #1a1a1a;
    }

    .addr-box-body .addr-name {
      font-weight: bold;
      margin-bottom: 2pt;
    }

    .addr-box-body .addr-line {
      color: #444;
    }

    /* ── Quote reference bar ─────────────────────────────────── */
    .ref-table-wrap {
      border: 1pt solid #b0bec8;
      margin-bottom: 14pt;
    }

    .ref-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 9.5pt;
    }

    .ref-table th {
      background-color: #2E86C1;
      color: #ffffff;
      font-size: 7.5pt;
      font-weight: bold;
      text-transform: uppercase;
      letter-spacing: 0.8pt;
      padding: 5pt 10pt;
      text-align: left;
      border-right: 1pt solid #5aa8d8;
    }

    .ref-table td {
      padding: 6pt 10pt;
      border-right: 1pt solid #b0bec8;
      vertical-align: top;
    }

    .ref-table th:last-child,
    .ref-table td:last-child {
      border-right: none;
    }

    /* ── Section header bar ──────────────────────────────────── */
    .section-header {
      background-color: #1B3A5C;
      color: #ffffff;
      font-size: 8pt;
      font-weight: bold;
      text-transform: uppercase;
      letter-spacing: 1pt;
      padding: 5pt 8pt;
      margin-bottom: 0;
    }

    /* ── Line items table ────────────────────────────────────── */
    .items-table-wrap {
      border: 1pt solid #b0bec8;
    }

    .items-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 9.5pt;
    }

    .items-table th {
      background-color: #2E86C1;
      color: #ffffff;
      font-size: 8pt;
      font-weight: bold;
      text-transform: uppercase;
      letter-spacing: 0.5pt;
      padding: 6pt 8pt;
      text-align: left;
      border-right: 1pt solid #5aa8d8;
    }

    .items-table th.right { text-align: right; }

    .items-table th:last-child { border-right: none; }

    .items-table td {
      padding: 6pt 8pt;
      border-bottom: 1pt solid #d0dae4;
      border-right: 1pt solid #d0dae4;
      vertical-align: top;
      color: #1a1a1a;
    }

    .items-table td.right { text-align: right; }

    .items-table td:last-child { border-right: none; }

    .items-table .even td { background-color: #f4f8fc; }

    .item-name { font-weight: bold; }
    .item-sku  { font-size: 8pt; color: #666; margin-bottom: 2pt; }
    .item-desc { font-size: 8.5pt; color: #666; margin-top: 2pt; }
    .item-disc { font-size: 8pt; color: #888; }

    /* ── Totals ──────────────────────────────────────────────── */
    .totals-wrap {
      width: 46%;
      margin-left: auto;
    }

    .totals-table {
      width: 100%;
      border-collapse: collapse;
      border: 1pt solid #b0bec8;
      font-size: 9.5pt;
    }

    .totals-table td {
      padding: 5pt 10pt;
      border-bottom: 1pt solid #d0dae4;
    }

    .totals-table .t-label { color: #444; }
    .totals-table .t-value { text-align: right; font-weight: bold; }

    .totals-table .total-row td {
      background-color: #1B3A5C;
      color: #ffffff;
      font-size: 10.5pt;
      font-weight: bold;
      border-bottom: none;
      padding: 7pt 10pt;
    }

    /* ── Footer ──────────────────────────────────────────────── */
    .doc-footer {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      padding: 5pt 20pt;
      font-size: 7.5pt;
      border-top: 0.5pt solid #b0bec8;
      background: #ffffff;
      color: #666;
    }

    .doc-footer table {
      width: 100%;
      border-collapse: collapse;
    }

    /* ── Terms page ──────────────────────────────────────────── */
    .terms-page {
      page-break-before: always;
      padding: 20pt;
    }

    .terms-title {
      font-size: 14pt;
      font-weight: bold;
      color: #1a1a1a;
      margin-bottom: 16pt;
      text-align: center;
    }

    .terms-content {
      font-size: 10px;
      line-height: 18px;
      color: #444;
      white-space: pre-line;
      word-wrap: break-word;
    }

  </style>
</head>
<body>

  <!-- ══════════════════════════════════════════════════════════
       HEADER BAND
  ══════════════════════════════════════════════════════════ -->
  <div class="doc-header">
    <table>
      <tr>
        <td style="vertical-align:middle;width:50%;">
          <apex:outputPanel rendered="{!NOT(ISBLANK(logoId))}">
            <img src="/servlet/servlet.FileDownload?file={!URLENCODE(logoId)}"
                 style="max-height:40pt; max-width:150pt;" alt="Logo"/>
          </apex:outputPanel>
          <apex:outputPanel rendered="{!ISBLANK(logoId)}">
            <div style="font-size:14pt;font-weight:bold;color:#1B3A5C;letter-spacing:0.5pt;">{!companyName}</div>
          </apex:outputPanel>
        </td>
        <td style="vertical-align:middle;text-align:right;width:46%;">
          <!-- ✏️ CUSTOMIZE: Change document title here -->
          <div class="doc-title">Purchase Quotation</div>
        </td>
        <td style="width:4%;"></td>
      </tr>
    </table>
  </div>

  <div class="doc-body">

    <!-- ══════════════════════════════════════════════════════
         BILL TO / SHIP TO — bordered address cards
    ══════════════════════════════════════════════════════ -->
    <div class="section">
      <table class="addr-table">
        <tr>
          <td style="width:48%;" class="addr-box">
            <div class="addr-box-header">Bill To</div>
            <div class="addr-box-body">
              <div class="addr-name">{!accName}</div>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accStreet))}">
                <div class="addr-line">{!accStreet}</div>
              </apex:outputPanel>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accCityStateZip))}">
                <div class="addr-line">{!accCityStateZip}</div>
              </apex:outputPanel>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accCountry))}">
                <div class="addr-line">{!accCountry}</div>
              </apex:outputPanel>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accPhone))}">
                <div class="addr-line" style="margin-top:3pt;">{!accPhone}</div>
              </apex:outputPanel>
            </div>
          </td>
          <td style="width:4%;"></td>
          <td style="width:48%;" class="addr-box">
            <div class="addr-box-header">Ship To</div>
            <div class="addr-box-body">
              <div class="addr-name">{!accName}</div>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accStreet))}">
                <div class="addr-line">{!accStreet}</div>
              </apex:outputPanel>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accCityStateZip))}">
                <div class="addr-line">{!accCityStateZip}</div>
              </apex:outputPanel>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accCountry))}">
                <div class="addr-line">{!accCountry}</div>
              </apex:outputPanel>
              <apex:outputPanel rendered="{!NOT(ISBLANK(accPhone))}">
                <div class="addr-line" style="margin-top:3pt;">{!accPhone}</div>
              </apex:outputPanel>
            </div>
          </td>
        </tr>
      </table>
    </div>

    <!-- ══════════════════════════════════════════════════════
         QUOTE REFERENCE BAR
    ══════════════════════════════════════════════════════ -->
    <div class="section">
      <div class="ref-table-wrap">
      <table class="ref-table">
        <thead>
          <tr>
            <th style="width:25%;">Quote Name</th>
            <th style="width:20%;">Date</th>
            <th style="width:20%;">Valid Until</th>
            <th style="width:17.5%;">Sales Rep</th>
            <th style="width:17.5%;">Rep Phone</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>{!quoteName}</td>
            <td>{!formattedDate}</td>
            <td>{!validUntil}</td>
            <td>{!repName}</td>
            <td>{!repPhone}</td>
          </tr>
        </tbody>
      </table>
      </div>
    </div>

    <!-- ══════════════════════════════════════════════════════
         PRODUCTS
    ══════════════════════════════════════════════════════ -->
    <div class="section">
      <div class="section-header">Products</div>
      <apex:outputPanel rendered="{!hasLineItems}">
        <div class="items-table-wrap">
        <table class="items-table">
          <thead>
            <tr>
              <th style="width:12%;">SKU</th>
              <th>Product</th>
              <th class="right" style="width:8%;">Qty</th>
              <th class="right" style="width:13%;">List Price</th>
              <th class="right" style="width:10%;">Discount</th>
              <th class="right" style="width:13%;">Unit Price</th>
              <th class="right" style="width:13%;">Total</th>
            </tr>
          </thead>
          <tbody>
            <apex:repeat value="{!lineItems}" var="row">
              <tr class="{!IF(row.isEven, 'even', '')}">
                <td style="font-size:8.5pt;">{!row.sku}</td>
                <td>
                  <div class="item-name">{!row.name}</div>
                  <apex:outputPanel rendered="{!row.hasDescription}">
                    <div class="item-desc">{!row.description}</div>
                  </apex:outputPanel>
                </td>
                <td class="right">{!row.qty}</td>
                <td class="right">{!row.listPrice}</td>
                <td class="right">
                  <apex:outputPanel rendered="{!row.hasDiscount}">
                    <span class="item-disc">{!row.discount}</span>
                  </apex:outputPanel>
                </td>
                <td class="right">{!row.unitPrice}</td>
                <td class="right">{!row.lineTotal}</td>
              </tr>
            </apex:repeat>
          </tbody>
        </table>
        </div>
      </apex:outputPanel>
      <apex:outputPanel rendered="{!NOT(hasLineItems)}">
        <p style="color:#aaa;font-size:10pt;padding:8pt 0;">No products listed.</p>
      </apex:outputPanel>
    </div>

    <!-- ══════════════════════════════════════════════════════
         TOTALS
    ══════════════════════════════════════════════════════ -->
    <div class="section">
      <div class="totals-wrap">
        <table class="totals-table">
          <tr>
            <td class="t-label">Subtotal</td>
            <td class="t-value">{!formattedSubtotal}</td>
          </tr>
          <apex:outputPanel rendered="{!hasDiscount}" layout="none">
            <tr>
              <td class="t-label">Discount</td>
              <td class="t-value">({!formattedDiscount})</td>
            </tr>
          </apex:outputPanel>
          <apex:outputPanel rendered="{!hasTax}" layout="none">
            <tr>
              <td class="t-label">Tax</td>
              <td class="t-value">{!formattedTax}</td>
            </tr>
          </apex:outputPanel>
          <tr class="total-row">
            <td>Total Due</td>
            <td style="text-align:right;font-weight:bold;">{!formattedTotal}</td>
          </tr>
        </table>
      </div>
    </div>

  </div><!-- end doc-body -->

  <!-- ══════════════════════════════════════════════════════════
       FIXED FOOTER
  ══════════════════════════════════════════════════════════ -->
  <div class="doc-footer">
    <table>
      <tr>
        <td style="text-align:left;">{!accName}</td>
        <td style="text-align:right;white-space:nowrap;">{!quoteName} &nbsp;|&nbsp; {!formattedDate}</td>
      </tr>
    </table>
  </div>

<!-- ══════════════════════════════════════════════════════════
     TERMS &amp; CONDITIONS — new page
══════════════════════════════════════════════════════════ -->
<apex:outputPanel rendered="{!hasTerms}">
  <div class="terms-page">
    <div class="terms-title">Terms &amp; Conditions</div>
    <div class="terms-content">
      <apex:outputText value="{!termsHtml}" escape="false"/>
    </div>
  </div>
</apex:outputPanel>

</body>
</html>
</apex:page>


Frequently Asked Questions

How do I create a bill of materials quote in Salesforce?

Salesforce doesn’t generate BOM-style quotes natively, but SilkQuote does. Once installed, you configure a manufacturing template with part number, description, quantity, unit price, extended total, and lead time columns — then generate a formatted PDF from any Opportunity with one click.

Can Salesforce generate a PDF purchase quote with part numbers?

Yes, with SilkQuote. SilkQuote reads your Opportunity Products and formats them into a professional PDF quote that includes part numbers, quantities, pricing, lead times, and payment terms. No spreadsheet, no copy-pasting.

What Salesforce quoting tools work for manufacturers without CPQ?

SilkQuote is built for exactly this use case. It works with standard Salesforce Opportunities and Products — no CPQ license required. Install it from AppExchange, configure your BOM-style template once, and your team can generate quotes directly from Salesforce.

How do I include freight and payment terms on a Salesforce quote?

SilkQuote’s template editor includes dedicated sections for freight and shipping terms (FOB point, carrier, prepaid/collect) and payment terms (Net 30, deposit requirements, progress billing). These appear as structured sections in the PDF below the line items.