Created
June 3, 2025 10:46
-
-
Save kururu-abdo/0000ecd7fb6fead749cdb3441d59b6f8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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