# -*- 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.""" _name = 'saat.saham' _description = "Saham / Emiten" _order = 'name' name = fields.Char(string="Kode Saham (Ticker)", required=True) perusahaan = fields.Char(string="Nama Perusahaan / Emiten") 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") _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