65.9K
CodeProject 正在变化。 阅读更多。
Home

如何创建 PDF 发票 Web 应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2021 年 11 月 12 日

CPOL

7分钟阅读

viewsIcon

12558

在本教程中,您将了解如何创建一个使用 Foxit PDF SDK 的 NodeJS 应用程序,该应用程序可将 Web 应用中的 HTML 发票转换为 PDF 发票。

收款是任何业务中最关键的功能之一,而数字发票正变得越来越普遍。有鉴于此,Web 应用程序开发人员经常被 T 务于以编程方式生成和发送 PDF 发票。

无论您是自动化发票生成和通知流程,还是构建一个允许您的团队主动提醒客户未付款发票的 GUI,您将面临的第一个技术障碍是生成 PDF 发票。虽然您可以编写自定义的 PDF 生成脚本,但这是一项巨大的工程。基于 Web 的服务很方便,但如果您与客户签订了保密协议,通过 Internet 向第三方服务发送数据可能会有问题。

幸运的是,Foxit 的 PDF 工具允许您快速安全地生成 PDF 文件。使用其HTML 转 PDF 转换器,您可以将任何 HTML 文档(包括发票)转换为 PDF 文件,然后您可以将其附加到电子邮件中或允许客户从您的 Web 应用程序下载。

在本教程中,您将学习如何创建一个 NodeJS 应用程序,该应用程序使用 Foxit PDF SDK 将 Web 应用中的 HTML 发票转换为 PDF 发票。创建完成后,您将使用 Nodemailer 通过 SMTP 将发票发送到客户的电子邮件地址。您可以按照下面的每个步骤操作,或者 [在 GitHub 上下载完成的代码库)。

访问 Foxit PDF SDK Web 演示,通过探索配置和功能来亲眼看看.

构建用于创建和发送 PDF 发票的 Web 应用程序

在本教程中,您将创建一个内部工具来帮助您的账单部门跟进未付款发票。您将创建一个列出所有未付款发票的页面,以及一个预览每张发票的页面。用户将能够单击一个链接,向每位客户发送带有附件发票的电子邮件提醒。

您将使用 Express Web 框架,Pure CSS 进行样式设置,以及 Nodemailer 发送电子邮件。

必备组件

创建新的 Express 应用

要创建一个新的样板 Express Web 应用程序,请使用 应用程序生成器

npx express-generator --git --view=hbs

这将创建一个带有 .gitignore 文件和 Handlebars 模板文件的 Web 应用程序。

接下来,添加 Nodemailer npm 包并安装 Express 的依赖项

npm i nodemailer && npm i

Express 生成的默认应用程序带有两个路由文件:/routes/index.js/routes/users.js。删除 users.js 路由并创建一个名为 invoices.js 的新路由文件。将此新路由添加到您的 app.js 文件中,并删除 usersRoute

    ...
    var indexRouter = require('./routes/index');
    var invoicesRouter = require('./routes/invoices');
    
    var app = express();
    ...
    app.use('/', indexRouter);
    app.use('/invoices', invoicesRouter);
    ...

invoices 路由器将是您在此应用程序中完成大部分工作的地方。

在创建路由之前,您需要一些数据。在实际应用程序中,您很可能会连接到数据库,但出于演示目的,您将把发票数据添加到 JSON 文件中。

/data/invoices.json 创建一个新文件并添加以下内容

    [
      {
        "id": "47427759-9362-4f8e-bfe4-2d3733534e83",
        "customer": "Bins and Sons",
        "contact_name": "Verne McKim",
        "contact_email": "vmckim0@example.com",
        "address": "3 Burning Wood Street",
        "city_state": "Memphis, TN 38118",
        "plan_id": "41595-5514",
        "plan_name": "Starter",
        "subtotal": 499.99,
        "fee": 50.00,
        "total": 549.99
      },
      {
        "id": "1afdd2fa-6353-437c-a923-e43baac506f4",
        "customer": "Koepp Group",
        "contact_name": "Junia Pretious",
        "contact_email": "jpretious1@example.com",
        "address": "7170 Fairfield Hill",
        "city_state": "Los Angeles, CA 90026",
        "plan_id": "43419-355",
        "plan_name": "Professional",
        "amount": 999.99,
        "fee": 50.00,
        "total": 1049.99
      },
      {
        "id": "59c216f8-7471-4ec2-a527-ab3641dc49aa",
        "customer": "Lynch-Bednar",
        "contact_name": "Evelin Stollenberg",
        "contact_email": "estollenberg2@example.com",
        "address": "9951 Erie Place",
        "city_state": "Chicago, IL 60605",
        "plan_id": "63323-714",
        "plan_name": "Starter",
        "amount": 499.99,
        "fee": 50.00,
        "total": 549.99
      }
    ]

