======================================================================================= Issue #47: feat: Extend PDF generation (Meeting minutes, contracts, financial reports) ======================================================================================= :State: **OPEN** :Milestone: Jalon 3: Features Différenciantes 🎯 :Labels: phase:vps,track:software priority:high :Assignees: Unassigned :Created: 2025-10-27 :Updated: 2025-11-13 :URL: `View on GitHub `_ Description =========== .. raw:: html
:: ## Context **Current PDF generation:** ⚠️ **50% implemented** - ✅ PCN reports (Belgian chart of accounts) via `printpdf` library - ✅ Excel export for PCN reports via `rust_xlsxwriter` - ❌ Meeting minutes (procès-verbaux) with vote results - ❌ Financial statements for owners - ❌ Contracts (ownership, services) - ❌ Work quotes **Files:** - `backend/src/domain/services/pcn_exporter.rs` ✅ - `backend/src/infrastructure/web/handlers/document_handlers.rs` ⚠️ ## Objective Extend PDF generation to cover all document types required for Belgian copropriété management. ## Required PDF Templates ### 1. Meeting Minutes (Procès-Verbal d'Assemblée Générale) **Content:** - Building name & address - Meeting type (AGO/AGE) - Date, time, location - Attendees list with voting power (tantièmes) - Quorum validation - Agenda - Resolutions with vote results (depends on #46) - Signatures section **API endpoint:** `POST /api/v1/meetings/:id/export-minutes-pdf` **Implementation:** ```rust // backend/src/domain/services/meeting_minutes_exporter.rs pub struct MeetingMinutesExporter; impl MeetingMinutesExporter { pub fn generate_pdf( meeting: &Meeting, building: &Building, resolutions: Vec, attendees: Vec<(Owner, f64)>, // (owner, voting_power) ) -> Result, String> { let doc = PdfDocument::empty("Meeting Minutes"); // Header self.add_header(&doc, building, meeting); // Attendees section self.add_attendees(&doc, attendees); // Quorum validation self.add_quorum(&doc, meeting); // Agenda self.add_agenda(&doc, meeting.agenda); // Resolutions & votes for resolution in resolutions { self.add_resolution(&doc, resolution); } // Signatures self.add_signatures(&doc); doc.save_to_bytes() } } ``` ### 2. Owner Financial Statement (Relevé de Charges) **Content:** - Owner name & address - Period (Q1 2025, Year 2025, etc.) - Units owned with ownership percentages - Expense breakdown by category - Payment status (paid/pending) - Total due - Payment instructions (bank details) **API endpoint:** `POST /api/v1/owners/:id/export-statement-pdf` **Use case:** ```rust pub async fn generate_owner_statement( &self, owner_id: Uuid, start_date: DateTime, end_date: DateTime, ) -> Result, String> { // Fetch owner data let owner = self.owner_repo.find_by_id(owner_id).await?; // Fetch units owned let units = self.unit_owner_repo.find_units_by_owner(owner_id).await?; // Fetch expenses for period let expenses = self.expense_repo.find_by_owner_and_period(owner_id, start_date, end_date).await?; // Generate PDF OwnerStatementExporter::generate_pdf(owner, units, expenses) } ``` ### 3. Ownership Contract (Contrat de Copropriété) **Content:** - Building information - Unit details (number, floor, area, tantièmes) - Owner information - Ownership start date - Percentage owned - Rights and obligations - General assembly rules - Expense allocation rules **API endpoint:** `POST /api/v1/unit-owners/:id/export-contract-pdf` **Template-based approach:** ```rust pub struct ContractExporter { template: String, // HTML template } impl ContractExporter { pub fn generate_pdf( building: &Building, unit: &Unit, owner: &Owner, unit_owner: &UnitOwner, ) -> Result, String> { // Render HTML template with data let html = self.render_template(building, unit, owner, unit_owner); // Convert HTML to PDF (using headless_chrome or wkhtmltopdf) self.html_to_pdf(html) } } ``` ### 4. Work Quote Document (Devis de Travaux) **Content:** - Building information - Work description - Cost breakdown - Timeline - Approval status - Signatures section **API endpoint:** `POST /api/v1/expenses/:id/export-quote-pdf` (if expense is type "Works") ### 5. Financial Report (Rapport Financier Annuel) **Content:** - Building information - Year summary - Income breakdown (charges paid) - Expense breakdown by category - Budget vs actual - Reserve fund status - Charts (pie chart for expenses, bar chart for trends) **API endpoint:** `POST /api/v1/buildings/:id/export-annual-report-pdf` **Includes charts:** ```rust // Use plotters or similar for chart generation use plotters::prelude::*; fn generate_expense_chart(expenses: Vec) -> Vec { // Generate PNG chart // Embed in PDF } ``` ## Technical Approaches ### Option A: printpdf (Current) **Pros:** - ✅ Pure Rust, no external dependencies - ✅ Fast and lightweight - ✅ Already used for PCN reports **Cons:** - ❌ Manual layout (low-level API) - ❌ No templating - ❌ Complex for multi-page documents **Best for:** Simple tabular reports (PCN, statements) ### Option B: HTML → PDF (wkhtmltopdf / headless_chrome) **Pros:** - ✅ Templating with HTML/CSS (Tera, Handlebars) - ✅ Easy styling - ✅ Responsive layouts - ✅ Complex documents easier **Cons:** - ❌ External dependency (wkhtmltopdf binary or Chrome) - ❌ Larger Docker image - ❌ Slower than printpdf **Best for:** Complex documents (contracts, meeting minutes) **Implementation:** ```rust use headless_chrome::{Browser, LaunchOptions}; pub fn html_to_pdf(html: &str) -> Result, String> { let browser = Browser::new(LaunchOptions::default())?; let tab = browser.wait_for_initial_tab()?; tab.navigate_to(&format!("data:text/html,{}", html))?; tab.wait_until_navigated()?; let pdf = tab.print_to_pdf(None)?; Ok(pdf) } ``` ### Option C: Hybrid Approach (Recommended) - **printpdf** for simple reports (PCN, statements) - **HTML → PDF** for complex documents (meeting minutes, contracts) ## Templating System **Use Tera templates:** `backend/templates/meeting_minutes.html`: ```html

