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:
Description
## 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<Resolution>,
attendees: Vec<(Owner, f64)>, // (owner, voting_power)
) -> Result<Vec<u8>, 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<Utc>,
end_date: DateTime<Utc>,
) -> Result<Vec<u8>, 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<Vec<u8>, 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<Expense>) -> Vec<u8> {
// 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<Vec<u8>, 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
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial; font-size: 12pt; }
h1 { text-align: center; color: #2c3e50; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
<h1>Procès-Verbal d'Assemblée Générale</h1>
<h2>Informations</h2>
<p>Immeuble: {{ building.name }}</p>
<p>Date: {{ meeting.date }}</p>
<p>Type: {{ meeting.meeting_type }}</p>
<h2>Présences</h2>
<table>
<tr><th>Copropriétaire</th><th>Millièmes</th></tr>
{% for attendee in attendees %}
<tr><td>{{ attendee.name }}</td><td>{{ attendee.voting_power }}</td></tr>
{% endfor %}
</table>
<h2>Résolutions</h2>
{% for resolution in resolutions %}
<div class="resolution">
<h3>{{ resolution.title }}</h3>
<p>{{ resolution.description }}</p>
<p><strong>Résultat:</strong> {{ resolution.status }}</p>
<p>Pour: {{ resolution.vote_count_pour }} | Contre: {{ resolution.vote_count_contre }}</p>
</div>
{% endfor %}
</body>
</html>
```
**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/