这三张发票包含客户、计划和账单数据,这将帮助您在下一节中生成发票。

创建发票路由

routes/invoices.js 文件将在您的应用程序中创建三个新路由

  • /invoices - 来自上面平面数据文件的所有发票列表。
  • /invoices/:id - 发票预览,以便用户在发送给客户之前可以看到发票的外观。
  • /invoices/:id/email - 一个端点,用于生成 PDF 发票并将其发送给记录在案的联系电子邮件。

最后一个路由将在稍后处理,但您可以先添加前两个路由。打开 invoices.js 文件并添加以下内容

    const express = require('express');
    const router = express.Router();
    const invoices = require('../data/invoices.json');
    // Import exec to run the Foxit HTML to PDF executable
    const { exec } = require('child_process');
    // Import nodemailer to send emails
    const nodemailer = require('nodemailer');
    
    router.get('/', function(req, res) {
      res.render('invoice-list', {
        invoices: invoices,
        // Accepts errors and successes as query string arguments
        success: req.query['success'],
        error: req.query['error'],
      });
    });
    
    router.get('/:id', function(req, res) {
      const invoice = invoices.find(invoice => invoice.id === req.params['id']);
      // If the invoice doesn't exist, redirect the user back to the list page
      if (!invoice) {
        res.redirect('/invoices');
      }
      // Make the date format pretty
      const date = new Date().toLocaleDateString("en", {
        year:"numeric",
        day:"2-digit",
        month:"2-digit",
      });
      res.render('invoice-single', { invoice, date });
    });
    
    router.get('/:id/email', function(req, res) {
      // Coming soon.
    });
    
    module.exports = router;

您的应用程序已准备好进行测试,但首先,您需要创建两个视图文件。

添加视图和样式

Express 将逻辑和表示分离到 routes/views/ 中。在 views/ 目录中添加两个新文件:invoice-list.jsinvoice-single.js

