tradedoc

Crates.iotradedoc
lib.rstradedoc
version0.3.0
created_at2025-02-18 17:21:58.715998+00
updated_at2025-07-15 08:08:52.526618+00
descriptionLibrary for generating PDFs like invoices, receipts, packing slips, etc.
homepagehttps://gofranz.com/
repositoryhttps://github.com/franzos/tradedoc-rs/
max_upload_size
id1560193
size1,491,398
Franz Geffke (franzos)

documentation

README

Tradedoc: PDF generation for trade documents

The goal of this module is to easily generate PDF's like invoices, receipts, packing lists, etc.

Supported:

  • Invoice
  • Proforma Invoice
  • Packing List

Features:

  • Translation*
    • English (Language::English)
    • German (Language::German)
    • French (Language::French)
    • Spanish (Language::Spanish)
    • Portuguese (Language::Portuguese)
    • Thai (Language::Thai)
    • Italian (Language::Italian)
    • Add or overwrite via Dictionary
  • Customization

*Machine translation: If something's off, please provide a PR, or overwrite it with your own dictionary.

Generate examples

cargo run --bin example invoice
cargo run --bin example proforma-invoice
cargo run --bin example packing-list

With language support:

cargo run --bin example invoice --language de
cargo run --bin example proforma-invoice --language fr
cargo run --bin example packing-list --language en

Usage

Generate PDF's

Refer to src/bin/example.rs for usage examples.

// Invoice
let pdf_data = generate_pdf_invoice(
    &order,
    &order_items,
    &warehouse_address,
    properties,
    translation,
    Some(logo_bytes),        // Logo data (PNG/SVG)
)?;

// Proforma Invoice
let pdf_data = generate_pdf_proforma_invoice(
    &order,
    &order_items,
    &warehouse_address,
    properties,
    translation,
    Some(logo_bytes),        // Logo data (PNG/SVG)
)?;

// Packing List
let pdf_data = generate_pdf_packing_list(
    &order,
    &order_items,
    &warehouse_address,
    properties,
    translation,
    Some(logo_bytes),        // Logo data (PNG/SVG)
)?;

Font customization is handled via the DocumentProperties struct.

Data Structures

1. Order

use chrono::NaiveDateTime;
use rust_decimal::Decimal;
use tradedoc::types::{Order, Address};

