Blob


1 use std::path::Path;
2 use genpdf::{
3 elements::{Paragraph, Break, Text, TableLayout, FrameCellDecorator},
4 style::{Style, StyledString},
5 error::Error,
6 fonts,
7 Document,
8 SimplePageDecorator,
9 Alignment,
10 Element,
11 Size,
12 };
13 use crate::data::{Invoice, Record, Business};
14 use super::elements::*;
16 const FONT_DIRS: &[&str] = &[
17 "/usr/share/fonts/liberation",
18 "/usr/share/fonts/Liberation",
19 "/usr/local/share/fonts/liberation",
20 "/usr/local/share/fonts/Liberation",
21 ];
22 const DEFAULT_FONT_NAME: &str = "LiberationSans";
24 macro_rules! doc {
25 [$($e:expr),* $(,)?] => {
26 {
27 let font_dir = FONT_DIRS
28 .iter()
29 .find(|path| Path::new(path).exists())
30 .unwrap();
31 let default_font = fonts::from_files(font_dir, DEFAULT_FONT_NAME, None)
32 .unwrap();
33 let mut doc = Document::new(default_font);
34 $(
35 doc.push($e);
36 )*
37 doc
38 }
39 };
40 }
42 macro_rules! table {
43 {($($width:expr),* $(,)?) => [ $($e:expr),* $(,)? ]} => {
44 {
45 let mut table = TableLayout::new(vec![$($width),*]);
46 $(table.push_row($e).unwrap();)*
47 table
48 }
49 };
50 }
52 fn text(text: impl Into<String>, style: Style) -> Text {
53 Text::new(StyledString::new(text, style))
54 }
56 pub fn render_to_file(invoice: &Invoice, path: &Path) -> Result<(), Error> {
57 let doc = render(invoice);
58 doc.render_to_file(path)
59 }
61 pub fn render(invoice: &Invoice) -> Document {
62 let biz = &invoice.business;
63 let cus = &invoice.customer;
65 let s8 = Style::new().with_font_size(8);
66 let s10 = Style::new().with_font_size(10);
67 let s12 = Style::new().with_font_size(12);
68 let s14 = Style::new().with_font_size(14);
69 let s16 = Style::new().with_font_size(16);
70 let s28 = Style::new().with_font_size(28);
72 let mut doc = doc! [
73 Paragraph::new(&biz.name).aligned(Alignment::Center).styled(s28.bold()),
74 Break::new(3),
75 UnderlinedText::new(format!("{}, {}, {}", biz.owner_name, biz.address, biz.zip_city), s8),
76 text(&cus.name, s12),
77 text(&cus.address, s12),
78 text(&cus.zip_city, s12),
79 Break::new(1),
80 Paragraph::new(format!("Datum: {}", invoice.date())).aligned(Alignment::Right),
81 text(format!("Kunde {}", cus.id), s12),
82 text(format!("Rechnung {}", invoice.id), s16.bold()),
83 Break::new(2),
84 table(&invoice.records, s14.bold(), s12),
85 table2(&invoice.records, s12.bold()),
86 Break::new(2),
87 text("zahlbar nach Rechnungserhalt.", s10),
88 text("Kein Ausweis von Umsatzsteuer,", s10),
89 text("da Kleinunternehmer gemäß $ 19 USTG.", s10),
90 text("", s10),
91 text("Vielen Dank für Ihren Auftrag und Ihr Vertrauen.", s10),
92 Break::new(18 - invoice.records.len() as i32),
93 HorizontalLine,
94 footer(&biz, s10),
95 ];
97 doc.set_title(format!("Invoice {} for {}", invoice.id, cus.id));
98 doc.set_minimal_conformance();
99 doc.set_line_spacing(1.25);
100 doc.set_paper_size(Size::new(210, 297)); // A4
102 let mut decorator = SimplePageDecorator::new();
103 decorator.set_margins(10);
104 doc.set_page_decorator(decorator);
106 doc
109 fn table(records: &[Record], header: Style, normal: Style) -> TableLayout {
110 let header = |text: &str, align: Alignment| {
111 Box::new(Paragraph::new(text).aligned(align).styled(header))
112 };
114 let row = |descr: &str, date: String, n: f32, c: f32| -> Vec<Box<dyn Element>> {
115 let f = |text: StyledString| {
116 let p = Paragraph::new(text)
117 .aligned(Alignment::Center)
118 .styled(normal);
119 Box::new(p)
120 };
121 vec![
122 Box::new(Paragraph::new(format!(" {descr}")).styled(normal)),
123 f(date.into()),
124 f(format!("{n:.2}").into()),
125 f(format!("{c:.2} EUR").into()),
126 f(format!("{:.2} EUR", n * c).into()),
128 };
130 let mut table = table! {
131 (15, 4, 3, 5, 5) => [
132 vec![
133 header(" Beschreibung", Alignment::Left),
134 header("Datum", Alignment::Center),
135 header("Anzahl", Alignment::Center),
136 header("Preis", Alignment::Center),
137 header("Gesamt", Alignment::Center),
140 };
142 for r in records {
143 table.push_row(row(&r.description, r.date(), r.count, r.price)).unwrap();
146 table.set_cell_decorator(FrameCellDecorator::new(true, true, false));
147 table
150 fn table2(records: &[Record], style: Style) -> TableLayout {
151 let total: f32 = records
152 .iter()
153 .map(|r| r.count * r.price)
154 .sum();
156 let mut table = table! {
157 (1, 1) => [
158 vec![
159 Box::new(Paragraph::new(" Netto").styled(style)),
160 Box::new(Paragraph::new(format!("{total:.2} EUR ")).aligned(Alignment::Right).styled(style)),
163 };
165 table.set_cell_decorator(FrameCellDecorator::new(false, true, false));
166 table
169 fn footer(biz: &Business, style: Style) -> TableLayout {
170 let f = |text, align| {
171 Box::new(Paragraph::new(text).aligned(align).styled(style))
172 };
173 let row = |a: &str, b: &str, c: &str| -> Vec<Box<dyn Element>> {
174 vec![
175 f(a.to_string(), Alignment::Left),
176 f(b.to_string(), Alignment::Center),
177 f(c.to_string(), Alignment::Right)
179 };
180 table! {
181 (1, 1, 1) => [
182 row(&biz.owner_name, "", &biz.iban),
183 row(&biz.address, &biz.ustid, &biz.bic),
184 row(&biz.zip_city, "", &biz.bank),
185 row(&biz.email, "", ""),