Generate Invoices from Word Templates
This guide show how to create professional DOCX invoices with line items, calculations, and conditional sections.
You will learn to use many of DocStencil's advanced features. This makes this tutorial a great starting point for your own templates.
For a more minimal example have a look at Spring Boot Quick Start.
Add the Dependency
- Maven
- Gradle (Kotlin)
- Gradle (Groovy)
<dependency>
<groupId>com.docstencil</groupId>
<artifactId>docstencil-core</artifactId>
<version>0.2.1</version>
</dependency>
implementation("com.docstencil:docstencil-core:0.2.1")
implementation 'com.docstencil:docstencil-core:0.2.1'
The Template
Download the invoice.docx template file or create it yourself using Microsoft Word or LibreOffice:
INVOICE
From: {invoice.company.name}, {invoice.company.address}
Bill To: {invoice.customer.name} ({invoice.customer.email})
Invoice #: {invoice.invoiceNumber}
Date: {$format(invoice.invoiceDate, "MMMM d, yyyy")}
{if invoice.paid}Status: PAID{end}
| # | Description | Qty | Unit Price | Amount |
|---|---|---|---|---|
| {for item in $enumerate(invoice.items)}{item.index + 1} | {item.value.description} | {item.value.quantity} | {$format(item.value.unitPrice, "#,##0.00")} | {$format(item.value.quantity * item.value.unitPrice, "#,##0.00")}{end} |
Subtotal: {$format(invoice.subtotal, "#,##0.00")}
Tax ({invoice.taxRate}%): {$format(invoice.taxAmount, "#,##0.00")}
Total: {$format(invoice.total, "#,##0.00")}
Payment Terms: Due within {invoice.paymentTermsDays} days.
Bank: {invoice.company.bankName}: Account: {invoice.company.accountNumber}
Thank you for your business!
Table Row Loops
Place {for...}{end} inside two different cells of a table row to repeat the entire row for each item:
| {for item in items}{item.name} | {item.price}{end} |
Use $enumerate() to wrap items with their index to get row numbers:
| {for item in $enumerate(items)}{item.index + 1} | {item.value.name}{end} |
The enumerated item provides:
item.index: 0, 1, 2, ...item.value: the original objectitem.isFirst,item.isLast: boolean flags
Conditionals
Show content based on conditions:
{if paid}PAID{end} // Shows when paid is true
{if !paid}Payment due{end} // Shows when paid is false
Formatting
Numbers:
{$format(price, "#,##0.00")} → 1,234.56
{$format(qty, "#,##0")} → 1,235
Dates:
{$format(date, "MMMM d, yyyy")} → January 15, 2025
{$format(date, "MM/dd/yyyy")} → 01/15/2025
Data Model
- Kotlin
- Java
data class LineItem(
val description: String,
val quantity: Int,
val unitPrice: BigDecimal
)
data class Invoice(
val invoiceNumber: String,
val invoiceDate: LocalDate,
val company: Company,
val customer: Customer,
val items: List<LineItem>,
val taxRate: Int,
val paymentTermsDays: Int,
val paid: Boolean = false
) {
val subtotal: BigDecimal
get() = items.fold(BigDecimal.ZERO) { acc, item ->
acc + (item.unitPrice * item.quantity.toBigDecimal())
}
val taxAmount: BigDecimal
get() = subtotal * (taxRate.toBigDecimal() / BigDecimal(100))
val total: BigDecimal
get() = subtotal + taxAmount
}
public record LineItem(String description, int quantity, BigDecimal unitPrice) {}
public class Invoice {
private final String invoiceNumber;
private final LocalDate invoiceDate;
private final Company company;
private final Customer customer;
private final List<LineItem> items;
private final int taxRate;
private final int paymentTermsDays;
private final boolean paid;
public Invoice(
String invoiceNumber,
LocalDate invoiceDate,
Company company,
Customer customer,
List<LineItem> items,
int taxRate,
int paymentTermsDays,
boolean paid) {
this.invoiceNumber = invoiceNumber;
this.invoiceDate = invoiceDate;
this.company = company;
this.customer = customer;
this.items = items;
this.taxRate = taxRate;
this.paymentTermsDays = paymentTermsDays;
this.paid = paid;
}
// Getters for all fields...
public String getInvoiceNumber() { return invoiceNumber; }
public LocalDate getInvoiceDate() { return invoiceDate; }
public Company getCompany() { return company; }
public Customer getCustomer() { return customer; }
public List<LineItem> getItems() { return items; }
public int getTaxRate() { return taxRate; }
public int getPaymentTermsDays() { return paymentTermsDays; }
public boolean isPaid() { return paid; }
// Computed properties
public BigDecimal getSubtotal() {
return items.stream()
.map(item -> item.unitPrice().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal getTaxAmount() {
return getSubtotal()
.multiply(BigDecimal.valueOf(taxRate))
.divide(BigDecimal.valueOf(100));
}
public BigDecimal getTotal() {
return getSubtotal().add(getTaxAmount());
}
}
DocStencil calls getters automatically. Even computed properties like subtotal work seamlessly.
The Service
- Kotlin
- Java
@Service
class InvoiceService {
private val template = OfficeTemplate.fromResource("templates/invoice.docx")
fun generate(invoice: Invoice): ByteArray {
return template.render(mapOf("invoice" to invoice)).toByteArray()
}
}
@Service
public class InvoiceService {
private final OfficeTemplate template =
OfficeTemplate.fromResource("templates/invoice.docx");
public byte[] generate(Invoice invoice) {
return template.render(Map.of("invoice", invoice)).toByteArray();
}
}
Since we pass the invoice object directly, the template accesses properties via {invoice.invoiceNumber}, {invoice.company.name}, etc. DocStencil uses reflection to access object properties automatically.
The Controller
- Kotlin
- Java
@RestController
@RequestMapping("/api/invoices")
class InvoiceController(private val invoiceService: InvoiceService) {
@GetMapping("/{id}/download")
fun download(@PathVariable id: String): ResponseEntity<ByteArray> {
val invoice = createSampleInvoice(id)
val bytes = invoiceService.generate(invoice)
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"invoice-$id.docx\"")
.contentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
))
.body(bytes)
}
private fun createSampleInvoice(id: String): Invoice {
return Invoice(
invoiceNumber = "INV-$id",
invoiceDate = LocalDate.now(),
company = Company("Acme Corp", "123 Business Ave, New York, NY", "First National Bank", "1234567890"),
customer = Customer("John Smith", "john@example.com"),
items = listOf(
LineItem("Web Development", 40, BigDecimal("150.00")),
LineItem("UI Design", 20, BigDecimal("125.00"))
),
taxRate = 10,
paymentTermsDays = 30
)
}
}
@RestController
@RequestMapping("/api/invoices")
public class InvoiceController {
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> download(@PathVariable String id) {
Invoice invoice = createSampleInvoice(id);
byte[] bytes = invoiceService.generate(invoice);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"invoice-" + id + ".docx\"")
.contentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
))
.body(bytes);
}
private Invoice createSampleInvoice(String id) {
return new Invoice(
"INV-" + id,
LocalDate.now(),
new Company("Acme Corp", "123 Business Ave, New York, NY", "First National Bank", "1234567890"),
new Customer("John Smith", "john@example.com"),
List.of(
new LineItem("Web Development", 40, new BigDecimal("150.00")),
new LineItem("UI Design", 20, new BigDecimal("125.00"))
),
10,
30,
false
);
}
}
Test It
curl "http://localhost:8080/api/invoices/2026-001/download" -o invoice.docx
Open invoice.docx to see that your placeholders are replaced with real values.
View the complete working example on GitHub:
Next Steps
- Template Language - Complete syntax reference