1use crate::domain::entities::{Building, Expense, Owner, Unit};
2use chrono::{DateTime, Utc};
3use printpdf::*;
4use std::io::BufWriter;
5
6pub struct OwnerStatementExporter;
10
11#[derive(Debug, Clone)]
12pub struct UnitWithOwnership {
13 pub unit: Unit,
14 pub ownership_percentage: f64, }
16
17impl OwnerStatementExporter {
18 pub fn export_to_pdf(
28 owner: &Owner,
29 building: &Building,
30 units: &[UnitWithOwnership],
31 expenses: &[Expense],
32 start_date: DateTime<Utc>,
33 end_date: DateTime<Utc>,
34 ) -> Result<Vec<u8>, String> {
35 let (doc, page1, layer1) =
37 PdfDocument::new("Relevé de Charges", Mm(210.0), Mm(297.0), "Layer 1");
38 let current_layer = doc.get_page(page1).get_layer(layer1);
39
40 let font = doc
42 .add_builtin_font(BuiltinFont::Helvetica)
43 .map_err(|e| e.to_string())?;
44 let font_bold = doc
45 .add_builtin_font(BuiltinFont::HelveticaBold)
46 .map_err(|e| e.to_string())?;
47
48 let mut y = 270.0; current_layer.use_text(
52 "RELEVÉ DE CHARGES".to_string(),
53 18.0,
54 Mm(20.0),
55 Mm(y),
56 &font_bold,
57 );
58 y -= 15.0;
59
60 current_layer.use_text(
62 format!("Copropriété: {}", building.name),
63 12.0,
64 Mm(20.0),
65 Mm(y),
66 &font_bold,
67 );
68 y -= 7.0;
69
70 current_layer.use_text(
71 format!("Adresse: {}", building.address),
72 10.0,
73 Mm(20.0),
74 Mm(y),
75 &font,
76 );
77 y -= 10.0;
78
79 let period = format!(
81 "Période: du {} au {}",
82 start_date.format("%d/%m/%Y"),
83 end_date.format("%d/%m/%Y")
84 );
85 current_layer.use_text(period, 10.0, Mm(20.0), Mm(y), &font);
86 y -= 10.0;
87
88 current_layer.use_text(
90 "COPROPRIÉTAIRE".to_string(),
91 14.0,
92 Mm(20.0),
93 Mm(y),
94 &font_bold,
95 );
96 y -= 8.0;
97
98 current_layer.use_text(
99 format!("{} {}", owner.first_name, owner.last_name),
100 11.0,
101 Mm(20.0),
102 Mm(y),
103 &font,
104 );
105 y -= 6.0;
106
107 current_layer.use_text(
108 format!("Email: {}", owner.email),
109 10.0,
110 Mm(20.0),
111 Mm(y),
112 &font,
113 );
114 y -= 6.0;
115
116 if let Some(ref phone) = owner.phone {
117 current_layer.use_text(
118 format!("Téléphone: {}", phone),
119 10.0,
120 Mm(20.0),
121 Mm(y),
122 &font,
123 );
124 y -= 6.0;
125 }
126 y -= 5.0;
127
128 current_layer.use_text(
130 "LOTS DÉTENUS".to_string(),
131 14.0,
132 Mm(20.0),
133 Mm(y),
134 &font_bold,
135 );
136 y -= 8.0;
137
138 current_layer.use_text("Lot", 10.0, Mm(20.0), Mm(y), &font_bold);
139 current_layer.use_text("Étage", 10.0, Mm(60.0), Mm(y), &font_bold);
140 current_layer.use_text("Surface", 10.0, Mm(90.0), Mm(y), &font_bold);
141 current_layer.use_text("Quote-part", 10.0, Mm(130.0), Mm(y), &font_bold);
142 y -= 6.0;
143
144 for unit_info in units {
145 if y < 100.0 {
146 break;
148 }
149
150 current_layer.use_text(&unit_info.unit.unit_number, 9.0, Mm(20.0), Mm(y), &font);
151
152 if let Some(floor) = unit_info.unit.floor {
153 current_layer.use_text(floor.to_string(), 9.0, Mm(60.0), Mm(y), &font);
154 }
155
156 current_layer.use_text(
157 format!("{:.2} m²", unit_info.unit.surface_area),
158 9.0,
159 Mm(90.0),
160 Mm(y),
161 &font,
162 );
163
164 current_layer.use_text(
165 format!("{:.2}%", unit_info.ownership_percentage * 100.0),
166 9.0,
167 Mm(130.0),
168 Mm(y),
169 &font,
170 );
171 y -= 5.0;
172 }
173 y -= 8.0;
174
175 current_layer.use_text(
177 "DÉTAIL DES CHARGES".to_string(),
178 14.0,
179 Mm(20.0),
180 Mm(y),
181 &font_bold,
182 );
183 y -= 8.0;
184
185 current_layer.use_text("Date", 10.0, Mm(20.0), Mm(y), &font_bold);
186 current_layer.use_text("Description", 10.0, Mm(50.0), Mm(y), &font_bold);
187 current_layer.use_text("Montant", 10.0, Mm(140.0), Mm(y), &font_bold);
188 current_layer.use_text("Statut", 10.0, Mm(170.0), Mm(y), &font_bold);
189 y -= 6.0;
190
191 let mut total_amount = 0.0;
192 let mut total_paid = 0.0;
193
194 for expense in expenses {
195 if y < 50.0 {
196 break;
198 }
199
200 current_layer.use_text(
201 expense.expense_date.format("%d/%m/%Y").to_string(),
202 9.0,
203 Mm(20.0),
204 Mm(y),
205 &font,
206 );
207
208 let description = if expense.description.len() > 30 {
209 format!("{}...", &expense.description[..30])
210 } else {
211 expense.description.clone()
212 };
213 current_layer.use_text(description, 9.0, Mm(50.0), Mm(y), &font);
214
215 current_layer.use_text(
216 format!("{:.2} €", expense.amount),
217 9.0,
218 Mm(140.0),
219 Mm(y),
220 &font,
221 );
222
223 let status = if expense.is_paid() {
224 "Payée"
225 } else {
226 "En attente"
227 };
228 current_layer.use_text(status.to_string(), 9.0, Mm(170.0), Mm(y), &font);
229
230 total_amount += expense.amount;
231 if expense.is_paid() {
232 total_paid += expense.amount;
233 }
234
235 y -= 5.0;
236 }
237 y -= 10.0;
238
239 current_layer.use_text(
241 "RÉCAPITULATIF".to_string(),
242 14.0,
243 Mm(20.0),
244 Mm(y),
245 &font_bold,
246 );
247 y -= 8.0;
248
249 current_layer.use_text(
250 format!("Total des charges: {:.2} €", total_amount),
251 11.0,
252 Mm(20.0),
253 Mm(y),
254 &font,
255 );
256 y -= 6.0;
257
258 current_layer.use_text(
259 format!("Montant payé: {:.2} €", total_paid),
260 11.0,
261 Mm(20.0),
262 Mm(y),
263 &font,
264 );
265 y -= 6.0;
266
267 let amount_due = total_amount - total_paid;
268 current_layer.use_text(
269 format!("Montant dû: {:.2} €", amount_due),
270 12.0,
271 Mm(20.0),
272 Mm(y),
273 &font_bold,
274 );
275 y -= 10.0;
276
277 if amount_due > 0.0 {
279 current_layer.use_text(
280 "Modalités de paiement:".to_string(),
281 10.0,
282 Mm(20.0),
283 Mm(y),
284 &font_bold,
285 );
286 y -= 6.0;
287
288 current_layer.use_text(
289 "Merci d'effectuer votre paiement par virement bancaire".to_string(),
290 9.0,
291 Mm(20.0),
292 Mm(y),
293 &font,
294 );
295 y -= 5.0;
296
297 current_layer.use_text(
298 "avec la référence suivante en communication.".to_string(),
299 9.0,
300 Mm(20.0),
301 Mm(y),
302 &font,
303 );
304 }
305
306 let mut buffer = Vec::new();
308 doc.save(&mut BufWriter::new(&mut buffer))
309 .map_err(|e| e.to_string())?;
310
311 Ok(buffer)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::domain::entities::ExpenseCategory;
319 use uuid::Uuid;
320
321 #[test]
322 fn test_export_owner_statement_pdf() {
323 let owner = Owner {
324 id: Uuid::new_v4(),
325 organization_id: Uuid::new_v4(),
326 user_id: None,
327 first_name: "Jean".to_string(),
328 last_name: "Dupont".to_string(),
329 email: "jean@example.com".to_string(),
330 phone: Some("+32 2 123 45 67".to_string()),
331 address: "123 Rue de Test".to_string(),
332 city: "Bruxelles".to_string(),
333 postal_code: "1000".to_string(),
334 country: "Belgium".to_string(),
335 created_at: Utc::now(),
336 updated_at: Utc::now(),
337 };
338
339 let building = Building {
340 id: Uuid::new_v4(),
341 name: "Les Jardins de Bruxelles".to_string(),
342 address: "123 Avenue Louise".to_string(),
343 city: "Bruxelles".to_string(),
344 postal_code: "1000".to_string(),
345 country: "Belgium".to_string(),
346 total_units: 10,
347 total_tantiemes: 1000,
348 construction_year: Some(1990),
349 syndic_name: None,
350 syndic_email: None,
351 syndic_phone: None,
352 syndic_address: None,
353 syndic_office_hours: None,
354 syndic_emergency_contact: None,
355 slug: None,
356 organization_id: owner.organization_id,
357 created_at: Utc::now(),
358 updated_at: Utc::now(),
359 };
360
361 let unit = Unit {
362 id: Uuid::new_v4(),
363 organization_id: building.organization_id,
364 building_id: building.id,
365 unit_number: "A1".to_string(),
366 unit_type: crate::domain::entities::UnitType::Apartment,
367 floor: Some(1),
368 surface_area: 75.5,
369 quota: 150.0,
370 owner_id: None,
371 created_at: Utc::now(),
372 updated_at: Utc::now(),
373 };
374
375 let units = vec![UnitWithOwnership {
376 unit,
377 ownership_percentage: 0.15, }];
379
380 let expenses = vec![Expense {
381 id: Uuid::new_v4(),
382 building_id: building.id,
383 organization_id: building.organization_id,
384 description: "Entretien ascenseur".to_string(),
385 amount: 150.0,
386 amount_excl_vat: Some(123.97),
387 vat_rate: Some(21.0),
388 vat_amount: Some(26.03),
389 amount_incl_vat: Some(150.0),
390 expense_date: Utc::now(),
391 invoice_date: None,
392 due_date: None,
393 paid_date: None,
394 category: ExpenseCategory::Maintenance,
395 approval_status: crate::domain::entities::ApprovalStatus::Approved,
396 submitted_at: None,
397 approved_by: None,
398 approved_at: None,
399 rejection_reason: None,
400 payment_status: crate::domain::entities::PaymentStatus::Pending,
401 supplier: None,
402 invoice_number: Some("INV-001".to_string()),
403 account_code: None,
404 created_at: Utc::now(),
405 updated_at: Utc::now(),
406 }];
407
408 let result = OwnerStatementExporter::export_to_pdf(
409 &owner,
410 &building,
411 &units,
412 &expenses,
413 Utc::now() - chrono::Duration::days(30),
414 Utc::now(),
415 );
416
417 assert!(result.is_ok());
418 let pdf_bytes = result.unwrap();
419 assert!(!pdf_bytes.is_empty());
420 assert!(pdf_bytes.len() > 100);
421 }
422}