let order = Order {
    id: "ORD-2023-001".to_string(),
    shipping_address: shipping_address,  // Address struct
    billing_address: billing_address,    // Address struct
    currency: "€".to_string(),
    status: "Completed".to_string(),
    shipping_method: "Express".to_string(),
    shipping_total: Decimal::new(1500, 2),            // €15.00
    subtotal_before_discount: Decimal::new(50000, 2), // €500.00
    discount_total: Decimal::new(5000, 2),            // €50.00
    subtotal: Decimal::new(45000, 2),                 // €450.00
    tax_total: Decimal::new(9000, 2),                 // €90.00
    total: Decimal::new(55500, 2),                    // €555.00
    notes: Some("Thank you for your business!".to_string()),
    created_at: NaiveDateTime::parse_from_str("2023-01-01 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
    updated_at: NaiveDateTime::parse_from_str("2023-01-01 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
};

2. Order Items

use tradedoc::types::OrderLineItem;

let order_items = vec![
    OrderLineItem {
        id: "ITEM1".to_string(),
        title: "Premium Widget".to_string(),
        sku: Some("WDG-001".to_string()),
        quantity: 2,
        unit_price: Decimal::new(15000, 2),               // €150.00
        unit_tax: Decimal::new(3000, 2),                  // €30.00
        unit_discount: Decimal::new(1500, 2),             // €15.00
        subtotal_before_discount: Decimal::new(30000, 2), // €300.00
        discount_total: Decimal::new(3000, 2),            // €30.00
        subtotal: Decimal::new(27000, 2),                 // €270.00
        tax_total: Decimal::new(5400, 2),                 // €54.00
        total: Decimal::new(32400, 2),                    // €324.00
    },
    OrderLineItem {
        id: "ITEM2".to_string(),
        title: "Basic Gadget".to_string(),
        sku: Some("GDG-001".to_string()),
        quantity: 1,
        unit_price: Decimal::new(20000, 2),               // €200.00
        unit_tax: Decimal::new(4000, 2),                  // €40.00
        unit_discount: Decimal::new(2000, 2),             // €20.00
        subtotal_before_discount: Decimal::new(20000, 2), // €200.00
        discount_total: Decimal::new(2000, 2),            // €20.00
        subtotal: Decimal::new(18000, 2),                 // €180.00
        tax_total: Decimal::new(3600, 2),                 // €36.00
        total: Decimal::new(21600, 2),                    // €216.00
    },
];

3. Warehouse Address

use tradedoc::types::Address;

let warehouse_address = Address {
    recipient_name: None,
    company_name: Some("ACME GmbH".to_string()),
    street: "Musterstraße 123".to_string(),
    street2: Some("4. Etage".to_string()),
    city: "Frankfurt am Main".to_string(),
    state: "Hesse".to_string(),
    country: "Germany".to_string(),
    zip: "60311".to_string(),
    phone: Some("+49 69 123 456 789".to_string()),
    vat_number: Some("DE123456789".to_string()),
};

// Customer addresses (similar structure)
let shipping_address = Address {
    recipient_name: Some("Max Mustermann".to_string()),
    company_name: Some("Musterfirma GmbH".to_string()),
    street: "Kundenstraße 456".to_string(),
    street2: None,
    city: "Frankfurt am Main".to_string(),
    state: "Hesse".to_string(),
    country: "Germany".to_string(),
    zip: "60329".to_string(),
    phone: Some("+49 69 987 654 321".to_string()),
    vat_number: Some("DE987654321".to_string()),
};

4. Document Properties

use tradedoc::types::DocumentProperties;

// Use defaults
let properties = DocumentProperties {
    font_normal_path: None,               // Will use embedded NotoSans
    font_bold_path: None,                 // Will use embedded NotoSans-Bold
    background_color: None,               // Will use light gray (0.9, 0.9, 0.9)
    font_size_title: None,                // Will use 20.0
    font_size_body: None,                 // Will use 10.0
    font_size_label: None,                // Will use 10.0
};

5. Translation / Dictionary

use tradedoc::types::{Dictionary, Language};

// Use default English translation
let translation = Dictionary::default();

// Use predefined language
let german_translation = Dictionary::for_language(Language::German);

// Custom translation (override specific labels)
let custom_translation = Dictionary {
    language: Language::English,
    invoice_title: "SALES INVOICE".to_string(),
    from_label: "Vendor:".to_string(),
    ship_to_label: "Delivery Address:".to_string(),
    bill_to_label: "Billing Address:".to_string(),
    phone_label: "Phone:".to_string(),
    vat_label: "Tax ID:".to_string(),
    product_header: "Item".to_string(),
    quantity_header: "Qty".to_string(),
    unit_price_header: "Unit Price".to_string(),
    discount_header: "Discount".to_string(),
    tax_header: "Tax".to_string(),
    total_header: "Total".to_string(),
    // ... other fields use default values
    ..Dictionary::default()
};

Fonts

The following fonts are embedded for ease of use:

  • NotoSans-Regular
  • NotoSans-SemiBold
  • NotoSansThai-Regular
  • NotoSansThai-SemiBold

Custom fonts: You can provide paths to custom TTF font files via font_normal_path and font_bold_path. If None, the system uses embedded NotoSans fonts which provide excellent multi-language support including Thai characters.

let properties = DocumentProperties {
    font_normal_path: Some("assets/fonts/MyFont-Regular.ttf".to_string()),
    font_bold_path: Some("assets/fonts/MyFont-Bold.ttf".to_string()),
    // ... other properties
};

Logo Support

All document types support optional logo placement in the top-right corner:

  • Supported formats: PNG, SVG
  • Usage: Pass logo bytes via the logo_data parameter
  • Position: Top-right corner of the document
  • Size: Automatically scaled to fit (80mm × 24mm)
  • Embedding: Use include_bytes!() to embed logo in binary

Example:

const LOGO: &[u8] = include_bytes!("assets/logo.png");

let pdf_data = generate_pdf_invoice(
    &order,
    &order_items,
    &warehouse_address,
    properties,
    translation,
    Some(LOGO),  // Logo data
)?;

Development

Compile and start server:

guix shell -m manifest.scm
cargo build

Clippy:

docker run --rm -v $(pwd):/app -w /app rust:1.82 sh \
-c "rustup component add clippy && cargo clippy \
--all-targets --all-features -- -D warnings"
Commit count: 1

cargo fmt