Files
saat-app/addons/saat_invest/models/portofolio.py
2026-10-05 17:17:29 +07:00

112 lines
5.2 KiB
Python

# -*- 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([('state', '=', 'done')], 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),
}
def action_recalc_click(self):
"""Tombol 'Rekalkulasi' untuk UI web: hitung ulang lalu muat ulang view list."""
result = self.env['saat.portofolio'].action_recalculate()
# kembalikan action window portofolio agar view ter-refresh
action = {
'type': 'ir.actions.act_window',
'name': 'Portofolio',
'res_model': 'saat.portofolio',
'view_mode': 'list,form',
'target': 'current',
}
return action
# --- 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