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