Skip to content

Instantly share code, notes, and snippets.

@kururu-abdo
Created June 3, 2025 10:46
Show Gist options
  • Save kururu-abdo/0000ecd7fb6fead749cdb3441d59b6f8 to your computer and use it in GitHub Desktop.
Save kururu-abdo/0000ecd7fb6fead749cdb3441d59b6f8 to your computer and use it in GitHub Desktop.
import 'dart:convert';
import 'dart:developer';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class OdooRpcClient {
final String baseUrl;
final String db;
String? _sessionId; // To store the session ID
final String _authPath = '/web/session/authenticate';
final String _callPath = '/web/dataset/call_kw'; // For standard calls
OdooRpcClient({required this.baseUrl, required this.db});
// --- Session Management ---
Future<void> _saveSessionId(String sessionId) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('odoo_session_id', sessionId);
_sessionId = sessionId;
}
Future<void> _loadSessionId() async {
final prefs = await SharedPreferences.getInstance();
_sessionId = prefs.getString('odoo_session_id');
}
Future<void> clearSession() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('odoo_session_id');
_sessionId = null;
}
// --- Core RPC Method ---
Future<dynamic> _makeRpcCall({
required String path,
required Map<String, dynamic> params,
bool requiresAuth = true,
}) async {
if (requiresAuth && _sessionId == null) {
await _loadSessionId(); // Try to load session if not present
if (_sessionId == null) {
throw Exception('Not authenticated. Please login first.');
}
}
final url = Uri.parse('$baseUrl$path');
final headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// Add session ID to cookies if available
if (_sessionId != null) {
headers['Cookie'] = 'session_id=$_sessionId';
}
final requestBody = jsonEncode({
'jsonrpc': '2.0',
'method': 'call',
'params': params,
'id': DateTime.now().millisecondsSinceEpoch, // Unique ID for the request
});
try {
final response = await http.post(
url,
headers: headers,
body: requestBody,
);
// Extract session ID from cookies for subsequent requests
final setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader != null) {
final match = RegExp(r'session_id=([^;]+)').firstMatch(setCookieHeader);
if (match != null && match.group(1) != null) {
_sessionId = match.group(1)!;
_saveSessionId(_sessionId!);
}
}
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = jsonDecode(response.body);
if (responseData.containsKey('error')) {
throw OdooRpcException(responseData['error']);
}
log('Response: ${responseData.toString()}');
if (responseData.containsKey('result')) {
return responseData['result'];
}
return responseData;
} else {
// log('HTTP Error: ${response.statusCode} - ${response.reasonPhrase}');
throw Exception('HTTP Error: ${response.statusCode} - ${response.reasonPhrase}');
}
} catch (e) {
// log(e.toString());
// Handle network errors, JSON parsing errors etc.
rethrow;
}
}
// --- Specific Odoo Methods ---
/// Authenticates a user and stores the session ID.
Future<Map<String, dynamic>?> authenticate(String username, String password) async {
final params = {
'db': 'Echoemaar_ERP',
'login': username,
'password': password,
};
try {
final result = await _makeRpcCall(
path: _authPath,
params: params,
requiresAuth: false, // Authentication itself doesn't require a prior session
);
return result; // Return the result directly
// Check if authentication was successful
if (result['uid'] != null) {
// Session ID is typically set in cookies upon successful authentication
// and handled by _makeRpcCall, but we can double check
if (_sessionId == null) {
// This might happen if the server doesn't send the session_id cookie
// immediately on authenticate, but usually it does.
// For robust apps, you might need to manually extract from headers if needed.
return null;
}
await _saveSessionId(result['session_id'] ?? ''); // Save session ID if available
return result;
}
return null;
} catch (e) {
// Clear session if authentication fails
await clearSession();
rethrow;
}
}
/// Generic method to call any Odoo model method.
///
/// [model]: The Odoo model name (e.g., 'res.partner', 'product.product').
/// [method]: The method name (e.g., 'search_read', 'create', 'write', 'unlink').
/// [args]: List of positional arguments for the method.
/// [kwargs]: Map of keyword arguments for the method.
Future<dynamic> callKw({
required String model,
required String method,
List args = const [],
Map<String, dynamic> kwargs = const {},
}) async {
final params = {
'model': model,
'method': method,
'args': args,
'kwargs': kwargs,
};
return await _makeRpcCall(path: _callPath, params: params);
}
/// Example: Read records from a model.
Future<List<dynamic>> read({
required String model,
required List<int> ids,
List<String> fields = const [],
}) async {
return await callKw(
model: model,
method: 'read',
args: [ids, fields],
);
}
/// Example: Search and read records from a model.
Future<dynamic> searchRead({
required String model,
List domain = const [],
List<String> fields = const [],
int offset = 0,
int limit = 0, // 0 for no limit
String order = '',
}) async {
final kwargs = {
'domain': domain,
'fields': fields,
'offset': offset,
'limit': limit,
'order': order,
};
// var result = await callKw(
// model: model,
// method: 'search_read',
// kwargs: kwargs,
// );
// log(result.toString());
// return result;
return await callKw(
model: model,
method: 'search_read',
kwargs: kwargs,
);
}
/// Example: Create a new record.
Future<int> create({
required String model,
required Map<String, dynamic> values,
}) async {
return await callKw(
model: model,
method: 'create',
args: [values],
);
}
/// Example: Update existing records.
Future<bool> write({
required String model,
required List<int> ids,
required Map<String, dynamic> values,
}) async {
return await callKw(
model: model,
method: 'write',
args: [ids, values],
);
}
/// Example: Delete records.
Future<bool> unlink({
required String model,
required List<int> ids,
}) async {
return await callKw(
model: model,
method: 'unlink',
args: [ids],
);
}
}
// Custom Exception for Odoo RPC errors
class OdooRpcException implements Exception {
final Map<String, dynamic> error;
OdooRpcException(this.error);
@override
String toString() {
final String message = error['data']?['message'] ?? error['message'] ?? 'Unknown Odoo RPC error';
final String debug = error['data']?['debug'] ?? '';
return 'OdooRpcException: $message\nDebug: $debug';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment