Blame


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