Enable Dark Mode!
how-to-fetch-related-fields-many2one-and-one2many-from-odoo-in-a-flutter-app.jpg
By: Jegdev S

How to Fetch Related Fields (Many2one & One2many) from Odoo in a Flutter App

Technical Flutter

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:

  1. Fetch the parent records (get list of order_line IDs).
  2. 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

ScenarioRecommended ApproachWhy
Simple Many2one display nameInclude field in search_readFast, no extra call
Need extra Many2one fieldsSeparate read on the IDFull control
One2many / Many2manyFetch IDs > read child modelOnly way to get full data
Performance-critical listsLimit fields + prefetch only needed relationsAvoid 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.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, KINFRA Techno Park
Kakkanchery, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message