Odoo uses relational fields extensively to connect models (e.g., a Sales Order links to a Customer via Many2one and to Order Lines via One2many). When building a Flutter app that consumes Odoo data, you need to handle these fields correctly because the JSON-RPC API returns them differently:
Many2one > [id, display_name] (tuple)
One2many / Many2many > list of IDs only
In this blog, we’ll explore how to fetch and display these fields using the popular odoo_rpc package, with clean helper classes and examples based on the sale.order model.
1. Setting Up Odoo RPC Connection in Flutter
Step 1: Create a new Flutter project
flutter create odoo_flutter_app
cd odoo_flutter_app
Step 2: Add the dependency
In pubspec.yaml:
dependencies:
odoo_rpc: ^0.4.0 # Check pub.dev for latest version
flutter:
sdk: flutter
Run:
flutter pub get
Step 3: Create a helper class (lib/odoo_helper.dart)
import 'package:odoo_rpc/odoo_rpc.dart';
class OdooHelper {
static final OdooClient client = OdooClient('https://your-odoo-instance.com');
static Future<void> authenticate() async {
try {
await client.authenticate('your_db_name', 'your_username', 'your_password');
print('Authenticated successfully');
} catch (e) {
print('Authentication failed: $e');
rethrow;
}
}
/// Generic search_read
static Future<List<dynamic>> searchRead({
required String model,
List<dynamic> domain = const [],
List<String> fields = const [],
}) async {
return await client.callKw({
'model': model,
'method': 'search_read',
'args': [domain],
'kwargs': {
'fields': fields,
'context': {'bin_size': true},
},
});
}
/// Read specific records by IDs (useful for One2many)
static Future<List<dynamic>> read({
required String model,
required List<int> ids,
List<String> fields = const [],
}) async {
return await client.callKw({
'model': model,
'method': 'read',
'args': [ids, fields],
'kwargs': {},
});
}
}
Call authentication once (usually in main.dart):
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await OdooHelper.authenticate();
runApp(const MyApp());
}2. Fetching Many2one Fields
Many2one fields return [id, display_name] automatically when included in fields.
Example: Fetch Sales Orders with Customer (partner_id)
Future<List<Map<String, dynamic>>> fetchSalesOrders() async {
final records = await OdooHelper.searchRead(
model: 'sale.order',
fields: ['name', 'partner_id', 'amount_total', 'date_order'],
);
return records.cast<Map<String, dynamic>>();
}Usage in UI:
ListView.builder(
itemCount: sales.length,
itemBuilder: (context, index) {
final order = sales[index];
final partner = order['partner_id']; // [id, name]
return ListTile(
title: Text(order['name']),
subtitle: Text('Customer: ${partner[1]}'), // partner[1] = display name
trailing: Text('\$${order['amount_total']}'),
);
},
);
Key Points:
partner_id > [id, name]
Access name with partner[1]
If you need more partner fields, do a separate read on the ID (see below).
3. Fetching One2many Fields (with full related data)
One2many fields only return IDs. To get the actual records:
- Fetch the parent records (get list of order_line IDs).
- Use OdooHelper.read() on the child model with those IDs.
Example: Sales Order + Order Lines
Future<List<Map<String, dynamic>>> fetchSalesWithLines() async {
// Step 1: Get orders with order_line IDs
final orders = await OdooHelper.searchRead(
model: 'sale.order',
fields: ['name', 'partner_id', 'order_line', 'amount_total'],
);
// Step 2: Collect all line IDs
final allLineIds = <int>[];
for (var order in orders) {
final lineIds = (order['order_line'] as List).cast<int>();
allLineIds.addAll(lineIds);
}
// Step 3: Read full line details
final lines = allLineIds.isNotEmpty
? await OdooHelper.read(
model: 'sale.order.line',
ids: allLineIds,
fields: ['name', 'product_id', 'product_uom_qty', 'price_unit', 'price_subtotal'],
)
: [];
// Step 4: Map lines back to orders (optional but clean)
final lineMap = {for (var line in lines) line['id']: line};
return orders.map((order) {
final lineIds = (order['order_line'] as List).cast<int>();
order['lines'] = lineIds.map((id) => lineMap[id]).toList();
return order;
}).toList();
}UI Example:
ExpansionTile(
title: Text(order['name']),
subtitle: Text('Customer: ${order['partner_id'][1]}'),
children: (order['lines'] as List).map((line) {
return ListTile(
title: Text(line['name']),
trailing: Text('${line['product_uom_qty']} × \$${line['price_unit']}'),
);
}).toList(),
)
4. Complete Example Widget (Sales Orders Screen)
class SalesScreen extends StatefulWidget {
const SalesScreen({super.key});
@override
State<SalesScreen> createState() => _SalesScreenState();
}
class _SalesScreenState extends State<SalesScreen> {
List<dynamic> _orders = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadOrders();
}
Future<void> _loadOrders() async {
final data = await fetchSalesWithLines();
setState(() {
_orders = data;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sales Orders')),
body: _loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _orders.length,
itemBuilder: (context, i) => buildOrderTile(_orders[i]),
),
);
}
}5. Handling Null, Empty, and Optional Relational Fields
In real Odoo databases, relational fields are not always populated. A Flutter app must defensively handle these cases to avoid runtime crashes.
Many2one Null Handling
A Many2one field can be false if not set.
Example response:
"partner_id": false
Safe Flutter handling:
final partner = order['partner_id'];
final customerName = partner != false ? partner[1] : 'No Customer';
UI Example:
subtitle: Text('Customer: $customerName'),One2many Empty Lists
If a record has no child records, Odoo returns an empty list:
"order_line": []
Always assume an empty list is valid:
final lineIds = (order['order_line'] as List?)?.cast<int>() ?? [];
Never assume relational fields exist or contain data — always guard against false, null, or empty arrays.
When to Use What
| Scenario | Recommended Approach | Why |
| Simple Many2one display name | Include field in search_read | Fast, no extra call |
| Need extra Many2one fields | Separate read on the ID | Full control |
| One2many / Many2many | Fetch IDs > read child model | Only way to get full data |
| Performance-critical lists | Limit fields + prefetch only needed relations | Avoid N+1 queries |
Fetching relational data from Odoo in a Flutter application becomes simple and predictable once you understand how Odoo’s JSON-RPC API represents relationships. Many2one fields are returned as a tuple in the form of [id, display_name], making them ideal for fast, user-friendly displays, while One2many and Many2many fields return only a list of record IDs, requiring an additional read to retrieve full related data.
Using a clean OdooHelper class + odoo_rpc package gives you reusable, readable code that scales to any model (products, invoices, employees, etc.).
To read more about How to Read Odoo Records with Filters (Domains) in Flutter, refer to our blog How to Read Odoo Records with Filters (Domains) in Flutter.