One of Odoo's most powerful features is its QWeb Reporting Engine, which generates pixel-perfect PDF documents for Invoices, Sales Orders, Delivery Slips, and more. When building a mobile app for Odoo using Flutter, you face a critical architectural decision:
Client-Side Generation
Re-create the document layout directly in Flutter code using packages like pdf.
Pros
- Works offline
- Total control over rendering
Cons
- Double maintenance
- Every Odoo report change (tax laws, branding, layouts) requires Flutter updates.
Server-Side Fetching
Download the exact PDF generated by Odoo.
Pros
- Single source of truth
- Mobile and Web PDFs are always identical.
Cons
- Requires online connectivity
For official business documents (Invoices, Quotations, Picking Slips), Server-Side Fetching is the gold standard. This guide offers a comprehensive exploration of implementing this robust pattern in Flutter.
Prerequisites
Odoo Instance
- Odoo v14, v15, v16, or v17+
- Community or Enterprise edition
Flutter Project Dependencies
dependencies:
http: ^1.2.0 # Raw HTTP requests
path_provider: ^2.1.2 # Access temporary storage
open_file: ^3.3.2 # Open PDF in native viewer
share_plus: ^9.0.0 # Share via WhatsApp / Email
Core Concepts
1. The Anatomy of a Report URL
Odoo exposes a dedicated HTTP endpoint for downloading PDF reports.
{base_url}/report/pdf/{report_xml_id}/{document_ids}Parameters explained:
- base_url – Your Odoo server URL (e.g., https://my-odoo-instance.com)
- report_xml_id – The technical identifier of the report
- document_ids – A single ID (123) or multiple IDs (123,124) merged into one PDF
Common Report XML IDs
- Invoice: account.report_invoice_with_payments
- Sales Order: sale.report_saleorder
- Delivery Slip: stock.report_deliveryslip
2. The Authentication Challenge
Downloading a report is a standard HTTP GET request, not a JSON-RPC call.
If you simply call:
http.get(url)
Odoo treats the request as anonymous and returns:
- An HTML login page, or
- A 404 / access error
Required Technique
Manually inject the session ID into the Cookie header:
Cookie: session_id=YOUR_SESSION_ID
This step is critical.
Implementation: The Robust Way
We’ll build a production-grade PdfReportService that handles:
- Authentication
- URL sanitation
- File I/O
- Content validation
- Viewing and sharing
Step 1: Design Logic
The service must:
- Accept Odoo connection details
- Construct a valid report URL
- Download binary data
- Verify the response is a PDF
- Save it securely
- Open or share it
Step 2: Production-Ready Service Class
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:open_file/open_file.dart';
import 'package:share_plus/share_plus.dart';
class PdfReportService {
/// Downloads a report from Odoo and returns the local File
Future<File> downloadReport({
required String baseUrl,
required String sessionId,
required String reportXmlId,
required int docId,
required String fileName,
}) async {
final cleanBaseUrl = baseUrl.endsWith('/')
? baseUrl.substring(0, baseUrl.length - 1)
: baseUrl;
final url = Uri.parse('$cleanBaseUrl/report/pdf/$reportXmlId/$docId');
try {
final response = await http.get(
url,
headers: {
'Cookie': 'session_id=$sessionId', // CRITICAL
},
).timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
throw Exception(
'Failed to download report. Status: ${response.statusCode}'
);
}
final contentType = response.headers['content-type'];
if (contentType != null && !contentType.contains('application/pdf')) {
throw Exception('Expected PDF, got $contentType');
}
final bytes = response.bodyBytes;
if (bytes.isEmpty) {
throw Exception('Received empty PDF from server');
}
final tempDir = await getTemporaryDirectory();
final safeName = fileName.replaceAll(RegExp(r'[^a-zA-Z0-9_.-]'), '_');
final file = File('${tempDir.path}/$safeName.pdf');
await file.writeAsBytes(bytes, flush: true);
return file;
} catch (e) {
print('downloadReport error: $e');
rethrow;
}
}
/// Opens the PDF using the native viewer
Future<void> openFile(File file) async {
final result = await OpenFile.open(file.path);
if (result.type != ResultType.done) {
throw Exception('Could not open file: ${result.message}');
}
}
}
Step 3: Finding the Report XML ID
- Enable Developer Mode in Odoo
- Navigate to Settings > Technical > Reports
- Search for the document ( Invoice, Sales Order, etc.)
- Copy the XML ID
Frequently Used IDs
- sale.report_saleorder
- account.report_invoice_with_payments
- stock.report_picking
- stock.report_deliveryslip
Handling Advanced Scenarios
1. Fallback Mechanism for Invoices
Different localizations install different invoice reports. Hardcoding one ID can cause failures.
Future<void> printInvoice(int invoiceId) async {
const candidates = [
'account.report_invoice_with_payments',
'account.report_invoice',
'account.report_invoice_document',
];
File? pdfFile;
for (final reportId in candidates) {
try {
pdfFile = await myService.downloadReport(
reportXmlId: reportId,
docId: invoiceId,
);
break;
} catch (_) {
continue;
}
}
if (pdfFile != null) {
await myService.openFile(pdfFile);
} else {
showError('Could not generate invoice PDF');
}
}2. Sharing via WhatsApp or Email
Future<void> shareFile(File file, String subject) async {
final xFile = XFile(file.path);
await Share.shareXFiles(
[xFile],
subject: subject,
text: 'Please find attached: $subject',
);
}3. HTML Instead of PDF (Most Common Bug)
Symptom
- Status code 200
- File opens as a blank page or HTML.
Cause
- Expired or invalid session_id
Fix
- Check content-type
- If text/html, trigger re-login
- Refresh session and retry.
Server-side PDF fetching is the backbone of any serious Flutter–Odoo ERP application. By leveraging Odoo’s QWeb engine and carefully handling cookie-based authentication, you ensure that mobile users receive documents that perfectly match backend expectations.
With a robust download, validation, and fallback strategy in place, your Flutter app delivers a professional, enterprise-grade document experience.
For Flutter developers focused on building advanced business applications, the Mobo mobile application offers a dependable integration framework that complements this approach. With secure session handling, real-time data synchronization, and resilient error management, Mobo supports the development of scalable, enterprise-level mobile solutions that align seamlessly with Odoo-powered systems.
To read more about How to Authenticate Mobo Apps with Odoo: Login, Sessions & Token Security Explained, refer to our blog How to Authenticate Mobo Apps with Odoo: Login, Sessions & Token Security Explained.