From b53406791f4a00b1f0b32f23f570873fe1187451 Mon Sep 17 00:00:00 2001 From: Alex Ardhian Date: Wed, 2 Sep 2026 14:02:01 +0700 Subject: [PATCH] Harga live otomatis via cron: Yahoo Finance tiap 6 menit saat jam bursa IDX - saat.saham: field harga_terakhir, harga_update_at + method _cron_fetch_live_prices() - _in_trading_window() pakai timezone Asia/Jakarta eksplisit (container UTC) 08:45-16:00 WIB - Data ir.cron (6 menit): ambil harga saham aktif, update KALAU berubah, sinkronkan ke saat.portofolio.harga_sekarang utk hitung equity - Terverifikasi: 8 saham ter-fetch & ter-update (07:01 UTC / 14:01 WIB) --- README.md | 2 + addons/saat_invest/__manifest__.py | 1 + addons/saat_invest/data/ir_cron.xml | 16 +++++++ addons/saat_invest/models/saham.py | 66 +++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 addons/saat_invest/data/ir_cron.xml diff --git a/README.md b/README.md index 8b397d2..1656a21 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Akses UI: http://localhost:18069 — login `admin` / `admin` - ✅ **Addon `saat_invest` terinstal** (model saham, transaksi, portofolio + seed 5 saham IDX) - ✅ **Portofolio auto-sync**: otomatis diperbarui tiap transaksi ditambah/ubah/hapus (tidak perlu tombol manual; buka menu Portofolio selalu datanya segar) +- ✅ **Live harga pasar (cron)**: ambil & update harga dari Yahoo Finance (.JK) + tiap 6 menit SAAT jam bursa IDX (08:45–16:00 WIB). Update hanya bila berubah. - ✅ End-to-end teruji: buat beli/jual → portofolio ter-update → hitung avg cost & realized/unrealized P/L - ⏳ Aplikasi Flutter (login, catat jual/beli, portfolio, P/L) diff --git a/addons/saat_invest/__manifest__.py b/addons/saat_invest/__manifest__.py index fb4b3e9..13e2829 100644 --- a/addons/saat_invest/__manifest__.py +++ b/addons/saat_invest/__manifest__.py @@ -18,6 +18,7 @@ via JSON-RPC bawaan Odoo. 'data': [ 'security/ir.model.access.csv', 'data/saat.demo.saham.xml', + 'data/ir_cron.xml', 'views/saham_views.xml', 'views/transaksi_views.xml', 'views/portofolio_views.xml', diff --git a/addons/saat_invest/data/ir_cron.xml b/addons/saat_invest/data/ir_cron.xml new file mode 100644 index 0000000..7338c39 --- /dev/null +++ b/addons/saat_invest/data/ir_cron.xml @@ -0,0 +1,16 @@ + + + + + + Saat: Fetch update harga live saham (cron 6m) + + code + model._cron_fetch_live_prices() + 6 + minutes + -1 + True + + + diff --git a/addons/saat_invest/models/saham.py b/addons/saat_invest/models/saham.py index 94ea362..dee9aa3 100644 --- a/addons/saat_invest/models/saham.py +++ b/addons/saat_invest/models/saham.py @@ -1,6 +1,13 @@ # -*- coding: utf-8 -*- +import logging +import urllib.request +import json + +from datetime import datetime from odoo import api, fields, models +_logger = logging.getLogger(__name__) + class SaatSaham(models.Model): """Master daftar saham / emiten yang dicatat.""" @@ -13,6 +20,11 @@ class SaatSaham(models.Model): bursa = fields.Char(string="Bursa", default="IDX") sector = fields.Char(string="Sektor") active = fields.Boolean(string="Aktif", default=True) + # Harga pasar terakhir (hasil fetch live), hanya utk jalur cron + harga_terakhir = fields.Monetary(string="Harga Pasar Terakhir", currency_field='currency_id') + harga_update_at = fields.Datetime(string="Harga Diperbarui") + currency_id = fields.Many2one( + 'res.currency', string="Mata Uang", default=lambda self: self.env.ref('base.IDR', raise_if_not_found=False) or self.env.company.currency_id) # o2m convenience menampilkan semua transaksi saham ini transaksi_ids = fields.One2many('saat.transaksi', 'saham_id', string="Transaksi") @@ -20,3 +32,57 @@ class SaatSaham(models.Model): _sql_constraints = [ ('saham_uniq', 'unique(name)', "Kode saham sudah terdaftar."), ] + + # ---- Live price fetch (dipakai cron) ---- + @api.model + def _yahoo_price_idr(self, ticker): + """Ambil harga pasar terakhir dari Yahoo Finance utk saham IDX (.JK).""" + url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}.JK?range=1d&interval=1d" + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)'}) + try: + d = json.load(urllib.request.urlopen(req, timeout=15)) + meta = d['chart']['result'][0]['meta'] + return meta.get('regularMarketPrice') + except Exception as e: + _logger.warning("Yahoo fetch gagal utk %s: %s", ticker, e) + return None + + @api.model + def _in_trading_window(self): + """Jam bursa IDX (Asia/Jakarta/WIB): sesi 1 09.00-12.00, sesi 2 13.30-15.50. + Dengan toleransi ambil 08.45 s/d 16.00 WIB. Gunakan timezone Asia/Jakarta eksplisit + (container biasanya UTC).""" + try: + from zoneinfo import ZoneInfo + now = datetime.now(ZoneInfo('Asia/Jakarta')) + except Exception: + import pytz + now = datetime.now(pytz.timezone('Asia/Jakarta')) + hm = now.hour * 60 + now.minute + return 8 * 60 + 45 <= hm <= 16 * 60 # 08:45 - 16:00 WIB + + @api.model + def _cron_fetch_live_prices(self): + """Cron: ambil harga live semua saham aktif, update bila berubah. + Hanya saat jam bursa. Idempoten & aman (tidak sentuh transaksi/saham).""" + if not self._in_trading_window(): + _logger.info("Di luar jam bursa, lewati fetch harga.") + return False + + sahams = self.search([('active', '=', True)]) + now = fields.Datetime.now() + updated = 0 + for s in sahams: + price = self._yahoo_price_idr(s.name.strip()) + if price is None: + continue + changed = False + if abs((price or 0) - (s.harga_terakhir or 0)) > 1e-6: + changed = True + if changed: + s.write({'harga_terakhir': price, 'harga_update_at': now}) + # sinkronkan ke portofolio terkait utk hitung equity + self.env['saat.portofolio'].search([('saham_id', '=', s.id)]).write({'harga_sekarang': price}) + updated += 1 + _logger.info("Fetch harga selesai: %d saham di-perbarui (dari %d)", updated, len(sahams)) + return updated