A Go package for generating invoices, delivery notes, and quotations as PDF files, built on top of go-pdf/fpdf.
- Three document types: Invoice, Quotation, Delivery Note
- Per-item tax and discount (percentage or fixed amount)
- Named taxes with per-name breakdown in the totals block
- Document-level discount applied after item discounts
- Default tax applied automatically to items that have none
- Programmatic access to all totals (no need to build the PDF first)
- Custom header and footer with optional pagination
- Unicode support via a configurable translation function
- Fully customisable labels, colours, and currency formatting
- Output to file or
[]byte - Roboto font embedded by default — no external font files required
- WIP / Experimental: Optional Factur-X subpackage produces PDF/A-3B compliant e-invoices for all five profiles, verified with veraPDF and mustangproject
The generator code has moved to the generator subpackage. Update your imports:
-import generator "github.com/angelodlfrtr/go-invoice-generator"
+import generator "github.com/angelodlfrtr/go-invoice-generator/generator"The module path (github.com/angelodlfrtr/go-invoice-generator) is unchanged.
go get github.com/angelodlfrtr/go-invoice-generator/generatorpackage main
import (
"log"
"os"
generator "github.com/angelodlfrtr/go-invoice-generator/generator"
)
func main() {
doc, err := generator.New(generator.Invoice, &generator.Options{
TextTypeInvoice: "INVOICE",
CurrencySymbol: "$ ",
})
if err != nil {
log.Fatal(err)
}
doc.SetRef("INV-2024-001")
doc.SetDate("01/01/2024")
doc.SetPaymentTerm("01/02/2024")
doc.SetCompany(&generator.Contact{
Name: "Acme Corp",
Address: &generator.Address{
Address: "1 Market Street",
PostalCode: "94105",
City: "San Francisco",
Country: "USA",
},
})
doc.SetCustomer(&generator.Contact{
Name: "John Doe",
Address: &generator.Address{
Address: "42 Main Street",
PostalCode: "10001",
City: "New York",
Country: "USA",
},
})
doc.AppendItem(&generator.Item{
Name: "Consulting",
UnitCost: "150.00",
Quantity: "8",
Tax: &generator.Tax{Percent: "20"},
})
pdf, err := doc.Build()
if err != nil {
log.Fatal(err)
}
if err := pdf.OutputFileAndClose("invoice.pdf"); err != nil {
log.Fatal(err)
}
}| Constant | Value |
|---|---|
generator.Invoice |
"INVOICE" |
generator.Quotation |
"QUOTATION" |
generator.DeliveryNote |
"DELIVERY_NOTE" |
doc, err := generator.New(generator.Quotation, &generator.Options{})All fields are optional and have sensible defaults.
doc, err := generator.New(generator.Invoice, &generator.Options{
// Currency formatting
CurrencySymbol: "€ ", // default: "€ "
CurrencyPrecision: 2, // default: 2
CurrencyDecimal: ".", // default: "."
CurrencyThousand: " ", // default: " "
// Localised labels
TextTypeInvoice: "INVOICE",
TextTypeQuotation: "QUOTATION",
TextTypeDeliveryNote: "DELIVERY NOTE",
TextRefTitle: "Ref.",
TextVersionTitle: "Version",
TextDateTitle: "Date",
TextPaymentTermTitle: "Payment term",
TextItemsNameTitle: "Name",
TextItemsUnitCostTitle: "Unit price",
TextItemsQuantityTitle: "Qty",
TextItemsTotalHTTitle: "Total no tax",
TextItemsTaxTitle: "Tax",
TextItemsDiscountTitle: "Discount",
TextItemsTotalTTCTitle: "Total",
TextTotalTotal: "Total",
TextTotalDiscounted: "Total discounted",
TextTotalTax: "Tax",
TextTotalTaxOther: "Other", // label for unnamed taxes in breakdown (default: "Other")
TextTotalWithTax: "Total with tax",
// Colours (RGB)
BaseTextColor: []int{35, 35, 35},
GreyTextColor: []int{82, 82, 82},
GreyBgColor: []int{232, 232, 232},
DarkBgColor: []int{212, 212, 212},
// Font family name. Roboto is embedded and used by default; any font
// registered on the underlying fpdf instance can be used here.
Font: "Roboto", // default: "Roboto"
BoldFont: "Roboto", // default: "Roboto"
})Both the company and the customer are Contact values. A logo can be embedded
as a []byte (PNG or JPEG).
logoBytes, err := os.ReadFile("logo.png")
if err != nil {
log.Fatal(err)
}
doc.SetCompany(&generator.Contact{
Name: "Acme Corp",
Logo: logoBytes,
Address: &generator.Address{
Address: "1 Market Street",
Address2: "Suite 200", // optional second line
PostalCode: "94105",
City: "San Francisco",
Country: "USA",
},
AddtionnalInfo: []string{ // extra lines printed below the address
"VAT: FR12345678901",
"SIRET: 123 456 789 00010",
},
})
doc.SetCustomer(&generator.Contact{
Name: "John Doe",
Address: &generator.Address{
Address: "42 Main Street",
PostalCode: "10001",
City: "New York",
Country: "USA",
},
})Each item has a name, optional description, unit cost, quantity, and optional tax and discount.
doc.AppendItem(&generator.Item{
Name: "Web development",
Description: "Frontend and backend implementation",
UnitCost: "1200.00",
Quantity: "3",
Tax: &generator.Tax{Percent: "20"},
Discount: &generator.Discount{Percent: "10"},
})UnitCost and Quantity are strings to avoid floating-point precision issues;
the library uses shopspring/decimal internally.
A tax is either a percentage or a fixed amount — not both. An optional Name
groups taxes in the totals breakdown.
// 20% VAT
Tax: &generator.Tax{Percent: "20", Name: "VAT 20%"}
// Reduced 5.5% VAT
Tax: &generator.Tax{Percent: "5.5", Name: "VAT 5.5%"}
// Fixed €89 eco-tax regardless of quantity
Tax: &generator.Tax{Amount: "89", Name: "Eco tax"}
// Unnamed — grouped under "Other" in the breakdown
Tax: &generator.Tax{Percent: "10"}When two or more distinct tax names are present the totals block shows the overall
tax amount (same as before) followed by a smaller per-name breakdown. Taxes without a
name are grouped under the label set by Options.TextTotalTaxOther (default: "Other").
A discount is either a percentage or a fixed amount — not both.
// 30% off the item subtotal
Discount: &generator.Discount{Percent: "30"}
// Fixed €50 deducted from the item subtotal
Discount: &generator.Discount{Amount: "50"}SetDefaultTax applies a tax to every item that does not have its own Tax field.
doc.SetDefaultTax(&generator.Tax{Percent: "20"})Items that already have a Tax are not affected.
A document discount is applied to the subtotal after all item discounts. It reduces both the pre-tax total and (proportionally) the tax due.
// Fixed amount discount
doc.SetDiscount(&generator.Discount{Amount: "500"})
// Percentage discount
doc.SetDiscount(&generator.Discount{Percent: "5"})All totals are available programmatically after calling Build() (which runs
Validate() internally). You can also call Validate() directly if you only
need the numbers without generating a PDF.
if err := doc.Validate(); err != nil {
log.Fatal(err)
}
fmt.Println(doc.TotalWithoutTaxAndWithoutDocumentDiscount()) // sum of item subtotals after item discounts
fmt.Println(doc.TotalWithoutTax()) // above minus document discount
fmt.Println(doc.Tax()) // total tax (respects document discount)
fmt.Println(doc.TotalWithTax()) // final amount dueItem-level helpers are also available:
item := &generator.Item{UnitCost: "100", Quantity: "2", Discount: &generator.Discount{Percent: "10"}}
if err := item.Prepare(); err != nil {
log.Fatal(err)
}
fmt.Println(item.TotalWithoutTaxAndWithoutDiscount()) // 200.00
fmt.Println(item.TotalWithoutTaxAndWithDiscount()) // 180.00
fmt.Println(item.TaxWithTotalDiscounted()) // 0 (no tax set)
fmt.Println(item.TotalWithTaxAndDiscount()) // 180.00doc.SetHeader(&generator.HeaderFooter{
Text: "<center>Acme Corp — Confidential</center>",
FontSize: 7,
Pagination: true, // show "Page X/{nb}" in the top-right corner
})
doc.SetFooter(&generator.HeaderFooter{
Text: "<center>Acme Corp · 1 Market Street · San Francisco</center>",
Pagination: true,
})Text supports basic HTML tags (<b>, <i>, <center>).
For full control you can provide a custom function directly on the underlying fpdf instance:
hf := &generator.HeaderFooter{UseCustomFunc: true}
hf.ApplyFunc(doc.Pdf(), func() {
// use doc.Pdf() (a *fpdf.Fpdf) to draw anything you want
})
doc.SetHeader(hf)By default the document uses the UnicodeTranslatorFromDescriptor("") translator
bundled with fpdf.
To use a different encoding (e.g. ISO-8859-2), check examples/iso_8859_2_cp.
To a file:
pdf, err := doc.Build()
if err != nil {
log.Fatal(err)
}
if err := pdf.OutputFileAndClose("out.pdf"); err != nil {
log.Fatal(err)
}To a byte slice (e.g. for an HTTP response or S3 upload):
import "bytes"
pdf, err := doc.Build()
if err != nil {
log.Fatal(err)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
log.Fatal(err)
}
// buf.Bytes() contains the PDFThe facturx subpackage embeds a Factur-X (also known as ZUGFeRD 2.x) compliant CII XML into the PDF produced by Build(). All five conformance levels are supported and validated with mustang-cli and veraPDF (PDF/A-3B). In addition to the XML attachment, Attach automatically:
- Sets
/AFRelationship /Alternativeon the embedded file, as required by PDF/A-3. - Inserts an sRGB ICC OutputIntent into the PDF catalog (pure Go, no external dependencies).
- Merges the required Factur-X
pdfaid,pdfaExtension, andfx:XMP declarations into the PDF's existing XMP packet without discarding fields written by fpdf (Producer, CreationDate, etc.).
go get github.com/angelodlfrtr/go-invoice-generator/facturximport (
"bytes"
generator "github.com/angelodlfrtr/go-invoice-generator/generator"
"github.com/angelodlfrtr/go-invoice-generator/facturx"
)
// 1. Build the PDF as usual.
pdf, err := doc.Build()
if err != nil {
log.Fatal(err)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
log.Fatal(err)
}
// 2. Attach the Factur-X XML and bring the document into PDF/A-3b conformance.
result, err := facturx.Attach(buf.Bytes(), doc, facturx.Options{
Profile: facturx.ProfileEN16931,
SellerTaxID: "FR12345678901",
SellerCountryCode: "FR",
BuyerCountryCode: "US",
CurrencyCode: "EUR",
PaymentDueDate: "20240201",
PaymentIBAN: "FR7630006000011234567890189",
PaymentBIC: "BNPAFRPP",
})
if err != nil {
log.Fatal(err)
}
// result contains the PDF/A-3b document with the embedded factur-x.xml attachment.
os.WriteFile("invoice_facturx.pdf", result, 0644)| Constant | Factur-X profile | Line items in XML |
|---|---|---|
facturx.ProfileMinimum |
MINIMUM | No |
facturx.ProfileBasicWL |
BASIC-WL | No |
facturx.ProfileBasic |
BASIC | Yes |
facturx.ProfileEN16931 |
EN 16931 | Yes |
facturx.ProfileExtended |
EXTENDED | Yes |
| Field | Type | Description |
|---|---|---|
Profile |
Profile | Conformance level (default: ProfileMinimum) |
CurrencyCode |
string | ISO 4217 code (default: "EUR") |
SellerTaxID |
string | Seller VAT registration number (e.g. "FR12345678901") |
SellerCountryCode |
string | ISO 3166-1 alpha-2 seller country code (e.g. "FR"); falls back to address country |
BuyerCountryCode |
string | ISO 3166-1 alpha-2 buyer country code (e.g. "US"); falls back to address country |
BuyerReference |
string | Buyer's internal reference (e.g. a purchase order number) |
BuyerTaxID |
string | Buyer VAT registration number (rendered for BASIC-WL and above) |
PaymentDueDate |
string | Payment due date in "YYYYMMDD" format |
PaymentIBAN |
string | Seller IBAN for bank transfer |
PaymentBIC |
string | Seller BIC/SWIFT code |
PaymentMeansCode |
string | UN/ECE 4461 payment means code (default: "58" when IBAN is set) |
TaxCategoryCode |
string | Default VAT category code — "S" standard, "E" exempt, "Z" zero-rated |
TypeCode |
string | UN/CEFACT type code (default: "380" invoice; "381" credit note) |
ItemDefaultUnitCode |
string | UN/ECE Rec 20 unit code for all line items (default: "C62" piece/unit) |
ShowIcon |
bool | Place the Factur-X profile icon in the bottom-right corner of the first page |
Distributed under the Apache License, Version 2.0. See LICENSE and NOTICE for details.
