diff --git a/flutter/lib/api/odoo_client.dart b/flutter/lib/api/odoo_client.dart index bef1abd..8945e68 100644 --- a/flutter/lib/api/odoo_client.dart +++ b/flutter/lib/api/odoo_client.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/cupertino.dart'; import 'package:http/http.dart' as http; +import '../odoo_rpc_helper.dart'; /// Klien JSON-RPC utk Odoo 17. Authenticate + execute_kw. class OdooClient { @@ -42,6 +44,68 @@ class OdooClient { return _call('object', 'execute_kw', params); } + Future executeKwdWithArgs(String model, String method, + {List fields = const [], Map? kwargs}) async { + _uid ??= await authenticate(); + final args = [ + + db, + _uid, + password, + model, + method, + [], + fields, + + if (kwargs != null) kwargs, + ]; + return _call('object', 'execute_kw', args); + } + + Future executeSearchReadPayload(String model, List fields) async { + int uid = await authenticate(); + final payload = OdooRpcHelper.searchRead( + db: db, + uid: uid, + password: password, + model: model, + domain: [[]], + fields: fields, + limit: 20, + ); + + return _callSearchRead(payload); + } + + Future _callSearchRead(Map payload) async{ + // Use jsonEncode along with a JsonEncoder for indentation + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + String prettyString = encoder.convert(payload); + + debugPrint(prettyString); + + try { + final resp = await _http + .post(_endpoint, + headers: {'Content-Type': 'application/json'}, body: jsonEncode(payload)) + .timeout(const Duration(seconds: 25)); + final json = jsonDecode(resp.body) as Map; + if (json.containsKey('error')) { + final err = json['error'] as Map?; + String? message = (err?['data'] as Map?)?['message'] as String? ?? + (err?['message'] as String?) ?? + 'Unknown Odoo error'; + throw Exception('Odoo error: $message'); + } + return json['result']; + } on TimeoutException { + throw Exception('Timeout — pastikan server Odoo:18069 tercapai'); + } on http.ClientException catch (e) { + throw Exception('Koneksi gagal: ${e.message}'); + } + + } + Future _call(String service, String method, List args) async { final body = { 'jsonrpc': '2.0', @@ -52,6 +116,13 @@ class OdooClient { 'args': args, }, }; + + // Use jsonEncode along with a JsonEncoder for indentation + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + String prettyString = encoder.convert(body); + + debugPrint(prettyString); + try { final resp = await _http .post(_endpoint, diff --git a/flutter/lib/odoo_rpc_helper.dart b/flutter/lib/odoo_rpc_helper.dart new file mode 100644 index 0000000..1c0a4b5 --- /dev/null +++ b/flutter/lib/odoo_rpc_helper.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +class OdooRpcHelper { + static const String _jsonRpcVersion = "2.0"; + static const String _method = "call"; + + /// Builds the correct JSON-RPC payload for any Odoo model method + static Map buildExecuteKw({ + required String db, + required int uid, + required String password, + required String model, + required String method, + List domain = const [], + Map kwargs = const {}, + }) { + // Make sure domain is always a list of lists (even if empty) + final safeDomain = domain.isEmpty ? [[]] : domain; + + return { + "jsonrpc": _jsonRpcVersion, + "method": _method, + "params": { + "service": "object", + "method": "execute_kw", + "args": [ + db, + uid, + password, + model, + method, + safeDomain, + kwargs, + ], + }, + "id": DateTime.now().millisecondsSinceEpoch, + }; + } + + /// Convenience wrapper for search_read + static Map searchRead({ + required String db, + required int uid, + required String password, + required String model, + List domain = const [], + List fields = const [], + int? limit, + int? offset, + String? order, + }) { + final kwargs = {}; + + if (fields.isNotEmpty) kwargs["fields"] = fields; + if (limit != null) kwargs["limit"] = limit; + if (offset != null) kwargs["offset"] = offset; + if (order != null) kwargs["order"] = order; + + return buildExecuteKw( + db: db, + uid: uid, + password: password, + model: model, + method: "search_read", + domain: domain, + kwargs: kwargs, + ); + } + + /// Convenience wrapper for create + static Map create({ + required String db, + required int uid, + required String password, + required String model, + required Map values, + }) { + return buildExecuteKw( + db: db, + uid: uid, + password: password, + model: model, + method: "create", + domain: [[]], // create doesn't use domain + kwargs: {"vals": values}, // Odoo expects 'vals' for create in some versions + ); + } +} \ No newline at end of file diff --git a/flutter/lib/services/stock_service.dart b/flutter/lib/services/stock_service.dart index 167b224..10c5dda 100644 --- a/flutter/lib/services/stock_service.dart +++ b/flutter/lib/services/stock_service.dart @@ -9,51 +9,100 @@ class StockService { final OdooClient client; // ---- Saham ---- + // Future> listSahamOld() async { + // final res = await client.executeKwd( + // 'saat.saham', + // 'search_read', + // args: [ + // "saat.saham", + // "search_read", + // [[]], + // {'fields': ['name', 'perusahaan', 'sector'], "limit": 100, "offset": 0, "order": "name asc"}, + // ], + // ); + // return (res as List).map((e) => Saham.fromOdoo(e as Map)).toList(); + // } + // + // Future> listSaham() async { + // final res = await client.executeKwdWithArgs( + // 'saat.saham', + // 'search_read', + // fields: [ + // {'fields': ['name', 'perusahaan', 'sector'],} + // ] + // ); + // return (res as List).map((e) => Saham.fromOdoo(e as Map)).toList(); + // } + Future> listSaham() async { - final res = await client.executeKwd( - 'saat.saham', - 'search_read', - args: [ - [], - {'fields': ['name', 'perusahaan', 'sector']}, - ], - ); + final fields = ['name', 'perusahaan', 'sector']; + final res = await client.executeSearchReadPayload("saat.saham", fields); return (res as List).map((e) => Saham.fromOdoo(e as Map)).toList(); } // ---- Portofolio ---- + // Future> listPortofolio() async { + // final res = await client.executeKwd( + // 'saat.portofolio', + // 'search_read', + // args: [ + // [], + // { + // 'fields': [ + // 'saham_id', 'sisa_lembar', 'avg_cost', 'harga_sekarang', + // 'nilai_sekarang', 'realized_pl', 'unrealized_pl', + // ] + // }, + // ], + // ); + // return (res as List).map((e) => Portfolio.fromOdoo(e as Map)).toList(); + // } + + // Future> listPortofolio() async { + // final res = await client.executeKwdWithArgs( + // 'saat.portofolio', + // 'search_read', + // fields: [ + // { + // "fields": ["saham_id", "sisa_lembar", "avg_cost", "harga_sekarang", + // "nilai_sekarang", "realized_pl", "unrealized_pl"], + // "limit": 20, + // "offset": 0 + // } + // ], + // ); + // return (res as List).map((e) => Portfolio.fromOdoo(e as Map)).toList(); + // } + Future> listPortofolio() async { - final res = await client.executeKwd( - 'saat.portofolio', - 'search_read', - args: [ - [], - { - 'fields': [ - 'saham_id', 'sisa_lembar', 'avg_cost', 'harga_sekarang', - 'nilai_sekarang', 'realized_pl', 'unrealized_pl', - ] - }, - ], - ); + final fields = [ + 'saham_id', 'sisa_lembar', 'avg_cost', 'harga_sekarang', + 'nilai_sekarang', 'realized_pl', 'unrealized_pl', + ]; + final res = await client.executeSearchReadPayload("saat.portofolio", fields); return (res as List).map((e) => Portfolio.fromOdoo(e as Map)).toList(); } // ---- Transaksi ---- + // Future> listTransaksi() async { + // final res = await client.executeKwdWithArgs( + // 'saat.transaksi', + // 'search_read', + // fields: [ + // { + // 'fields': [ + // 'tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total' + // ], + // 'order': 'tanggal desc, id desc', + // }, + // ], + // ); + // return (res as List).map((e) => Transaksi.fromOdoo(e as Map)).toList(); + // } + Future> listTransaksi() async { - final res = await client.executeKwd( - 'saat.transaksi', - 'search_read', - args: [ - [], - { - 'fields': [ - 'tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total' - ], - 'order': 'tanggal desc, id desc', - }, - ], - ); + final fields = ['tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total']; + final res = await client.executeSearchReadPayload("saat.transaksi", fields); return (res as List).map((e) => Transaksi.fromOdoo(e as Map)).toList(); }