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