{
  "id": "E5QvVu7b1SGiH1EF",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Pricing Proposal Generator",
  "tags": [],
  "nodes": [
    {
      "id": "de5e1a55-89b9-4d5f-a066-790a84ed7b4b",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 420,
        "height": 1012,
        "content": "## \ud83d\udcc4 Pricing Proposal Generator\n\nGenerates a branded PDF pricing proposal on demand and delivers it by email.\n\n### How it works\n1. **Webhook Trigger** \u2014 A web form POSTs prospect details, selected services, discount level, and requestor info to the webhook endpoint.\n2. **Fetch Pricing Sheet** \u2014 Retrieves all rows from your `pricing_request.xlsx` Excel table in OneDrive/SharePoint.\nDownload template: https://iportgpt.com/n8n_assets/pricing_request.xlsx\n3. **Process & Price** \u2014 Filters the pricing rows to the selected services, applies the correct Discount / Retail / Premium price tier, and calculates the total.\n4. **Build HTML** \u2014 Renders a fully branded HTML proposal document with cover info, line-item pricing table, terms, and a signature block.\n5. **Convert to PDF** \u2014 Sends the HTML to a self-hosted [Gotenberg](https://gotenberg.dev) instance, which returns a print-ready PDF.\n6. **Email Proposal** \u2014 Delivers the PDF as an attachment via Gmail OAuth2 to the requester and any internal recipients you configure.\n\n### Setup\n1. **Webhook** \u2014 Set the `path` field to a value that matches your form POST URL (e.g. `pricing-request`).\n2. **Fetch Pricing Sheet** \u2014 Connect a Microsoft Excel credential, then select your OneDrive/SharePoint workbook, worksheet, and table.\n3. **Process & Price** \u2014 Confirm the column names in the Code node match your Excel table (`Title`, `Description`, `Cost`, `Discount`, `Retail`, `Premium`, `Unit`).\n4. **Build HTML Proposal** \u2014 Replace `YOUR_LOGO_URL` and brand color variables with your own values.\n5. **Gotenberg \u2192 PDF** \u2014 Update the URL to point to your Gotenberg instance and attach a Basic Auth credential.\n6. **Email Proposal** \u2014 Connect a Gmail OAuth2 credential. Update `YOUR_INTERNAL_EMAIL` in the `sendTo` field.\n\n### Customization\n- Add new price tiers by extending the `switch` block in **Process & Price**.\n- Adjust proposal branding in **Build HTML Proposal** by updating the CSS color variables and logo URL.\n- Validity period defaults to 30 days \u2014 change the `30` multiplier in **Build HTML Proposal** and the email body text."
      },
      "typeVersion": 1
    },
    {
      "id": "b81eea6c-155c-46c1-a311-5a5882db2f92",
      "name": "Section \u2013 Intake",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 180,
        "height": 448,
        "content": "## 1 \u00b7 Intake\nReceives the form POST from your web form."
      },
      "typeVersion": 1
    },
    {
      "id": "d0a5d8c9-bb5b-4888-b5c0-718268f8e7cd",
      "name": "Section \u2013 Data",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 180,
        "height": 448,
        "content": "## 2 \u00b7 Data\nPulls all rows from the pricing Excel table in OneDrive."
      },
      "typeVersion": 1
    },
    {
      "id": "db0e3bcc-1e53-4fdd-b9f8-1346713b1f0d",
      "name": "Section \u2013 Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 196,
        "height": 448,
        "content": "## 3 \u00b7 Processing\nFilters services, applies price tier (Discount / Retail / Premium), and calculates total."
      },
      "typeVersion": 1
    },
    {
      "id": "c3957feb-4194-4e72-84f2-70f16bc5f91a",
      "name": "Section \u2013 Build",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 180,
        "height": 448,
        "content": "## 4 \u00b7 Proposal Build\nRenders the branded HTML document for PDF conversion.\n\nUpdate `YOUR_LOGO_URL` and the brand color variables to match your identity."
      },
      "typeVersion": 1
    },
    {
      "id": "ae3f4da8-02bd-4bdf-816d-5cfd87365a2b",
      "name": "Section \u2013 PDF",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 180,
        "height": 448,
        "content": "## 5 \u00b7 PDF Conversion\nSelf-hosted [Gotenberg](https://gotenberg.dev) converts HTML \u2192 PDF.\n\nUpdate the URL to your Gotenberg instance and attach a **Basic Auth** credential."
      },
      "typeVersion": 1
    },
    {
      "id": "af29b910-ddde-40ab-b3a1-541895ebdf75",
      "name": "Section \u2013 Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 448,
        "content": "## 6 \u00b7 Email Delivery\nSends the PDF to the requester via Gmail OAuth2.\n\nReplace `YOUR_INTERNAL_EMAIL` in the **Email Proposal** node with your own address."
      },
      "typeVersion": 1
    },
    {
      "id": "b155c9f1-3352-4ebe-8b25-46397e9a6a52",
      "name": "Pricing Request Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        480,
        256
      ],
      "parameters": {
        "path": "pricing-request",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "8cdce527-7d8d-4aeb-97b0-8b2d7fc2b6ee",
      "name": "Fetch Pricing Sheet",
      "type": "n8n-nodes-base.microsoftExcel",
      "position": [
        688,
        256
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "Table1"
        },
        "filters": {},
        "resource": "table",
        "workbook": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "pricing_request.xlsx"
        },
        "operation": "getRows",
        "returnAll": true,
        "worksheet": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "Sheet1"
        }
      },
      "credentials": {
        "microsoftExcelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "711c31c7-eb06-4c01-90d6-86637325d70f",
      "name": "Process & Price",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        256
      ],
      "parameters": {
        "jsCode": "// Extract form data\n// Expected fields: prospectName, date, services (array or string),\n// discountLevel ('Discount' | 'Retail' | 'Premium'),\n// requestorName (format: \"Full Name (user@example.com)\")\nconst formData = $('Pricing Request Webhook').item.json.body;\nconst prospectName   = formData.prospectName;\nconst date           = formData.date;\nconst selectedServices = Array.isArray(formData.services) ? formData.services : [formData.services];\nconst discountLevel  = formData.discountLevel; // 'Discount', 'Retail', or 'Premium'\nconst requestorInfo  = formData.requestorName; // e.g. \"Jane Smith (user@example.com)\"\n\n// Extract email from requestor info\nconst emailMatch    = requestorInfo.match(/\\(([^)]+)\\)/);\nconst requestorEmail = emailMatch ? emailMatch[1] : '';\nconst requestorName  = requestorInfo.replace(/\\s*\\([^)]*\\)/, '').trim();\n\n// Get pricing data from Excel node\n// Expected columns: Title, Description, Cost, Discount, Retail, Premium, Unit\nconst pricingData = $('Fetch Pricing Sheet').all();\n\n// Filter pricing data for selected services (match on 'Title' column)\nconst filteredPricing = pricingData.filter(item => {\n  const service = item.json.Title;\n  return selectedServices.includes(service);\n});\n\n// Map pricing based on discount level\nconst processedPricing = filteredPricing.map(item => {\n  const data = item.json;\n  let price;\n  switch (discountLevel) {\n    case 'Discount': price = data.Discount; break;\n    case 'Premium':  price = data.Premium;  break;\n    default:         price = data.Retail;\n  }\n  return {\n    service:     data.Title,\n    description: data.Description,\n    cost:        data.Cost,\n    price:       price,\n    unit:        data.Unit\n  };\n});\n\n// Calculate total\nconst total = processedPricing.reduce((sum, item) => sum + (parseFloat(item.price) || 0), 0);\n\nreturn [{\n  json: {\n    prospectName,\n    date,\n    requestorName,\n    requestorEmail,\n    discountLevel,\n    selectedServices,\n    pricingItems: processedPricing,\n    total: total.toFixed(2)\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ce1c5eb0-8bc9-432f-954e-079bae0b95b3",
      "name": "Build HTML Proposal",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        256
      ],
      "parameters": {
        "jsCode": "/**\n * Pricing Proposal \u2013 HTML Builder\n * Replace the variables below with your own branding before activating.\n */\n\nconst data = $json;\n\n// \u2500\u2500 BRANDING \u2013 update these \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst logoUrl    = 'YOUR_LOGO_URL';          // e.g. https://example.com/logo.png\nconst companyName = 'Your Company Name';\nconst tagline    = 'A division of Your Parent Company';\nconst colorNavy  = '#002657';\nconst colorBlue  = '#0021a5';\nconst colorOrange = '#fa4616';\nconst colorGold  = '#f2a900';\nconst colorDark  = '#343741';\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst displayDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });\nconst validUntil  = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });\n\nconst pricingRows = data.pricingItems.map((item, index) =>\n  '<tr style=\"' + (index % 2 === 0 ? '' : 'background-color:#f9f9f9;') + '\">'\n  + '<td style=\"padding:12px;border-bottom:1px solid #eee;font-weight:bold;color:' + colorNavy + ';\">' + item.service + '</td>'\n  + '<td style=\"padding:12px;border-bottom:1px solid #eee;font-size:11px;\">' + item.description + '</td>'\n  + '<td style=\"padding:12px;border-bottom:1px solid #eee;text-align:center;\">' + item.unit + '</td>'\n  + '<td style=\"padding:12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold;color:' + colorBlue + ';\">$' + parseFloat(item.price).toFixed(2) + '</td>'\n  + '</tr>'\n).join('');\n\nconst htmlContent = '<!DOCTYPE html>'\n+ '<html lang=\"en\"><head><meta charset=\"UTF-8\">'\n+ '<title>Pricing Proposal - ' + data.prospectName + '</title>'\n+ '<style>'\n+ '@page{size:letter;margin:15mm;}'\n+ 'body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;line-height:1.5;color:' + colorDark + ';margin:0;padding:0;}'\n+ '.header{display:flex;justify-content:space-between;align-items:center;border-bottom:4px solid ' + colorOrange + ';padding-bottom:20px;margin-bottom:30px;}'\n+ '.logo{height:70px;width:auto;}'\n+ '.proposal-title{font-size:32px;font-weight:900;color:' + colorNavy + ';margin:10px 0;text-transform:uppercase;letter-spacing:-1px;}'\n+ '.cover-info{background-color:#fcfcfc;padding:25px;border-radius:4px;border:1px solid #eee;margin:20px 0;}'\n+ '.info-row{display:flex;margin:8px 0;font-size:14px;}'\n+ '.label{font-weight:bold;color:' + colorBlue + ';width:150px;text-transform:uppercase;font-size:11px;}'\n+ '.pricing-table{width:100%;border-collapse:collapse;margin:25px 0;}'\n+ '.pricing-table th{background-color:' + colorNavy + ';color:white;padding:15px 12px;text-align:left;text-transform:uppercase;font-size:11px;}'\n+ '.legal-section{margin-top:40px;padding:20px;background-color:#fffcf5;border-left:5px solid ' + colorGold + ';font-size:12px;line-height:1.7;}'\n+ '.signature-section{margin-top:40px;padding:25px;background-color:#f9f9f9;border-radius:4px;}'\n+ '.sig-line{border-bottom:2px solid ' + colorDark + ';display:inline-block;width:100%;margin-top:30px;}'\n+ '</style></head><body>'\n+ '<div class=\"header\">'\n+ '<img src=\"' + logoUrl + '\" class=\"logo\" alt=\"' + companyName + '\">'\n+ '<div style=\"text-align:right;color:' + colorNavy + ';font-size:10px;font-weight:bold;\">' + tagline + '</div>'\n+ '</div>'\n+ '<div class=\"proposal-title\">Pricing Proposal</div>'\n+ '<div class=\"cover-info\">'\n+ '<div class=\"info-row\"><span class=\"label\">Prepared For:</span><span><strong>' + data.prospectName + '</strong></span></div>'\n+ '<div class=\"info-row\"><span class=\"label\">Date:</span><span>' + displayDate + '</span></div>'\n+ '<div class=\"info-row\"><span class=\"label\">Prepared By:</span><span>' + data.requestorName + '</span></div>'\n+ '<div class=\"info-row\"><span class=\"label\">Valid Until:</span><span>' + validUntil + '</span></div>'\n+ '</div>'\n+ '<div>'\n+ '<h2 style=\"color:' + colorNavy + ';border-bottom:2px solid ' + colorOrange + ';padding-bottom:10px;font-size:18px;\">Proposed Services &amp; Pricing</h2>'\n+ '<table class=\"pricing-table\"><thead><tr>'\n+ '<th>Service</th><th>Description</th><th style=\"text-align:center;\">Unit</th><th style=\"text-align:right;\">Price</th>'\n+ '</tr></thead><tbody>' + pricingRows + '</tbody></table>'\n+ '</div>'\n+ '<div class=\"legal-section\">'\n+ '<h3 style=\"color:' + colorNavy + ';margin-top:0;font-size:14px;\">Terms &amp; Conditions</h3>'\n+ '<p>This pricing proposal is valid for <strong>thirty (30) days</strong>. After this period, all prices are subject to change. A binding contract is only created upon execution of a separate Services Agreement.</p>'\n+ '</div>'\n+ '<div class=\"signature-section\">'\n+ '<h3 style=\"color:' + colorNavy + ';margin-top:0;font-size:14px;\">Authorization &amp; Acceptance</h3>'\n+ '<table style=\"width:100%;margin-top:20px;\"><tr>'\n+ '<td style=\"width:60%;padding-right:20px;\"><div class=\"sig-line\"></div><div style=\"font-size:10px;color:' + colorDark + ';margin-top:5px;\">Client Authorized Signature</div></td>'\n+ '<td style=\"width:30%;\"><div class=\"sig-line\"></div><div style=\"font-size:10px;color:' + colorDark + ';margin-top:5px;\">Date</div></td>'\n+ '</tr><tr><td colspan=\"2\" style=\"padding-top:20px;\">'\n+ '<div class=\"sig-line\" style=\"width:60%;\"></div>'\n+ '<div style=\"font-size:10px;color:' + colorDark + ';margin-top:5px;\">Printed Name &amp; Title</div>'\n+ '</td></tr></table></div>'\n+ '</body></html>';\n\nconst htmlBuffer = Buffer.from(htmlContent, 'utf-8');\n\nreturn [{\n  binary: {\n    data: {\n      data: htmlBuffer.toString('base64'),\n      mimeType: 'text/html',\n      fileName: 'index.html',\n      fileExtension: 'html'\n    }\n  },\n  json: {\n    htmlContent:    htmlContent,\n    fileName:       data.prospectName.replace(/[^a-zA-Z0-9]/g, '-') + '-Pricing-Proposal-' + new Date().toISOString().split('T')[0] + '.pdf',\n    prospectName:   data.prospectName,\n    requestorEmail: data.requestorEmail,\n    requestorName:  data.requestorName\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a491e6b8-873f-4fb0-88a4-9d1d6b49c7d0",
      "name": "Gotenberg \u2192 PDF",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1280,
        256
      ],
      "parameters": {
        "url": "https://YOUR_GOTENBERG_INSTANCE/forms/chromium/convert/html",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "index.html",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "=data"
            }
          ]
        },
        "genericAuthType": "httpBasicAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Gotenberg-Output-Filename",
              "value": "={{ $('Build HTML Proposal').item.json.fileName }}"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "01262f19-5231-446e-8ea3-4d2bdf0ea0e4",
      "name": "Send a message",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        1472,
        256
      ],
      "parameters": {
        "subject": "=Pricing Proposal prepared for {{ $('Build HTML Proposal').item.json.prospectName }}\n",
        "bodyContent": "=Please find the attached pricing proposal for {{ $('Build HTML Proposal').item.json.prospectName }}, requested by {{ $('Build HTML Proposal').item.json.requestorName }}. The attached pricing is valid for 30 days.\n",
        "toRecipients": "=YOUR_INTERNAL_EMAIL;{{ $('Build HTML Proposal').item.json.requestorEmail }}\n",
        "additionalFields": {
          "attachments": {
            "attachments": [
              {
                "binaryPropertyName": "data"
              }
            ]
          }
        }
      },
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "69b14a64-11a4-40a0-8f23-4bebf29fb3c3",
  "connections": {
    "Process & Price": {
      "main": [
        [
          {
            "node": "Build HTML Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gotenberg \u2192 PDF": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build HTML Proposal": {
      "main": [
        [
          {
            "node": "Gotenberg \u2192 PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Pricing Sheet": {
      "main": [
        [
          {
            "node": "Process & Price",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pricing Request Webhook": {
      "main": [
        [
          {
            "node": "Fetch Pricing Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}