Zum Inhalt springen

Jinja2PDF

US-Rechnung mit Sales Tax

Papierformat Letter, Dollar und US-Datumsangaben aus der Locale en-US der Vorlage. Die Sales Tax gilt nur für steuerpflichtige Positionen, eine Anzahlung verringert den offenen Betrag, und Status und QR-Code zum Bezahlen folgen aus dem, was noch offen ist.

Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.

Gerendertes Ergebnis

Gerendertes Ergebnis: US-Rechnung mit Sales Tax
PDF · 1 Seite · 141 KB ↓ herunterladen

Was dieses Beispiel zeigt

  • paper.format ist Letter und die Locale en-US, money gibt also $11,278.46 aus und date('MMMM d, yyyy') September 28, 2026.
  • Die Sales Tax gilt nur für Positionen mit taxable: true: where('taxable', true) | sum('amount') sind die $1,565.00, auf die 7,25 % fallen, und round(2) legt die Steuer auf $113.46 fest, bevor sie addiert wird.
  • payments ist eine Liste; eine Anzahlung und jede spätere Teilzahlung bekommen je eine Zeile unter der Summe, und offen ist, was übrig bleibt: $8,278.46.
  • Status und QR-Code folgen aus dem offenen Betrag: „Paid“ bei null, „Partially paid“ nach einer Anzahlung und kein Bezahl-Code mehr, wenn nichts offen ist.
  • dateAdd(invoice.terms_days, 'days') macht aus Net 30 das Fälligkeitsdatum October 28, 2026, und default('—') ersetzt eine fehlende Bestellnummer.

Verwendete Helfer

Mehr zu diesem Anwendungsfall: Rechnungs-PDF-API →

Vorlagenordner

templates/invoice-us/template.htmlJinja2
{% set subtotal = invoice.lines | sum('amount') %}
{% set taxable = invoice.lines | where('taxable', true) | sum('amount') %}
{% set tax = (taxable * invoice.tax_rate / 100) | round(2) %}
{% set total = subtotal + tax %}
{% set paid = invoice.payments | sum('amount') %}
{% set balance = total - paid %}

<header class="top">
  <div>
    <p class="brand">{{ company.name }}</p>
    <p class="muted">{{ company.street }}<br>{{ company.city }}</p>
  </div>
  <div class="title">
    <h1>Invoice</h1>
    <p class="muted">{{ invoice.number }}</p>
    {% if balance <= 0 %}<p class="status paid">Paid</p>{% elif paid > 0 %}<p class="status partial">Partially paid</p>{% endif %}
  </div>
</header>

<section class="parties">
  <div>
    <h2>Bill to</h2>
    <p><strong>{{ customer.name }}</strong><br>Attn: {{ customer.contact }}<br>{{ customer.street }}<br>{{ customer.city }}</p>
  </div>
  <dl>
    <div><dt>Invoice date</dt><dd>{{ invoice.date | date('MMMM d, yyyy') }}</dd></div>
    <div><dt>Terms</dt><dd>Net {{ invoice.terms_days }}</dd></div>
    <div><dt>Due date</dt><dd>{{ invoice.date | dateAdd(invoice.terms_days, 'days') | date('MMMM d, yyyy') }}</dd></div>
    <div><dt>PO number</dt><dd>{{ invoice.po | default('—') }}</dd></div>
  </dl>
</section>

<table class="lines">
  <thead>
    <tr><th>Description</th><th class="num">Qty</th><th class="num">Rate</th><th class="num">Amount</th></tr>
  </thead>
  <tbody>
    {% for line in invoice.lines %}
    <tr>
      <td>{{ line.description }}{% if line.taxable %} <span class="taxed">T</span>{% endif %}</td>
      <td class="num">{{ line.qty | number }}</td>
      <td class="num">{{ line.rate | money }}</td>
      <td class="num">{{ line.amount | money }}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>

<section class="summary">
  <div class="pay">
    {% if balance > 0 %}
    <img src="{{ qrcode(invoice.pay_url, { size: 96 }) }}" alt="Pay online">
    <p><strong>Pay online</strong><br>Scan the code or open<br>{{ invoice.pay_url | replace('https://', '') }}</p>
    {% endif %}
  </div>
  <table class="totals">
    <tr><td>Subtotal</td><td class="num">{{ subtotal | money }}</td></tr>
    <tr><td>Sales tax {{ invoice.tax_rate }}% on {{ taxable | money }} <span class="taxed">T</span></td><td class="num">{{ tax | money }}</td></tr>
    <tr class="total"><td>Total</td><td class="num">{{ total | money }}</td></tr>
    {% for payment in invoice.payments %}
    <tr><td>Paid {{ payment.date | date('MMM d') }}, {{ payment.method }}</td><td class="num">−{{ payment.amount | money }}</td></tr>
    {% endfor %}
    <tr class="due"><td>Balance due</td><td class="num">{{ balance | money }}</td></tr>
  </table>
