1use crate::domain::services::PcnReportLine;
2use printpdf::*;
3use rust_decimal::prelude::ToPrimitive;
4use rust_decimal::Decimal;
5use std::io::BufWriter;
6
7pub struct PcnExporter;
9
10impl PcnExporter {
11 pub fn export_to_pdf(
14 building_name: &str,
15 report_lines: &[PcnReportLine],
16 total_amount: Decimal,
17 ) -> Result<Vec<u8>, String> {
18 let (doc, page1, layer1) = PdfDocument::new("Rapport PCN", Mm(210.0), Mm(297.0), "Layer 1");
20 let current_layer = doc.get_page(page1).get_layer(layer1);
21
22 let font = doc
24 .add_builtin_font(BuiltinFont::Helvetica)
25 .map_err(|e| e.to_string())?;
26 let font_bold = doc
27 .add_builtin_font(BuiltinFont::HelveticaBold)
28 .map_err(|e| e.to_string())?;
29
30 current_layer.use_text(
32 "Rapport PCN - Plan Comptable Normalisé".to_string(),
33 24.0,
34 Mm(20.0),
35 Mm(270.0),
36 &font_bold,
37 );
38
39 current_layer.use_text(
41 format!("Immeuble: {}", building_name),
42 14.0,
43 Mm(20.0),
44 Mm(260.0),
45 &font,
46 );
47
48 let mut y = 245.0;
50 current_layer.use_text("Code", 12.0, Mm(20.0), Mm(y), &font_bold);
51 current_layer.use_text("Libellé", 12.0, Mm(50.0), Mm(y), &font_bold);
52 current_layer.use_text("Montant (€)", 12.0, Mm(140.0), Mm(y), &font_bold);
53 current_layer.use_text("Nb", 12.0, Mm(180.0), Mm(y), &font_bold);
54
55 y -= 10.0;
57 for line in report_lines {
58 current_layer.use_text(&line.account.code, 10.0, Mm(20.0), Mm(y), &font);
59 current_layer.use_text(&line.account.label_fr, 10.0, Mm(50.0), Mm(y), &font);
60 current_layer.use_text(
61 format!("{:.2}", line.total_amount),
62 10.0,
63 Mm(140.0),
64 Mm(y),
65 &font,
66 );
67 current_layer.use_text(
68 format!("{}", line.entry_count),
69 10.0,
70 Mm(180.0),
71 Mm(y),
72 &font,
73 );
74 y -= 7.0;
75 }
76
77 y -= 5.0;
79 current_layer.use_text("TOTAL:", 12.0, Mm(50.0), Mm(y), &font_bold);
80 current_layer.use_text(
81 format!("{:.2} €", total_amount),
82 12.0,
83 Mm(140.0),
84 Mm(y),
85 &font_bold,
86 );
87
88 let mut buffer = Vec::new();
90 doc.save(&mut BufWriter::new(&mut buffer))
91 .map_err(|e| e.to_string())?;
92
93 Ok(buffer)
94 }
95
96 pub fn export_to_excel(
99 building_name: &str,
100 report_lines: &[PcnReportLine],
101 total_amount: Decimal,
102 ) -> Result<Vec<u8>, String> {
103 use rust_xlsxwriter::*;
104
105 let mut workbook = Workbook::new();
107 let worksheet = workbook.add_worksheet();
108
109 worksheet
111 .set_column_width(0, 10)
112 .map_err(|e| e.to_string())?; worksheet
114 .set_column_width(1, 35)
115 .map_err(|e| e.to_string())?; worksheet
117 .set_column_width(2, 35)
118 .map_err(|e| e.to_string())?; worksheet
120 .set_column_width(3, 35)
121 .map_err(|e| e.to_string())?; worksheet
123 .set_column_width(4, 35)
124 .map_err(|e| e.to_string())?; worksheet
126 .set_column_width(5, 15)
127 .map_err(|e| e.to_string())?; worksheet
129 .set_column_width(6, 10)
130 .map_err(|e| e.to_string())?; let bold_format = Format::new().set_bold();
134 let currency_format = Format::new().set_num_format("#,##0.00 €");
135 let header_format = Format::new()
136 .set_bold()
137 .set_background_color(Color::RGB(0xD3D3D3));
138
139 worksheet
141 .write_string_with_format(0, 0, "Rapport PCN - Plan Comptable Normalisé", &bold_format)
142 .map_err(|e| e.to_string())?;
143
144 worksheet
146 .write_string_with_format(
147 1,
148 0,
149 format!("Immeuble: {}", building_name).as_str(),
150 &Format::new(),
151 )
152 .map_err(|e| e.to_string())?;
153
154 worksheet
156 .write_string_with_format(3, 0, "Code PCN", &header_format)
157 .map_err(|e| e.to_string())?;
158 worksheet
159 .write_string_with_format(3, 1, "Nederlands (NL)", &header_format)
160 .map_err(|e| e.to_string())?;
161 worksheet
162 .write_string_with_format(3, 2, "Français (FR)", &header_format)
163 .map_err(|e| e.to_string())?;
164 worksheet
165 .write_string_with_format(3, 3, "Deutsch (DE)", &header_format)
166 .map_err(|e| e.to_string())?;
167 worksheet
168 .write_string_with_format(3, 4, "English (EN)", &header_format)
169 .map_err(|e| e.to_string())?;
170 worksheet
171 .write_string_with_format(3, 5, "Montant", &header_format)
172 .map_err(|e| e.to_string())?;
173 worksheet
174 .write_string_with_format(3, 6, "Nb Écritures", &header_format)
175 .map_err(|e| e.to_string())?;
176
177 let mut row = 4;
179 for line in report_lines {
180 worksheet
181 .write_string(row, 0, &line.account.code)
182 .map_err(|e| e.to_string())?;
183 worksheet
184 .write_string(row, 1, &line.account.label_nl)
185 .map_err(|e| e.to_string())?;
186 worksheet
187 .write_string(row, 2, &line.account.label_fr)
188 .map_err(|e| e.to_string())?;
189 worksheet
190 .write_string(row, 3, &line.account.label_de)
191 .map_err(|e| e.to_string())?;
192 worksheet
193 .write_string(row, 4, &line.account.label_en)
194 .map_err(|e| e.to_string())?;
195 worksheet
196 .write_number_with_format(
197 row,
198 5,
199 line.total_amount.to_f64().unwrap_or(0.0),
200 ¤cy_format,
201 )
202 .map_err(|e| e.to_string())?;
203 worksheet
204 .write_number(row, 6, line.entry_count as f64)
205 .map_err(|e| e.to_string())?;
206 row += 1;
207 }
208
209 row += 1;
211 worksheet
212 .write_string_with_format(row, 4, "TOTAL:", &bold_format)
213 .map_err(|e| e.to_string())?;
214 worksheet
215 .write_number_with_format(
216 row,
217 5,
218 total_amount.to_f64().unwrap_or(0.0),
219 &Format::new().set_bold().set_num_format("#,##0.00 €"),
220 )
221 .map_err(|e| e.to_string())?;
222
223 let buffer = workbook.save_to_buffer().map_err(|e| e.to_string())?;
225
226 Ok(buffer)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::domain::entities::ExpenseCategory;
234 use crate::domain::services::PcnMapper;
235
236 fn create_test_report() -> (Vec<PcnReportLine>, Decimal) {
237 use rust_decimal_macros::dec;
238 let lines = vec![
239 PcnReportLine {
240 account: PcnMapper::map_expense_to_pcn(&ExpenseCategory::Maintenance),
241 total_amount: dec!(1500),
242 entry_count: 5,
243 },
244 PcnReportLine {
245 account: PcnMapper::map_expense_to_pcn(&ExpenseCategory::Utilities),
246 total_amount: dec!(800),
247 entry_count: 3,
248 },
249 PcnReportLine {
250 account: PcnMapper::map_expense_to_pcn(&ExpenseCategory::Insurance),
251 total_amount: dec!(2000),
252 entry_count: 1,
253 },
254 ];
255 let total = lines.iter().map(|l| l.total_amount).sum();
256 (lines, total)
257 }
258
259 #[test]
262 fn test_export_pdf_returns_bytes() {
263 let (lines, total) = create_test_report();
264
265 let result = PcnExporter::export_to_pdf("Test Building", &lines, total);
266
267 assert!(result.is_ok());
268 let pdf_bytes = result.unwrap();
269 assert!(!pdf_bytes.is_empty());
270
271 assert_eq!(&pdf_bytes[0..4], b"%PDF");
273 }
274
275 #[test]
276 fn test_export_pdf_empty_report() {
277 let result = PcnExporter::export_to_pdf("Empty Building", &[], Decimal::ZERO);
278
279 assert!(result.is_ok());
280 let pdf_bytes = result.unwrap();
281 assert!(!pdf_bytes.is_empty());
282 assert_eq!(&pdf_bytes[0..4], b"%PDF");
283 }
284
285 #[test]
286 fn test_export_pdf_contains_building_name() {
287 let (lines, total) = create_test_report();
288
289 let result = PcnExporter::export_to_pdf("My Test Building", &lines, total);
290
291 assert!(result.is_ok());
292 }
294
295 #[test]
298 fn test_export_excel_returns_bytes() {
299 let (lines, total) = create_test_report();
300
301 let result = PcnExporter::export_to_excel("Test Building", &lines, total);
302
303 assert!(result.is_ok());
304 let excel_bytes = result.unwrap();
305 assert!(!excel_bytes.is_empty());
306
307 assert_eq!(&excel_bytes[0..2], b"PK");
309 }
310
311 #[test]
312 fn test_export_excel_empty_report() {
313 let result = PcnExporter::export_to_excel("Empty Building", &[], Decimal::ZERO);
314
315 assert!(result.is_ok());
316 let excel_bytes = result.unwrap();
317 assert!(!excel_bytes.is_empty());
318 assert_eq!(&excel_bytes[0..2], b"PK");
319 }
320
321 #[test]
322 fn test_export_excel_has_correct_row_count() {
323 let (lines, total) = create_test_report();
324
325 let result = PcnExporter::export_to_excel("Test Building", &lines, total);
326
327 assert!(result.is_ok());
328 }
331}