From 39131f05b8d0c02f7a43b26f49fb05db42b6a37c Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 5 Oct 2026 17:17:29 +0700 Subject: [PATCH] add cancel transaction --- addons/saat_invest/models/portofolio.py | 2 +- addons/saat_invest/models/transaksi.py | 13 +++ addons/saat_invest/views/transaksi_views.xml | 8 +- flutter/lib/home_screen.dart | 109 ++++++++++++++++++- flutter/lib/models/saham_transaksi.dart | 4 + flutter/lib/services/stock_service.dart | 6 +- flutter/test/widget_test.dart | 4 +- 7 files changed, 137 insertions(+), 9 deletions(-) diff --git a/addons/saat_invest/models/portofolio.py b/addons/saat_invest/models/portofolio.py index 1fc56be..a1b94e2 100644 --- a/addons/saat_invest/models/portofolio.py +++ b/addons/saat_invest/models/portofolio.py @@ -38,7 +38,7 @@ class SaatPortofolio(models.Model): def action_recalculate(self): """Rekalkulasi semua posisi portofolio dari transaksi. Return dict ringkas.""" self.env['saat.portofolio'].search([]).unlink() - trans = self.env['saat.transaksi'].search([], order='tanggal, id') + trans = self.env['saat.transaksi'].search([('state', '=', 'done')], order='tanggal, id') per_saham = {} for t in trans: a = per_saham.setdefault(t.saham_id.id, { diff --git a/addons/saat_invest/models/transaksi.py b/addons/saat_invest/models/transaksi.py index 67cac6d..9692007 100644 --- a/addons/saat_invest/models/transaksi.py +++ b/addons/saat_invest/models/transaksi.py @@ -30,6 +30,11 @@ class SaatTransaksi(models.Model): # IDR return self.env.ref('base.IDR', raise_if_not_found=False) or self.env.company.currency_id + state = fields.Selection([ + ('done', 'Selesai'), + ('cancel', 'Dibatalkan'), + ], string="Status", default='done', required=True, copy=False) + @api.depends('tanggal', 'tipe', 'saham_id') def _compute_name(self): for r in self: @@ -50,6 +55,14 @@ class SaatTransaksi(models.Model): if r.harga < 0: raise models.ValidationError("Harga tidak boleh negatif.") + def action_cancel(self): + self.write({'state': 'cancel'}) + return True + + def action_set_to_done(self): + self.write({'state': 'done'}) + return True + # --- auto-sync portofolio: setiap transaksi berubah, portofolio diperbarui --- def _resync_portofolio(self): """Rekalkulasi portofolio dari seluruh transaksi (idempoten).""" diff --git a/addons/saat_invest/views/transaksi_views.xml b/addons/saat_invest/views/transaksi_views.xml index be341c2..5c4ba9a 100644 --- a/addons/saat_invest/views/transaksi_views.xml +++ b/addons/saat_invest/views/transaksi_views.xml @@ -4,7 +4,7 @@ saat.transaksi.list saat.transaksi - + @@ -12,6 +12,7 @@ + @@ -21,6 +22,11 @@ saat.transaksi
+
+
diff --git a/flutter/lib/home_screen.dart b/flutter/lib/home_screen.dart index 5bc8549..393f561 100644 --- a/flutter/lib/home_screen.dart +++ b/flutter/lib/home_screen.dart @@ -304,6 +304,44 @@ class _TransaksiTabState extends State<_TransaksiTab> { void _reload() => setState(() => _fut = widget.service.listTransaksi()); + Future _cancelTransaksi(Transaksi t) async { + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Batalkan Transaksi'), + content: Text( + 'Apakah Anda yakin ingin membatalkan transaksi ${t.sahamTicker} (${t.volume} lembar)?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Batal'), + ), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('Ya, Batalkan'), + ), + ], + ), + ); + + if (confirm == true) { + try { + await widget.service.cancelTransaksi(t.id); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Transaksi berhasil dibatalkan')), + ); + _reload(); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal membatalkan transaksi: $e')), + ); + } + } + } + @override Widget build(BuildContext context) { return FutureBuilder>( @@ -327,15 +365,76 @@ class _TransaksiTabState extends State<_TransaksiTab> { itemBuilder: (c, i) { final t = list[i]; final buy = t.isBuy; + final isCancelled = t.isCancelled; return ListTile( leading: CircleAvatar( - backgroundColor: - buy ? Colors.green : Colors.red, + backgroundColor: isCancelled + ? Colors.grey + : (buy ? Colors.green : Colors.red), child: Text(buy ? 'B' : 'J', style: const TextStyle(color: Colors.white))), - title: Text('${t.sahamTicker} · ${t.volume} lbr'), - subtitle: Text('${t.tanggal} @ ${_fmt(t.harga)} · fee ${_fmt(t.fee)}'), - trailing: Text(_fmt(t.total)), + title: Row( + children: [ + Text( + '${t.sahamTicker} · ${t.volume} lbr', + style: TextStyle( + decoration: isCancelled + ? TextDecoration.lineThrough + : null, + color: isCancelled ? Colors.grey : null, + ), + ), + if (isCancelled) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Dibatalkan', + style: TextStyle( + fontSize: 10, + color: Colors.red.shade900, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + subtitle: Text( + '${t.tanggal} @ ${_fmt(t.harga)} · fee ${_fmt(t.fee)}', + style: TextStyle( + decoration: isCancelled + ? TextDecoration.lineThrough + : null, + color: isCancelled ? Colors.grey : null, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _fmt(t.total), + style: TextStyle( + decoration: isCancelled + ? TextDecoration.lineThrough + : null, + color: isCancelled ? Colors.grey : null, + ), + ), + if (!isCancelled) + IconButton( + icon: const Icon(Icons.cancel_outlined, + color: Colors.redAccent, size: 20), + tooltip: 'Batalkan transaksi', + onPressed: () => _cancelTransaksi(t), + ), + ], + ), ); }, ), diff --git a/flutter/lib/models/saham_transaksi.dart b/flutter/lib/models/saham_transaksi.dart index 1da7012..f59450c 100644 --- a/flutter/lib/models/saham_transaksi.dart +++ b/flutter/lib/models/saham_transaksi.dart @@ -26,6 +26,7 @@ class Transaksi { final double harga; final double fee; final double total; + final String state; // done | cancel Transaksi({ required this.id, @@ -37,9 +38,11 @@ class Transaksi { required this.harga, required this.fee, required this.total, + this.state = 'done', }); bool get isBuy => tipe == 'buy'; + bool get isCancelled => state == 'cancel'; factory Transaksi.fromOdoo(Map m) { String ticker = ''; @@ -58,6 +61,7 @@ class Transaksi { harga: (m['harga'] as num?)?.toDouble() ?? 0, fee: (m['fee'] as num?)?.toDouble() ?? 0, total: (m['total'] as num?)?.toDouble() ?? 0, + state: (m['state'] as String?) ?? 'done', ); } } diff --git a/flutter/lib/services/stock_service.dart b/flutter/lib/services/stock_service.dart index e92d161..2d7b943 100644 --- a/flutter/lib/services/stock_service.dart +++ b/flutter/lib/services/stock_service.dart @@ -56,7 +56,7 @@ class StockService { // ---- Transaksi ---- Future> listTransaksi() async { - final fields = ['tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total']; + final fields = ['tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total', 'state']; final res = await client.executeSearchReadPayload("saat.transaksi", fields); return (res as List).map((e) => Transaksi.fromOdoo(e as Map)).toList(); } @@ -81,6 +81,10 @@ class StockService { return (res as num).toInt(); } + Future cancelTransaksi(int id) async { + await client.executeKwd('saat.transaksi', 'action_cancel', args: [[id]]); + } + Future hapusTransaksi(List ids) async { await client.executeKwd('saat.transaksi', 'unlink', args: [ids]); } diff --git a/flutter/test/widget_test.dart b/flutter/test/widget_test.dart index 10a4427..b12def8 100644 --- a/flutter/test/widget_test.dart +++ b/flutter/test/widget_test.dart @@ -7,7 +7,9 @@ import 'package:saat_invest_mobile/main.dart'; void main() { testWidgets('App boot shows Saat Invest login', (tester) async { await tester.pumpWidget(const SaatInvestApp()); - await tester.pumpAndSettle(); + await tester.pump(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Masuk'), findsOneWidget); }); }