生成付款发票是任何企业的一个重要方面,数字发票已成为各个行业的常见做法。在这种情况下,Web 应用程序开发人员经常面临以编程方式生成和发送 PDF 发票的任务。
无论您是自动化发票生成和通知流程,还是开发图形用户界面 (GUI) 来主动提醒客户未结发票,最初的技术挑战都在于生成 PDF 发票。虽然您可以开发用于 PDF 生成的自定义脚本,但这将是一项艰巨的任务。虽然基于网络的服务很方便,但如果您与客户签订了保密协议,则它们可能不适合,因为通过互联网将数据发送到第三方服务可能会引起担忧。
幸运的是,福昕
本教程将指导您完成创建
在本教程中,我们将引导您完成构建 Web 应用程序的过程,该应用程序使您的计费部门能够有效地管理未付发票。您将创建各种功能,包括用于跟进的内部工具、显示未结发票的页面以及每个发票的预览页面。用户可以选择向客户发送电子邮件提醒并附上发票。
对于这个项目,我们将利用
先决条件:
要创建新的样板 Express Web 应用程序,您需要使用应用程序生成器:
npx express-generator --git --view=hbs
这将创建一个带有.gitignore
文件的 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); ...
此应用程序中的大部分工作都在发票路由器内完成。
在创建路线之前,您需要一些可以使用的数据。在实际应用程序中,您可能会连接到数据库,但出于演示目的,请将发票数据添加到 JSON 文件中。
在/data/invoices.json
创建一个新文件并添加以下内容:
[ { "id": "47427759-9362-4f8e-bfe4-2d3733534e83", "customer": "Bins and Sons", "contact_name": "Verne McKim", "contact_email": "[email protected]", "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": "[email protected]", "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": "[email protected]", "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.hbs
和invoice-single.hbs
。
另外,将以下内容添加到您的invoice-list.hbs
文件中:
<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.hbs
文件并添加以下内容:
<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>
将样式添加到应用程序的样式表并合并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/[email protected]/build/pure-min.css" integrity="sha384-cg6SkqEOCV1NbJoCu11+bm0NvBRc8IYLRGXkmNrqUBfTjmMYwNKPWBTIKyw9mHNJ" 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 file
并添加以下 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; }
虽然不是强制性的,但向发票添加样式可以增强其专业外观,因为福昕在生成 PDF 时会捕获 HTML 文档中的所有样式。
现在,您已准备好测试您的应用程序。请在命令行界面中run npm
start 并打开 Web 浏览器到localhost:3000/invoices
您将看到类似于以下内容的发票列表:
单击“查看”可预览每张发票。
在最后两个步骤中,您将使用 Foxit__ HTML 到 PDF 工具__ 生成 PDF 发票。生成发票后,您将在发送之前使用 Nodemailer 将其附加到电子邮件中。
Foxit的SDK提供了一系列用于PDF创建和操作的功能,包括从HTML文档或URL生成PDF文件的能力。下载 HTML 并将其编译为 PDF 可执行文件的过程可用
Node 的child_process
库包含一个名为exec()
的函数,使您能够执行命令行函数。此方法对于从 Foxit 运行 C++ 可执行文件很有用。要执行 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} -html /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 发票了,最后一步是将这些发票发送给您的客户。
sendmail
命令作为替代方案。
要测试 Nodemailer,您可以使用/invoices/:id/email
路径中的“PDF generated and save to...” console.log
语句下方添加以下代码片段:
... // Construct the message const message = { from: '[email protected]', 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'); } }); ...
请刷新您的节点应用程序,然后单击任何发票的“电子邮件提醒”。这次,您将观察到在控制台中显示为 JSON 的完整电子邮件数据对象。
{ "from": { "address": "[email protected]", "name": "" }, "to": [ { "address": "[email protected]", "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": "<[email protected]>" }
attachments.content
字符串表示编码的 PDF 文件。为了简洁起见,我在上面的示例中截断了它。
使用真实的电子邮件测试电子邮件createTransport({jsonTransport: true})
调用替换为以下代码片段:
createTransport({ host: "smtp.mailtrap.io", port: 2525, auth: { user: "<YOUR_MAILTRAP_USERID>", pass: "<YOUR_MAILTRAP_PASS>" } })
通过电子邮件发送发票后,Mailtrap 将捕获输出并为您提供下载 PDF 附件的选项。如果您访问 Mailtrap 帐户,您将看到一封类似于以下示例的电子邮件:
准备好将应用程序部署到生产环境后,请将 Mailtrap SMTP 凭据替换为生产邮件服务器。我们的网络应用程序现在能够根据我们的计费团队的要求生成 PDF 发票并自动发送给客户。
如果您需要在线提供发票并将其作为 PDF 发送的解决方案,我们的应用程序可以提供可靠的基础。虽然福昕的 HTML 转 PDF 工具是生成 PDF 的便捷高效的选项,但值得注意的是,它们还提供各种其他解决方案。和他们的
也发布在这里。