Procès-Verbal d'Assemblée Générale

Informations

Immeuble: {{ building.name }}

Date: {{ meeting.date }}

Type: {{ meeting.meeting_type }}

Présences

{% for attendee in attendees %} {% endfor %}
CopropriétaireMillièmes
{{ attendee.name }}{{ attendee.voting_power }}

Résolutions

{% for resolution in resolutions %}

{{ resolution.title }}

{{ resolution.description }}

Résultat: {{ resolution.status }}

Pour: {{ resolution.vote_count_pour }} | Contre: {{ resolution.vote_count_contre }}

{% endfor %} ``` **Render with Tera:** ```rust use tera::{Tera, Context}; let tera = Tera::new("templates/**/*.html")?; let mut context = Context::new(); context.insert("building", &building); context.insert("meeting", &meeting); context.insert("resolutions", &resolutions); let html = tera.render("meeting_minutes.html", &context)?; let pdf = html_to_pdf(&html)?; ``` ## Dependencies `backend/Cargo.toml`: ```toml [dependencies] # Option B (HTML → PDF) tera = "1.19" headless_chrome = "1.0" # Option A (Pure Rust, existing) printpdf = "0.7" # Charts (optional) plotters = "0.3" ``` ## Implementation Priority **Sprint 1 (High Priority):** 1. Meeting minutes PDF (#46 voting system dependency) 2. Owner financial statements **Sprint 2 (Medium Priority):** 3. Ownership contracts 4. Annual financial reports **Sprint 3 (Low Priority):** 5. Work quote documents ## Testing - [ ] Generate meeting minutes PDF with sample data - [ ] Generate owner statement PDF with expenses - [ ] Generate ownership contract PDF - [ ] PDF rendering correct (margins, fonts, pagination) - [ ] Multi-page documents handled correctly - [ ] Charts render correctly in PDFs - [ ] Performance acceptable (<2s for typical document) ## Acceptance Criteria - [ ] Meeting minutes PDF exporter complete - [ ] Owner financial statement PDF exporter complete - [ ] Ownership contract PDF exporter complete - [ ] Annual report PDF exporter complete (optional charts) - [ ] Work quote PDF exporter complete - [ ] HTML template system integrated (Tera) - [ ] API endpoints functional - [ ] Tests passing - [ ] Documentation updated ## Effort Estimate **Medium** (3-4 days) - Day 1: Meeting minutes PDF + Tera templates setup - Day 2: Owner financial statement PDF - Day 3: Ownership contract PDF - Day 4: Annual report + charts (optional) ## Related - Depends on: Issue #46 (voting system for meeting minutes) - Enhances: Current PDF generation (PCN reports) - Supports: Document management ## References - printpdf: https://docs.rs/printpdf/ - Tera templates: https://tera.netlify.app/ - headless_chrome: https://docs.rs/headless_chrome/ - plotters: https://docs.rs/plotters/ .. raw:: html