将以下内容添加到您的 invoice-list.js 文件中

    <h1><a href="/invoices">Unpaid Invoices</a></h1>
    {{#if success}}
    <p class="success"><strong>Success!</strong> The invoice has been sent to the client.</p>
    {{/if}}
    {{#if error}}
    <p class="error"><strong>Whoops!</strong> Something went wrong and your invoice could not be sent.</p>
    {{/if}}
    {{#each invoices}}
    <h3>{{this.customer}}</h3>
    <p>ID: {{this.id}} <br/>
      <a href="/invoices/{{this.id}}">View</a> | <a href="/invoices/{{this.id}}/email">Email Reminder</a>
    </p>
    {{/each}}

打开 invoice-single.js 文件并添加此内容

    <div class="pure-g">
      <div class="pure-u-1-2">
        <h1>Invoice</h1>
      </div>
      <div class="pure-u-1-2" style="text-align: right;">
        <p class="muted">Issued on {{ date }}</p>
      </div>
    </div>
    <div class="pure-g">
      <div class="pure-u-1-2">
        <h3>Provider</h3>
        <p>
          <strong>Tiller, Inc.</strong><br/>
          1255 S. Clark<br/>
          Chicago, IL 60608
        </p>
      </div>
      <div class="pure-u-1-2" style="text-align: right;">
        <h3>Billed to</h3>
        <p>
          <strong>{{invoice.customer}}</strong><br/>
          {{invoice.contact_name}}<br/>
          {{invoice.address}}<br/>
          {{invoice.city_state}}
        </p>
      </div>
    </div>
    <table class="pure-table pure-table-horizontal">
      <thead>
      <tr>
        <th>ID</th>
        <th>Plan Name</th>
        <th class="text-right">Amount</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <td>{{invoice.plan_id}}</td>
        <td>{{invoice.plan_name}}</td>
        <td class="text-right">${{invoice.subtotal}}</td>
      </tr>
      <tr>
        <td></td>
        <td class="text-right">Subtotal:</td>
        <td class="text-right">${{invoice.subtotal}}</td>
      </tr>
      <tr>
        <td></td>
        <td class="text-right">Taxes and Fees:</td>
        <td class="text-right">${{invoice.fee}}</td>
      </tr>
      <tr class="bold">
        <td></td>
        <td class="text-right">Total:</td>
        <td class="text-right">${{invoice.total}}</td>
      </tr>
      </tbody>
    </table>
    <div class="footer">
      <p>Please make checks payable to <strong>Tiller, Inc</strong>. Invoices are due 30 days after date issued.</p>
      <p>Thank you for your business!</p>
    </div>

接下来,您需要为应用程序的样式表添加样式,并加载 Pure CSS 模块使其看起来不错。打开 views/layout.hbs 文件,用以下内容替换它以导入 Pure 并创建一个单列网格布局

    <!DOCTYPE html>
    <html>
      <head>
        <title>{{title}}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://unpkg.com/purecss@2.0.3/build/pure-min.css" crossorigin="anonymous">
        <link rel='stylesheet' href='/stylesheets/style.css' />
      </head>
      <body>
        <div class="container">
          <div class="pure-g">
            <div class="pure-u-1">
              {{{body}}}
            </div>
          </div>
        </div>
      </body>
    </html>

打开应用程序的 public/style.css 文件并添加以下内容

    body {
      background-color: #f7f7f7;
      color: #333333;
    }
    a {
      color: #156d6a;
    }
    h1 a,
    h2 a,
    h3 a {
      text-decoration: none;
    }
    table {
      width: 100%;
    }
    .container {
      background-color: #ffffff;
      max-width: 700px;
      margin: 0 auto;
      padding: 30px;
    }
    .muted {
      color: #999999;
    }
    .bold {
      font-weight: bold;
    }
    .text-right {
      text-align: right;
    }
    .footer p {
      margin-top: 30px;
    }
    .success {
      background-color: #c0f5f3;
      color: #0d928d;
      padding: 10px;
    }
    .error {
      background-color: #f5c0c0;
      color: #792525;
      padding: 10px;
    }

虽然您不必添加样式,但它会让您的发票看起来更专业,因为 Foxit 在生成 PDF 时会捕获 HTML 文档中的所有样式。

在浏览器中尝试我们的 SDK Web 演示,无需下载或登录.

此时,您已准备好测试应用程序。从命令行运行 npm start,然后在 Web 浏览器中打开 localhost:3000/invoices。您应该会看到如下的发票列表

单击“查看”以预览每张发票

在最后两个步骤中,您将使用 Foxit HTML 转 PDF 工具在通过 Nodemailer 将它们附加到电子邮件之前生成 PDF 发票。

使用 Foxit 生成 PDF

您可以使用 Foxit 的 SDK 进行各种 PDF 创建和操作操作,但一个常见用例是从 HTML 文档或 URL 生成 PDF 文件。下载和编译 HTML 到 PDF 可执行文件的过程在此处进行了文档记录。一旦您成功从命令行运行了演示,就可以继续。

Node 的 child_process 库包含一个名为 exec() 的函数,该函数允许您执行命令行函数。这是运行用 C++ 编写的 Foxit 可执行文件的便捷方法。要运行 HTML 转 PDF 可执行文件,请更新您的 /:id/email 路由,将其替换为以下内容

    ...
    
    router.get('/:id/email', function(req, res) {
      // Set the executable path and output folder
      const htmlToPdfPath = '/path/to/foxit/html2pdf';
      const outputFolder = __dirname + '/../invoices/';
    
      // Get the invoice
      const invoice = invoices.find(invoice => invoice.id === req.params['id']);
      if (!invoice) {
        res.redirect('/invoices?error=1');
      }
      
      // Convert the HTML to PDF
      exec(
        ${htmlToPdfPath} -h localhost:3000/invoices/${req.params['id']} -o ${outputFolder}${req.params['id']}.pdf,
        (err, stdout, stderr) => {
          if (err || stderr) {
            console.error(err, stderr);
            res.redirect('/invoices?error=1');
          } else {
            // For now: log the output file path
            console.log(PDF generated and saved to ${outputFolder}${req.params['id']}.pdf);
            res.redirect('/invoices?success=1');
          }
      });
    });

在运行此代码之前,请确保更新 htmlToPdfPath 以指向您的 htmltopdf 可执行文件。

返回发票列表并单击任何发票上的“发送电子邮件提醒”,Node 应用程序将调用 htmltopdf 可执行文件。该可执行文件将把您的发票从 Express 提供的 HTML 文档转换为 PDF 文件。您可以在 Web 应用程序的 invoices/ 目录中找到 PDF 文件。

既然您已经能够生成 PDF 发票,最后一步就是将这些发票发送给您的客户。

使用 Nodemailer 发送电子邮件

Nodemailer 提供了一个方便的接口来访问许多电子邮件传输层。SMTP 是最普遍的传输层之一,但您也可以使用 Amazon SES 或您的服务器的 sendmail 命令。

要测试 Nodemailer,您可以使用 [流传输的 JSON 选项,它允许您将消息记录到控制台。要设置消息并使用 Nodemailer 发送,请在 /invoices/:id/email 路由中的“PDF 已生成并保存到…”console.log 语句正下方添加以下内容

    ...
    // Construct the message
    const message = {
      from: 'accounting@example.com',
      to: invoice.contact_email,
      subject: 'Reminder: Your Invoice from Tiller, Inc. is Due',
      html: <p>Hey ${invoice.contact_name},</p><p>I just wanted to remind you that your invoice for last month's services is now due. I've attached it here for your convenience.</p><p>Thanks for your business!</p>,
      attachments: [
        {
          filename: 'invoice.pdf',
          path: ${outputFolder}${req.params['id']}.pdf,
        }
      ]
    };
    // Use mailer to send invoice
    nodemailer
      .createTransport({jsonTransport: true})
      .sendMail(message, function (err, info) {
        if (err) {
          res.redirect('/invoices?error=1');
        } else {
          console.log(info.message);
          res.redirect('/invoices?success=1');
        }
      });
    ...

刷新您的 Node 应用程序并单击任何发票上的“发送电子邮件提醒”。这一次,您将在控制台中看到整个电子邮件数据对象作为 JSON

    {
      "from": {
        "address": "accounting@example.com",
        "name": ""
      },
      "to": [
        {
          "address": "jpretious1@example.com",
          "name": ""
        }
      ],
      "subject": "Reminder: Your Invoice from Tiller, Inc. is Due",
      "html": "<p>Hey Junia Pretious,</p><p>I just wanted to remind you that your invoice for last month's services is now due. I've attached it here for your convenience.</p><p>Thanks for your business!</p>",
      "attachments": [
        {
          "content": "JVBERi0xLjMKJcTl8uXrp...",
          "filename": "invoice.pdf",
          "contentType": "application/pdf",
          "encoding": "base64"
        }
      ],
      "headers": {},
      "messageId": "<65ea9109-8d5a-295e-9295-8e98e1b2c667@example.com>"
    }

attachments.content 字符串是您的编码 PDF 文件,因此出于简洁起见,我在上面对其进行了截断。

要使用实际的 SMTP 服务器测试电子邮件,您可以使用 Mailtrap。假设您有一个帐户,请将 createTransport({jsonTransport: true}) 调用替换为以下内容

    createTransport({
      host: "smtp.mailtrap.io",
      port: 2525,
      auth: {
        user: "<YOUR_MAILTRAP_USERID>",
        pass: "<YOUR_MAILTRAP_PASS>"
      }
    })

现在,当您发送发票电子邮件时,Mailtrap 将捕获输出并允许您下载 PDF 附件。在您的 Mailtrap 帐户中,您应该会看到如下电子邮件

当您准备好将应用程序部署到生产环境时,请将 Mailtrap SMTP 凭据替换为生产邮件服务器。您的 Web 应用程序现在可以在您的账单团队想要跟进时生成 PDF 发票并发送给客户。

结论

如果您需要在线展示发票并将其作为 PDF 发送,以上应用程序应该能为您提供一个有用的起点。Foxit 的 HTML 转 PDF 工具是生成 PDF 的一种方便且性能良好的方法,但这并不是他们提供的唯一解决方案。

在您选择的平台(Web、Windows、Android、iOS、Linux、UWP 或 Mac)上尝试 Foxit PDF SDK 的先进技术。立即注册 免费的三十天试用

当您需要将 PDF 支持构建到您的 Web、移动或桌面应用程序中时,Foxit 是明确的选择。

© . All rights reserved.