# -*- coding: utf-8 -*- from odoo import api, fields, models class SaatPortofolio(models.Model): """Ringkasan posisi per saham, dihitung dari seluruh transaksi. Bukan stored-view melainkan snapshot yang direkalkulasi dari transaksi (via action_recalculate / API). Satu record per saham yang pernah ditransaksikan. Menyimpan aggregate terakhir + P/L realised/unrealised. """ _name = 'saat.portofolio' _description = "Portofolio Saham" _rec_name = 'saham_id' saham_id = fields.Many2one('saat.saham', string="Saham", required=True, ondelete='cascade') # Posisi agregat total_beli_lembar = fields.Integer(string="Total Beli (lembar)", default=0) total_beli_biaya = fields.Monetary(string="Total Beli (biaya)", default=0.0, currency_field='currency_id') total_jual_lembar = fields.Integer(string="Total Jual (lembar)", default=0) total_jual_hasil = fields.Monetary(string="Total Jual (hasil)", default=0.0, currency_field='currency_id') sisa_lembar = fields.Integer(string="Sisa Lembar", compute='_compute_sisa', store=True) avg_cost = fields.Monetary(string="Avg Cost/Lembar", compute='_compute_avg', store=True, currency_field='currency_id') harga_sekarang = fields.Monetary(string="Harga Sekarang/Lembar", currency_field='currency_id') nilai_sekarang = fields.Monetary( string="Nilai Sekarang", compute='_compute_nilai', store=True, currency_field='currency_id') realized_pl = fields.Monetary( string="Realized P/L", currency_field='currency_id') unrealized_pl = fields.Monetary( string="Unrealized P/L", compute='_compute_pl', store=True, currency_field='currency_id') currency_id = fields.Many2one('res.currency', string="Mata Uang", default=lambda self: self.env.company.currency_id) _sql_constraints = [ ('portofolio_saham_uniq', 'unique(saham_id)', "Sudah ada portofolio untuk saham ini."), ] # --- aggregate helper (dijalankan saat rekalkulasi) --- 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') per_saham = {} for t in trans: a = per_saham.setdefault(t.saham_id.id, { 'saham': t.saham_id, 'beli_lb': 0, 'beli_by': 0.0, 'jual_lb': 0, 'jual_hl': 0.0, 'moves': [], 'tot_cost': 0.0, }) a['moves'].append(t) if t.tipe == 'buy': a['beli_lb'] += t.volume a['beli_by'] += t.total else: a['jual_lb'] += t.volume a['jual_hl'] += t.total # total sudah termasuk fee (keluar) portfolios = self.env['saat.portofolio'] for sid, a in per_saham.items(): # FIFO realized P/L sederhana: realized saat sell = jual - (avg cost beli * volume terjual) # avg cost beli per lembar: avg_beli = (a['beli_by'] / a['beli_lb']) if a['beli_lb'] else 0.0 lembar_terjual = min(a['jual_lb'], a['beli_lb']) realized = (a['jual_hl'] - avg_beli * lembar_terjual) if lembar_terjual else 0.0 sisa = max(a['beli_lb'] - a['jual_lb'], 0) portfolios.create({ 'saham_id': sid, 'total_beli_lembar': a['beli_lb'], 'total_beli_biaya': a['beli_by'], 'total_jual_lembar': a['jual_lb'], 'total_jual_hasil': a['jual_hl'], 'harga_sekarang': a['saham']._get_last_harga() if hasattr(a['saham'], '_get_last_harga') else 0.0, 'realized_pl': realized, }) return { 'portofolio_ids': portfolios.ids, 'count': len(portfolios), } # --- computed store fields --- @api.depends('total_beli_lembar', 'total_jual_lembar') def _compute_sisa(self): for r in self: r.sisa_lembar = max((r.total_beli_lembar or 0) - (r.total_jual_lembar or 0), 0) @api.depends('total_beli_biaya', 'total_beli_lembar') def _compute_avg(self): for r in self: r.avg_cost = (r.total_beli_biaya / r.total_beli_lembar) if r.total_beli_lembar else 0.0 @api.depends('sisa_lembar', 'harga_sekarang') def _compute_nilai(self): for r in self: r.nilai_sekarang = (r.sisa_lembar or 0) * (r.harga_sekarang or 0.0) @api.depends('harga_sekarang', 'avg_cost', 'sisa_lembar') def _compute_pl(self): for r in self: r.unrealized_pl = ((r.harga_sekarang or 0.0) - (r.avg_cost or 0.0)) * (r.sisa_lembar or 0) # realized dihitung di action_recalculate (dibawa via currency) — simpan sisa