diff --git a/flutter/lib/api/odoo_client.dart b/flutter/lib/api/odoo_client.dart index 8945e68..3287545 100644 --- a/flutter/lib/api/odoo_client.dart +++ b/flutter/lib/api/odoo_client.dart @@ -44,24 +44,6 @@ 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( @@ -145,4 +127,41 @@ class OdooClient { } void close() => _http.close(); + + Future logout(int uid, String db, String password) async { + + int uid = await authenticate(); + + final payload = OdooRpcHelper.buildLogout( + service: "common", + db: db, + uid: uid, + password: password, + model: "", // or just use session logout + method: "", // some versions expose this + domain: [[]], + kwargs: {}, + ); + + 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}'); + } + } + } diff --git a/flutter/lib/home_screen.dart b/flutter/lib/home_screen.dart index 98e8bc1..ea119e6 100644 --- a/flutter/lib/home_screen.dart +++ b/flutter/lib/home_screen.dart @@ -297,3 +297,4 @@ String _formatAbs(double v) { final a = v.abs().toStringAsFixed(0); return a.replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.'); } + diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 8c4f5aa..844f475 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -49,8 +49,31 @@ class _RootScreenState extends State { } Future _logout() async { - await Session.clear(); - setState(() => _check = Session.load()); + // try { + // String base = await Session.readBase(); + // String? db = await Session.readDb(); + // String? pass = await Session.readPass(); + // String? user = await Session.readUser(); + // + // int uid = await Session.readUid() ?? 0; + // + // final c = OdooClient( + // baseUrl: base, db: db!, username: user!, + // password: pass!, + // ); + // + // c.logout(uid, db, pass); + // } catch(e) { + // // leave it empty + // } + + await Session.clear(); + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const RootScreen()), + (route) => false, + ); } @override @@ -177,7 +200,10 @@ class _LoginScreenState extends State { baseUrl: base, db: _db.text.trim(), username: _user.text.trim(), password: _pass.text, ); - await c.authenticate(); // hit; lempar bila gagal + int _uid = await c.authenticate(); // hit; lempar bila gagal + if (_uid > 0) { + await Session.saveUid(_uid); + } await Session.save(base, _db.text.trim(), _user.text.trim(), _pass.text); if (!mounted) return; widget.onSuccess(base: base, db: _db.text.trim(), user: _user.text.trim(), pass: _pass.text); diff --git a/flutter/lib/odoo_rpc_helper.dart b/flutter/lib/odoo_rpc_helper.dart index 1c0a4b5..702d643 100644 --- a/flutter/lib/odoo_rpc_helper.dart +++ b/flutter/lib/odoo_rpc_helper.dart @@ -4,8 +4,8 @@ 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({ + static Map buildLogout({ + required String service, required String db, required int uid, required String password, @@ -21,7 +21,40 @@ class OdooRpcHelper { "jsonrpc": _jsonRpcVersion, "method": _method, "params": { - "service": "object", + "service": service, + "method": "logout", + "args": [ + db, + uid, + password, + model, + method, + safeDomain, + kwargs, + ], + }, + "id": DateTime.now().millisecondsSinceEpoch, + }; + } + /// Builds the correct JSON-RPC payload for any Odoo model method + static Map buildExecuteKw({ + required String service, + 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": service, "method": "execute_kw", "args": [ db, @@ -57,6 +90,7 @@ class OdooRpcHelper { if (order != null) kwargs["order"] = order; return buildExecuteKw( + service: "object", db: db, uid: uid, password: password, @@ -67,6 +101,37 @@ class OdooRpcHelper { ); } + static Map executeKw({ + required String method, + 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( + service: "object", + db: db, + uid: uid, + password: password, + model: model, + method: method, + domain: domain, + kwargs: kwargs, + ); + } + /// Convenience wrapper for create static Map create({ required String db, @@ -76,6 +141,7 @@ class OdooRpcHelper { required Map values, }) { return buildExecuteKw( + service: "object", db: db, uid: uid, password: password, diff --git a/flutter/lib/services/session.dart b/flutter/lib/services/session.dart index e4c93cf..5f13ad9 100644 --- a/flutter/lib/services/session.dart +++ b/flutter/lib/services/session.dart @@ -4,12 +4,17 @@ const _kBase = 'odoo_base'; const _kDb = 'odoo_db'; const _kUser = 'odoo_user'; const _kPass = 'odoo_pass'; +const _kUid = 'odoo_uid'; /// Menyimpan konfigurasi login (url, db, user, password) secara aman /// + menyediakan OdooClient terautentikasi. class Session { static const _storage = FlutterSecureStorage(); + static Future saveUid(int uid) async { + await _storage.write(key: _kUid, value: uid.toString()); + } + static Future save(String baseUrl, String db, String user, String pass) async { await _storage.write(key: _kBase, value: baseUrl); await _storage.write(key: _kDb, value: db); @@ -35,4 +40,28 @@ class Session { static Future readLastUrl() async { return _storage.read(key: _kBase); } + + static Future readBase() async { + final base = await _storage.read(key: _kBase); + if (base == null) throw Exception('URL tidak ditemukan'); + return base; + } + + static Future readDb() async { + return _storage.read(key: _kDb); + } + + static Future readUser() async { + return _storage.read(key: _kUser); + } + + static Future readPass() async { + return _storage.read(key: _kPass); + } + + static Future readUid() async { + final uid = await _storage.read(key: _kUid); + if (uid == null) return null; + return int.tryParse(uid); + } } diff --git a/flutter/lib/services/stock_service.dart b/flutter/lib/services/stock_service.dart index 10c5dda..72577fb 100644 --- a/flutter/lib/services/stock_service.dart +++ b/flutter/lib/services/stock_service.dart @@ -9,31 +9,6 @@ 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 fields = ['name', 'perusahaan', 'sector']; final res = await client.executeSearchReadPayload("saat.saham", fields); @@ -41,39 +16,6 @@ class StockService { } // ---- 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 fields = [ 'saham_id', 'sisa_lembar', 'avg_cost', 'harga_sekarang', @@ -84,22 +26,6 @@ class StockService { } // ---- 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 fields = ['tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total']; final res = await client.executeSearchReadPayload("saat.transaksi", fields);