</section>

<p class="notes">{{ invoice.notes }}</p>
templates/invoice-us/style.cssCSS
body { font-family: 'Inter', sans-serif; font-size: 9.5pt; color: #1e293b; }
.top { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 7mm; border-bottom: 2px solid #0f172a; }
.brand { margin: 0; font-size: 15pt; font-weight: 700; color: #0f172a; }
.muted { margin: 1mm 0 0; color: #64748b; line-height: 1.5; }
.title { text-align: right; }
h1 { margin: 0; font-size: 24pt; letter-spacing: -0.01em; }
.status { display: inline-block; margin: 2mm 0 0; padding: 1mm 2.5mm; border-radius: 1mm; font-size: 7.5pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.status.paid { background: #dcfce7; color: #166534; }
.status.partial { background: #fef3c7; color: #92400e; }
.parties { display: flex; justify-content: space-between; margin: 8mm 0; }
h2 { margin: 0 0 1.5mm; font-size: 7.5pt; letter-spacing: 0.08em; text-transform: uppercase; color: #64748b; }
.parties p { margin: 0; line-height: 1.5; }
dl { margin: 0; }
dl div { display: flex; justify-content: space-between; gap: 10mm; padding: 0.6mm 0; }
dt { color: #64748b; }
dd { margin: 0; font-weight: 600; }
table { border-collapse: collapse; }
.lines { width: 100%; }
.lines th { padding: 2mm 1.5mm; border-bottom: 1.5px solid #0f172a; text-align: left; font-size: 7.5pt; letter-spacing: 0.05em; text-transform: uppercase; color: #64748b; }
.lines td { padding: 2.4mm 1.5mm; border-bottom: 1px solid #e2e8f0; }
.num { text-align: right !important; white-space: nowrap; }
.taxed { display: inline-block; padding: 0 1mm; border: 1px solid #94a3b8; border-radius: 0.8mm; font-size: 6.5pt; color: #475569; }
.summary { display: flex; justify-content: space-between; align-items: flex-start; gap: 10mm; margin-top: 6mm; break-inside: avoid; }
.pay { display: flex; align-items: center; gap: 4mm; }
.pay img { width: 25mm; height: 25mm; }
.pay p { margin: 0; font-size: 8pt; line-height: 1.5; color: #475569; }
.totals { width: 88mm; }
.totals td { padding: 1.3mm 1.5mm; }
.totals .total td { border-top: 1.5px solid #0f172a; font-weight: 700; }
.totals .due td { border-top: 1px solid #cbd5e1; font-size: 12pt; font-weight: 700; color: #0f172a; }
.notes { margin-top: 10mm; font-size: 8pt; color: #64748b; }
templates/invoice-us/head.htmlHTML
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap">
templates/invoice-us/footer.htmlJinja2
<div style="width:100%;display:flex;justify-content:space-between;font-size:7.5px;color:#64748b">
  <span>{{ company.name }} · EIN {{ company.ein }} · {{ company.email }}</span>
  <span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>
templates/invoice-us/settings.jsonJSON
{
  "pdf": {
    "metadata": {
      "title": "Invoice {{ invoice.number }}"
    }
  },
  "paper": {
    "format": "Letter"
  },
  "margin": {
    "top": "0.6in",
    "right": "0.7in",
    "bottom": "0.8in",
    "left": "0.7in"
  },
  "locale": "en-US",
  "timezone": "America/New_York",
  "currency": "USD",
  "footer": {
    "height": "0.5in"
  }
}
templates/invoice-us/data/default.jsonJSON
{
  "company": {
    "name": "Fennlor Studio LLC",
    "street": "100 Example Street, Suite 200",
    "city": "Exampletown, NY 00000",
    "email": "billing@fennlor.example",
    "ein": "00-0000000"
  },
  "customer": {
    "name": "Olvarest Inc.",
    "contact": "John Doe",
    "street": "200 Example Avenue",
    "city": "Exampleville, NY 00000"
  },
  "invoice": {
    "number": "INV-2026-0107",
    "date": "2026-09-28",
    "terms_days": 30,
    "po": "PO-55120",
    "tax_rate": 7.25,
    "pay_url": "https://pay.fennlor.example/i/INV-2026-0107",
    "lines": [
      {
        "description": "Template design, fixed fee",
        "qty": 1,
        "rate": 4800,
        "amount": 4800,
        "taxable": false
      },
      {
        "description": "Implementation, hours",
        "qty": 32,
        "rate": 150,
        "amount": 4800,
        "taxable": false
      },
      {
        "description": "Thermal label printer",
        "qty": 2,
        "rate": 649,
        "amount": 1298,
        "taxable": true
      },
      {
        "description": "Label rolls, 4 × 6 in, case of 12",
        "qty": 3,
        "rate": 89,
        "amount": 267,
        "taxable": true
      }
    ],
    "payments": [
      {
        "date": "2026-09-02",
        "method": "ACH",
        "amount": 3000
      }
    ],
    "notes": "Please pay by ACH or card through the link above and quote the invoice number. Thank you for your business."
  }
}

Über die API rendern

Speichern Sie die Beispieldaten als data.json und senden Sie sie mit dem Namen der Vorlage. TypeScript und Python nutzen die SDKs; jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek an die REST-API. Renderings mit einem Schlüssel, der mit ff_test_ beginnt, werden nicht gezählt; im Free-Tarif tragen sie ein kleines Wasserzeichen.

render.shShell
jq '{template: "invoice-us", data: ., filename: "invoice-INV-2026-0107.pdf"}' data.json \
  | curl https://api-eu.formfeed.dev/v1/renders \
      -H "Authorization: Bearer ff_test_..." \
      -H "Content-Type: application/json" \
      --data-binary @-
render.tsTypeScript
import { Formfeed } from '@formfeed/sdk';
import data from './data.json' with { type: 'json' };

const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });

const render = await formfeed.renders.create({
  template: 'invoice-us',
  data,
  filename: 'invoice-INV-2026-0107.pdf',
});
console.log(render.status, render.download_url);
render.pyPython
import json
import os

from formfeed import Formfeed

formfeed = Formfeed(api_key=os.environ["FORMFEED_API_KEY"])

with open("data.json", encoding="utf-8") as file:
    data = json.load(file)

render = formfeed.renders.create(
    template="invoice-us",
    data=data,
    filename="invoice-INV-2026-0107.pdf",
)
print(render.status, render.download_url)
render.phpPHP
<?php
$body = [
    'template' => 'invoice-us',
    'data' => json_decode(file_get_contents('data.json'), true),
    'filename' => 'invoice-INV-2026-0107.pdf',
];

$curl = curl_init('https://api-eu.formfeed.dev/v1/renders');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('FORMFEED_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
]);
$render = json_decode(curl_exec($curl), true);

echo $render['status'], ' ', $render['download_url'], PHP_EOL;
main.goGo
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	data, err := os.ReadFile("data.json")
	if err != nil {
		panic(err)
	}
	body, _ := json.Marshal(map[string]any{
		"template": "invoice-us",
		"data":     json.RawMessage(data),
		"filename": "invoice-INV-2026-0107.pdf",
	})

	request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1/renders", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
	request.Header.Set("Content-Type", "application/json")

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()

	var render struct {
		Status      string `json:"status"`
		DownloadURL string `json:"download_url"`
	}
	json.NewDecoder(response.Body).Decode(&render)
	fmt.Println(render.Status, render.DownloadURL)
}
Program.csC#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var body = new
{
    template = "invoice-us",
    data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
    filename = "invoice-INV-2026-0107.pdf",
};

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FORMFEED_API_KEY"));

var response = await http.PostAsJsonAsync("https://api-eu.formfeed.dev/v1/renders", body);
var render = await response.Content.ReadFromJsonAsync<JsonElement>();

Console.WriteLine($"{render.GetProperty("status")} {render.GetProperty("download_url")}");
Render.javaJava
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;

public class Render {
  public static void main(String[] args) throws Exception {
    String data = Files.readString(Path.of("data.json"));
    String body = """
        {
          "template": "invoice-us",
          "filename": "invoice-INV-2026-0107.pdf",
          "data": %s
        }
        """.formatted(data);

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1/renders"))
        .header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

    HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println(response.body()); // status, download_url, page_count, units …
  }
}
render.rbRuby
require "json"
require "net/http"

body = {
  template: "invoice-us",
  data: JSON.parse(File.read("data.json")),
  filename: "invoice-INV-2026-0107.pdf",
}

response = Net::HTTP.post(
  URI("https://api-eu.formfeed.dev/v1/renders"),
  JSON.generate(body),
  "Authorization" => "Bearer #{ENV.fetch("FORMFEED_API_KEY")}",
  "Content-Type" => "application/json"
)
render = JSON.parse(response.body)

puts render["status"], render["download_url"]

Heute mit dem Rendern beginnen

Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.