From 0ebadbdb48fc5dba988ec153f6afa869eb81cff6 Mon Sep 17 00:00:00 2001 From: Jakob Date: Mon, 23 Mar 2026 11:04:24 +0100 Subject: [PATCH] Add PDF generation for invoices - Generate both HTML and PDF versions of each customer invoice - PDFs use printpdf with HTML rendering for consistent styling - Same layout and formatting as HTML output --- Cargo.toml | 1 + src/main.rs | 62 +++++++------ src/pdf.rs | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 src/pdf.rs diff --git a/Cargo.toml b/Cargo.toml index 6c6e527..832818b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" askama = "0.15.5" chrono = "0.4.44" csv = "1.4.0" +printpdf = "0.9.1" serde = "1.0.228" diff --git a/src/main.rs b/src/main.rs index 98da45f..ef26c65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::fs; use std::path::Path; mod invoice_generator; +mod pdf; use invoice_generator::{group_by_customer, read_csv_file, Customer}; @@ -14,43 +15,43 @@ fn fmt(v: f64) -> String { } #[derive(Clone)] -struct ProductSummary { - name: String, - volume: String, - amount: String, - avg_price: String, +pub struct ProductSummary { + pub name: String, + pub volume: String, + pub amount: String, + pub avg_price: String, } #[derive(Clone)] -struct Summary { - total_volume: String, - grand_total: String, - products: Vec, +pub struct Summary { + pub total_volume: String, + pub grand_total: String, + pub products: Vec, } #[derive(Clone)] -struct CardData { - card_number: String, - transactions: Vec, - total_amount: String, - total_volume: String, +pub struct CardData { + pub card_number: String, + pub transactions: Vec, + pub total_amount: String, + pub total_volume: String, } #[derive(Clone)] -struct FormattedTransaction { - date: String, - quality_name: String, - price: String, - volume: String, - amount: String, - receipt: String, +pub struct FormattedTransaction { + pub date: String, + pub quality_name: String, + pub price: String, + pub volume: String, + pub amount: String, + pub receipt: String, } #[derive(Clone)] -struct PreparedCustomer { - customer_number: String, - cards: Vec, - summary: Summary, +pub struct PreparedCustomer { + pub customer_number: String, + pub cards: Vec, + pub summary: Summary, } impl PreparedCustomer { @@ -212,7 +213,7 @@ fn main() -> Result<(), Box> { for (customer_num, customer) in customers { let prepared = PreparedCustomer::from_customer(customer); let customer_html = CustomerTemplate { - customer: prepared, + customer: prepared.clone(), period: period.clone(), generated_date: generated_date.clone(), } @@ -221,6 +222,15 @@ fn main() -> Result<(), Box> { let filename = format!("customer_{}.html", customer_num); fs::write(output_dir.join(&filename), customer_html)?; println!("Genererade {}", filename); + + let pdf_filename = format!("customer_{}.pdf", customer_num); + pdf::generate_customer_pdf( + &prepared, + &period, + &generated_date, + &output_dir.join(&pdf_filename), + )?; + println!("Genererade {}", pdf_filename); } println!( diff --git a/src/pdf.rs b/src/pdf.rs new file mode 100644 index 0000000..4d557e6 --- /dev/null +++ b/src/pdf.rs @@ -0,0 +1,244 @@ +use printpdf::*; +use std::collections::BTreeMap; +use std::fs::File; +use std::io::BufWriter; +use std::path::Path; + +use crate::PreparedCustomer; + +pub fn generate_customer_pdf( + customer: &PreparedCustomer, + period: &str, + generated_date: &str, + output_path: &Path, +) -> Result<(), Box> { + let html = format!( + r#" + + + + Faktura - Kund {} + + + +
+
+

Faktura - Kund {}

+
Period: {}
+
+
+
Genererad: {}
+
+
+ +
+

Sammanfattning

+ + + + + + + + + + + {} + + + + + + + +
ProduktVolym (L)BeloppSnittpris/L
Totalt{}{} Kr
+
+ + {} + +"#, + customer.customer_number, + customer.customer_number, + period, + generated_date, + generate_product_rows(&customer.summary.products), + customer.summary.total_volume, + customer.summary.grand_total, + generate_card_sections(&customer.cards), + ); + + let mut warnings = Vec::new(); + let doc = PdfDocument::from_html( + &html, + &BTreeMap::new(), + &BTreeMap::new(), + &GeneratePdfOptions::default(), + &mut warnings, + )?; + + let file = File::create(output_path)?; + let mut writer = BufWriter::new(file); + let mut warnings = Vec::new(); + doc.save_writer(&mut writer, &PdfSaveOptions::default(), &mut warnings); + + Ok(()) +} + +fn generate_product_rows(products: &[crate::ProductSummary]) -> String { + products + .iter() + .map(|p| { + format!( + r#" + {} + {} + {} Kr + {} Kr + "#, + p.name, p.volume, p.amount, p.avg_price + ) + }) + .collect::>() + .join("\n ") +} + +fn generate_card_sections(cards: &[crate::CardData]) -> String { + cards + .iter() + .map(|card| { + format!( + r#"
+
Kort: {} | {} transaktioner
+
Summa: {} Kr | Volym: {} L
+ + + + + + + + + + + + + {} + + + + + + + +
DatumProduktPris/LVolymBeloppKvitto
Kortsumma{}{}
+
"#, + card.card_number, + card.transactions.len(), + card.total_amount, + card.total_volume, + generate_transaction_rows(&card.transactions), + card.total_volume, + card.total_amount, + ) + }) + .collect::>() + .join("\n\n ") +} + +fn generate_transaction_rows(transactions: &[crate::FormattedTransaction]) -> String { + transactions + .iter() + .map(|tx| { + format!( + r#" + {} + {} + {} + {} + {} + {} + "#, + tx.date, tx.quality_name, tx.price, tx.volume, tx.amount, tx.receipt + ) + }) + .collect::>() + .join("\n ") +}