Aplikasi catat jual-beli saham: Odoo 17 + Postgres 16 + addon saat_invest
- docker-compose.yml: odoo:17.0 (port 8069) + postgres:16 (internal, no publish) - Addon saat_invest: model saat.saham / saat.transaksi / saat.portofolio - P/L computed (realized FIFO, unrealized dari harga_sekarang) - action_recalculate() endpoint untuk Flutter - seed 5 saham IDX (BBRI, TLKM, BBCA, ASII, PTBA) - JSON-RPC end-to-end terverifikasi: create beli/jual -> recalculate -> portfolio (avg_cost 4525, realized 65000, unrealized 220000, nilai 3840000) - PLAN.md: rancangan disetujui Bozz - Login Odoo admin/admin (ganti password master di .env, jangan commit)
This commit is contained in:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Rahasia — jangan pernah commit
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Terraform / backup dll
|
||||||
|
*.bak
|
||||||
|
*.log
|
||||||
40
PLAN.md
Normal file
40
PLAN.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Rancangan Addon `saat_invest` — Catat Jual/Beli Saham
|
||||||
|
|
||||||
|
**Status:** Disetujui oleh Bozz (2026-09-02)
|
||||||
|
**Backend:** Odoo 17 Community
|
||||||
|
|
||||||
|
## Keputusan (dari Bozz)
|
||||||
|
1. Hitung P/L **otomatis (computed)** di Odoo dari data transaksi.
|
||||||
|
2. **Seed 5 saham contoh** IDX saat install (BBRI, TLKM, BBCA, +2).
|
||||||
|
3. **Endpoint JSON-RPC khusus** untuk Flutter (method Python, dipanggil via `execute_kw`).
|
||||||
|
|
||||||
|
## Model
|
||||||
|
- `saat.saham` — master saham (ticker `name`, `perusahaan`, `sector`)
|
||||||
|
- `saat.transaksi` — catat beli/jual (tanggal, tipe buy/sell, saham, volume, harga, fee, catatan)
|
||||||
|
- `saat.portofolio` — ringkasan per saham (jumlah lembar, avg_cost, investasi, realized/unrealized P/L — semua computed dari transaksi)
|
||||||
|
|
||||||
|
## Relasi & perhitungan
|
||||||
|
- Total lembar = Σ(buy.volume) − Σ(sell.volume) per saham
|
||||||
|
- **Realized P/L** = saat sell: (harga_jual − avg_cost) × volume − fee
|
||||||
|
- **Unrealized P/L** = (harga_sekarang − avg_cost) × sisa lembar
|
||||||
|
- Avg cost = weighted average (total beli ÷ jumlah lembar beli)
|
||||||
|
|
||||||
|
## API (untuk Flutter, via execute_kw JSON-RPC)
|
||||||
|
- `saat.portofolio.search_read` — daftar posisi
|
||||||
|
- `saat.transaksi.create` / `search_read` — catat & lihat transaksi
|
||||||
|
- Method khusus: `action_calculate_portfolio()` pada saat.portofolio akselerasi agregasi
|
||||||
|
|
||||||
|
## Struktur file
|
||||||
|
```
|
||||||
|
addons/saat_invest/
|
||||||
|
├── __manifest__.py
|
||||||
|
├── __init__.py
|
||||||
|
├── models/{__init__,saham,transaksi,portofolio}.py
|
||||||
|
├── security/ir.model.access.csv
|
||||||
|
├── data/saat.demo.saham.csv (seed)
|
||||||
|
└── views/{saham,transaksi,portofolio}_views.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keamanan
|
||||||
|
- Model internal (group `Sales`? no → buat group "User Investasi" via access CSV)
|
||||||
|
- Record rule: user hanya akses data miliknya (default res.users via owner field opsional; v1 sederhana → semua internal user bisa CRUD)
|
||||||
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Aplikasi Catat Jual-Beli Saham — Odoo 17 + Flutter
|
||||||
|
|
||||||
|
Backend Odoo ERP (Community 17) untuk mencatat pembelian & penjualan saham,
|
||||||
|
diakses dari aplikasi **Flutter** (Android).
|
||||||
|
|
||||||
|
## Arsitektur
|
||||||
|
```
|
||||||
|
Flutter app (Android) ──JSON-RPC──► Odoo 17 (Python)
|
||||||
|
│ custom addon: saat_invest
|
||||||
|
▼
|
||||||
|
PostgreSQL 16
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Layer | Teknologi | Port |
|
||||||
|
|---|---|---|
|
||||||
|
| Backend | Odoo 17 Community (`odoo:17.0`) | `8069` (host) |
|
||||||
|
| Database | PostgreSQL 16 (`postgres:16`) | internal (5432, tdk di-publish) |
|
||||||
|
| Mobile | Flutter (Android) | — |
|
||||||
|
|
||||||
|
## Struktur proyek
|
||||||
|
```
|
||||||
|
saat-app/
|
||||||
|
├── docker-compose.yml # Odoo 17 + PostgreSQL 16
|
||||||
|
├── .env # RAHASIA: password (jangan commit)
|
||||||
|
├── addons/ # custom addon buatan sendiri (saat_invest)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cara menjalankan
|
||||||
|
```bash
|
||||||
|
cd /home/ardhi/Application/saat-app
|
||||||
|
docker compose config # validasi
|
||||||
|
docker compose up -d # start
|
||||||
|
```
|
||||||
|
|
||||||
|
Akses UI: http://localhost:8069 — login `admin` / `admin`
|
||||||
|
(`ADMIN_PASSWORD` untuk Database Management di `.env`).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- ✅ Odoo 17 container jalan (port 8069), DB PostgreSQL 16 healthy
|
||||||
|
- ✅ JSON-RPC aktif (uid `admin` verified) — endpoint yang akan dipakai Flutter
|
||||||
|
- ✅ **Addon `saat_invest` terinstal** (model saham, transaksi, portofolio + seed 5 saham IDX)
|
||||||
|
- ✅ End-to-end teruji: buat beli/jual → recalculate → hitung avg cost & realized/unrealized P/L
|
||||||
|
- ⏳ Aplikasi Flutter (login, catat jual/beli, portfolio, P/L)
|
||||||
|
|
||||||
|
## Install / upgrade addon saat_invest
|
||||||
|
```bash
|
||||||
|
cd /home/ardhi/Application/saat-app
|
||||||
|
set -a && source .env && set +a
|
||||||
|
# install pertama kali
|
||||||
|
docker exec -e PASSWORD="$POSTGRES_PASSWORD" saat-odoo odoo \
|
||||||
|
-d "$POSTGRES_DB" --db_host=db --db_user="$POSTGRES_USER" \
|
||||||
|
--db_password="$POSTGRES_PASSWORD" --no-http --stop-after-init -i saat_invest
|
||||||
|
# upgrade setelah ubah kode
|
||||||
|
docker exec -e PASSWORD="$POSTGRES_PASSWORD" saat-odoo odoo \
|
||||||
|
-d "$POSTGRES_DB" --db_host=db --db_user="$POSTGRES_USER" \
|
||||||
|
--db_password="$POSTGRES_PASSWORD" --no-http --stop-after-init -u saat_invest
|
||||||
|
docker restart saat-odoo
|
||||||
|
```
|
||||||
|
|
||||||
|
> Catatan Odoo: `<list>` view di Odoo 17 kadang bermasalah, pakai `<tree>`
|
||||||
|
> (kompatibel lintas versi) seperti yang sudah dilakukan.
|
||||||
|
|
||||||
|
## API untuk Flutter (via JSON-RPC `execute_kw`)
|
||||||
|
Semua melalui `POST /jsonrpc` dengan `{service:'object', method:'execute_kw'}`.
|
||||||
|
Login: `common.authenticate(db, user, pass)` → `uid`.
|
||||||
|
- List saham: `saat.saham search_read [domain] {fields}`
|
||||||
|
- Catat transaksi: `saat.transaksi create [{vals}]`
|
||||||
|
- List transaksi: `saat.transaksi search_read [domain] {fields}`
|
||||||
|
- **Rekalkulasi portofolio**: `saat.portofolio action_recalculate [[]]`
|
||||||
|
- Baca portofolio: `saat.portofolio search_read [domain] {fields}`
|
||||||
|
(sisa_lembar, avg_cost, total_beli_biaya, realized_pl, unrealized_pl, nilai_sekarang)
|
||||||
|
|
||||||
|
## Catatan keamanan
|
||||||
|
- `.env` berisi rahasia → **tidak boleh di-commit** (sudah di `.gitignore`)
|
||||||
|
- PostgreSQL tidak di-publish ke host, hanya akses antar-container
|
||||||
|
- Ganti `ADMIN_PASSWD` segera setelah setup awal
|
||||||
2
addons/saat_invest/__init__.py
Normal file
2
addons/saat_invest/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
29
addons/saat_invest/__manifest__.py
Normal file
29
addons/saat_invest/__manifest__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "Catat Jual/Beli Saham (Saat Invest)",
|
||||||
|
'summary': "Aplikasi pencatatan pembelian & penjualan saham dengan perhitungan portofolio & P/L",
|
||||||
|
'description': """
|
||||||
|
Modul untuk mencatat transaksi jual-beli saham, menghitung posisi portofolio
|
||||||
|
(jumlah lembar, average cost, investasi) serta realised/unrealised profit-loss
|
||||||
|
secara otomatis. Dirancang untuk diakses melalui aplikasi mobile Flutter
|
||||||
|
via JSON-RPC bawaan Odoo.
|
||||||
|
""",
|
||||||
|
'version': '1.0.0',
|
||||||
|
'category': 'Account/Finance',
|
||||||
|
'author': "Kos Barokah",
|
||||||
|
'website': "https://kosbarokah.com",
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'sequence': 1,
|
||||||
|
'depends': ['base', 'web'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/saat.demo.saham.xml',
|
||||||
|
'views/saham_views.xml',
|
||||||
|
'views/transaksi_views.xml',
|
||||||
|
'views/portofolio_views.xml',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
35
addons/saat_invest/data/saat.demo.saham.xml
Normal file
35
addons/saat_invest/data/saat.demo.saham.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="saham_bbri" model="saat.saham">
|
||||||
|
<field name="name">BBRI</field>
|
||||||
|
<field name="perusahaan">Bank Rakyat Indonesia Tbk</field>
|
||||||
|
<field name="bursa">IDX</field>
|
||||||
|
<field name="sector">Perbankan</field>
|
||||||
|
</record>
|
||||||
|
<record id="saham_tlkm" model="saat.saham">
|
||||||
|
<field name="name">TLKM</field>
|
||||||
|
<field name="perusahaan">Telkom Indonesia Tbk</field>
|
||||||
|
<field name="bursa">IDX</field>
|
||||||
|
<field name="sector">Telekomunikasi</field>
|
||||||
|
</record>
|
||||||
|
<record id="saham_bbca" model="saat.saham">
|
||||||
|
<field name="name">BBCA</field>
|
||||||
|
<field name="perusahaan">Bank Central Asia Tbk</field>
|
||||||
|
<field name="bursa">IDX</field>
|
||||||
|
<field name="sector">Perbankan</field>
|
||||||
|
</record>
|
||||||
|
<record id="saham_asii" model="saat.saham">
|
||||||
|
<field name="name">ASII</field>
|
||||||
|
<field name="perusahaan">Astra International Tbk</field>
|
||||||
|
<field name="bursa">IDX</field>
|
||||||
|
<field name="sector">Otomotif</field>
|
||||||
|
</record>
|
||||||
|
<record id="saham_ptba" model="saat.saham">
|
||||||
|
<field name="name">PTBA</field>
|
||||||
|
<field name="perusahaan">Bukit Asam Tbk</field>
|
||||||
|
<field name="bursa">IDX</field>
|
||||||
|
<field name="sector">Energi / Batu Bara</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
4
addons/saat_invest/models/__init__.py
Normal file
4
addons/saat_invest/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import saham
|
||||||
|
from . import transaksi
|
||||||
|
from . import portofolio
|
||||||
BIN
addons/saat_invest/models/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
addons/saat_invest/models/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
addons/saat_invest/models/__pycache__/portofolio.cpython-311.pyc
Normal file
BIN
addons/saat_invest/models/__pycache__/portofolio.cpython-311.pyc
Normal file
Binary file not shown.
BIN
addons/saat_invest/models/__pycache__/saham.cpython-311.pyc
Normal file
BIN
addons/saat_invest/models/__pycache__/saham.cpython-311.pyc
Normal file
Binary file not shown.
BIN
addons/saat_invest/models/__pycache__/transaksi.cpython-311.pyc
Normal file
BIN
addons/saat_invest/models/__pycache__/transaksi.cpython-311.pyc
Normal file
Binary file not shown.
98
addons/saat_invest/models/portofolio.py
Normal file
98
addons/saat_invest/models/portofolio.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# -*- 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
|
||||||
22
addons/saat_invest/models/saham.py
Normal file
22
addons/saat_invest/models/saham.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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."),
|
||||||
|
]
|
||||||
51
addons/saat_invest/models/transaksi.py
Normal file
51
addons/saat_invest/models/transaksi.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SaatTransaksi(models.Model):
|
||||||
|
"""Catat setiap pembelian / penjualan saham."""
|
||||||
|
_name = 'saat.transaksi'
|
||||||
|
_description = "Transaksi Jual/Beli Saham"
|
||||||
|
_order = 'tanggal desc, id desc'
|
||||||
|
|
||||||
|
TIPE = [('buy', 'Beli'), ('sell', 'Jual')]
|
||||||
|
|
||||||
|
name = fields.Char(string="Referensi", compute='_compute_name', store=True)
|
||||||
|
tanggal = fields.Date(string="Tanggal", required=True, default=fields.Date.context_today)
|
||||||
|
tipe = fields.Selection(TIPE, string="Tipe", required=True, default='buy')
|
||||||
|
saham_id = fields.Many2one('saat.saham', string="Saham", required=True, ondelete='restrict')
|
||||||
|
volume = fields.Integer(string="Jumlah Lembar", required=True)
|
||||||
|
harga = fields.Monetary(string="Harga/Lembar", currency_field='currency_id', required=True)
|
||||||
|
fee = fields.Monetary(string="Biaya (Komisi/PPN)", currency_field='currency_id', default=0.0)
|
||||||
|
total = fields.Monetary(
|
||||||
|
string="Total Biaya", currency_field='currency_id',
|
||||||
|
compute='_compute_total', store=True)
|
||||||
|
catatan = fields.Text(string="Catatan")
|
||||||
|
user_id = fields.Many2one('res.users', string="Pencatat", default=lambda self: self.env.user)
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency', string="Mata Uang", default=lambda self: self._default_currency())
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _default_currency(self):
|
||||||
|
# IDR
|
||||||
|
return self.env.ref('base.IDR', raise_if_not_found=False) or self.env.company.currency_id
|
||||||
|
|
||||||
|
@api.depends('tanggal', 'tipe', 'saham_id')
|
||||||
|
def _compute_name(self):
|
||||||
|
for r in self:
|
||||||
|
ticker = r.saham_id.name or ''
|
||||||
|
tipe_label = dict(self.TIPE).get(r.tipe, '')
|
||||||
|
r.name = f"{r.tanggal or ''} {tipe_label} {ticker}"
|
||||||
|
|
||||||
|
@api.depends('volume', 'harga', 'fee', 'tipe')
|
||||||
|
def _compute_total(self):
|
||||||
|
for r in self:
|
||||||
|
r.total = (r.volume or 0) * (r.harga or 0) + (r.fee or 0)
|
||||||
|
|
||||||
|
@api.constrains('volume', 'harga')
|
||||||
|
def _check_positive(self):
|
||||||
|
for r in self:
|
||||||
|
if r.volume <= 0:
|
||||||
|
raise models.ValidationError("Volume harus lebih dari 0.")
|
||||||
|
if r.harga < 0:
|
||||||
|
raise models.ValidationError("Harga tidak boleh negatif.")
|
||||||
4
addons/saat_invest/security/ir.model.access.csv
Normal file
4
addons/saat_invest/security/ir.model.access.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_saat_saham_all,saat.saham.all,model_saat_saham,base.group_user,1,1,1,1
|
||||||
|
access_saat_transaksi_all,saat.transaksi.all,model_saat_transaksi,base.group_user,1,1,1,1
|
||||||
|
access_saat_portofolio_all,saat.portofolio.all,model_saat_portofolio,base.group_user,1,1,1,1
|
||||||
|
62
addons/saat_invest/views/portofolio_views.xml
Normal file
62
addons/saat_invest/views/portofolio_views.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_saat_portofolio_list" model="ir.ui.view">
|
||||||
|
<field name="name">saat.portofolio.list</field>
|
||||||
|
<field name="model">saat.portofolio</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Portofolio Saham" create="false" delete="false">
|
||||||
|
<field name="saham_id"/>
|
||||||
|
<field name="sisa_lembar"/>
|
||||||
|
<field name="avg_cost"/>
|
||||||
|
<field name="harga_sekarang"/>
|
||||||
|
<field name="nilai_sekarang"/>
|
||||||
|
<field name="realized_pl" widget="monetary"/>
|
||||||
|
<field name="unrealized_pl" widget="monetary"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_saat_portofolio_form" model="ir.ui.view">
|
||||||
|
<field name="name">saat.portofolio.form</field>
|
||||||
|
<field name="model">saat.portofolio</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Portofolio">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="saham_id" readonly="1"/>
|
||||||
|
<field name="harga_sekarang"/>
|
||||||
|
</group>
|
||||||
|
<group string="Posisi">
|
||||||
|
<group>
|
||||||
|
<field name="total_beli_lembar"/>
|
||||||
|
<field name="total_beli_biaya"/>
|
||||||
|
<field name="total_jual_lembar"/>
|
||||||
|
<field name="total_jual_hasil"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="sisa_lembar"/>
|
||||||
|
<field name="avg_cost"/>
|
||||||
|
<field name="nilai_sekarang"/>
|
||||||
|
<field name="realized_pl"/>
|
||||||
|
<field name="unrealized_pl"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_saat_portofolio" model="ir.actions.act_window">
|
||||||
|
<field name="name">Portofolio</field>
|
||||||
|
<field name="res_model">saat.portofolio</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Klik <b>Rekalkulasi</b> (menu Form) untuk memperbarui posisi dari transaksi.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_saat_portofolio" name="Portofolio" parent="menu_saat_root"
|
||||||
|
action="action_saat_portofolio" sequence="30"/>
|
||||||
|
</odoo>
|
||||||
59
addons/saat_invest/views/saham_views.xml
Normal file
59
addons/saat_invest/views/saham_views.xml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_saat_saham_list" model="ir.ui.view">
|
||||||
|
<field name="name">saat.saham.list</field>
|
||||||
|
<field name="model">saat.saham</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Daftar Saham" create="true">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="perusahaan"/>
|
||||||
|
<field name="bursa"/>
|
||||||
|
<field name="sector"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_saat_saham_form" model="ir.ui.view">
|
||||||
|
<field name="name">saat.saham.form</field>
|
||||||
|
<field name="model">saat.saham</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Saham">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="perusahaan"/>
|
||||||
|
<field name="bursa"/>
|
||||||
|
<field name="sector"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Transaksi">
|
||||||
|
<field name="transaksi_ids">
|
||||||
|
<tree>
|
||||||
|
<field name="tanggal"/>
|
||||||
|
<field name="tipe"/>
|
||||||
|
<field name="volume"/>
|
||||||
|
<field name="harga"/>
|
||||||
|
<field name="total"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_saat_saham" model="ir.actions.act_window">
|
||||||
|
<field name="name">Saham</field>
|
||||||
|
<field name="res_model">saat.saham</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">Daftar saham/emiten yang dicatat.</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_saat_root" name="Investasi Saham" sequence="50"/>
|
||||||
|
<menuitem id="menu_saat_saham" name="Saham" parent="menu_saat_root"
|
||||||
|
action="action_saat_saham" sequence="10"/>
|
||||||
|
</odoo>
|
||||||
55
addons/saat_invest/views/transaksi_views.xml
Normal file
55
addons/saat_invest/views/transaksi_views.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_saat_transaksi_list" model="ir.ui.view">
|
||||||
|
<field name="name">saat.transaksi.list</field>
|
||||||
|
<field name="model">saat.transaksi</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Transaksi Jual/Beli" create="true">
|
||||||
|
<field name="tanggal"/>
|
||||||
|
<field name="tipe"/>
|
||||||
|
<field name="saham_id"/>
|
||||||
|
<field name="volume"/>
|
||||||
|
<field name="harga"/>
|
||||||
|
<field name="fee"/>
|
||||||
|
<field name="total"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_saat_transaksi_form" model="ir.ui.view">
|
||||||
|
<field name="name">saat.transaksi.form</field>
|
||||||
|
<field name="model">saat.transaksi</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Transaksi">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="tanggal"/>
|
||||||
|
<field name="tipe"/>
|
||||||
|
<field name="saham_id"/>
|
||||||
|
<field name="volume"/>
|
||||||
|
<field name="harga"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="fee"/>
|
||||||
|
<field name="total" readonly="1"/>
|
||||||
|
<field name="catatan" colspan="2"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_saat_transaksi" model="ir.actions.act_window">
|
||||||
|
<field name="name">Transaksi</field>
|
||||||
|
<field name="res_model">saat.transaksi</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">Catat pembelian / penjualan saham di sini.</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_saat_transaksi" name="Transaksi" parent="menu_saat_root"
|
||||||
|
action="action_saat_transaksi" sequence="20"/>
|
||||||
|
</odoo>
|
||||||
56
docker-compose.yml
Normal file
56
docker-compose.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Docker Compose untuk Odoo 17 (Community) + PostgreSQL 16
|
||||||
|
# Proyek: Aplikasi catat pembelian/penjualan saham
|
||||||
|
# Lokasi: /home/ardhi/Application/saat-app
|
||||||
|
#
|
||||||
|
# Catatan:
|
||||||
|
# - HANYA container baru, TIDAK menyentuh traefik/gitea/portainer yang sudah ada.
|
||||||
|
# - Postgres port 5432 TIDAK dipublish ke host (hanya akses antar container).
|
||||||
|
# - Odoo ter-expose di host port 8069. Traefik routing opsional belakangan.
|
||||||
|
# - Buat project admin di Odoo langsung via web UI setelah `docker compose up`.
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: saat-odoo-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- saat_odoo_pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- saat-odoo-net
|
||||||
|
|
||||||
|
odoo:
|
||||||
|
image: odoo:17.0
|
||||||
|
container_name: saat-odoo
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8069:8069"
|
||||||
|
environment:
|
||||||
|
- HOST=db
|
||||||
|
- USER=${POSTGRES_USER}
|
||||||
|
- PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWD}
|
||||||
|
volumes:
|
||||||
|
- saat_odoo_filestore:/var/lib/odoo
|
||||||
|
- ./addons:/mnt/extra-addons
|
||||||
|
networks:
|
||||||
|
- saat-odoo-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
saat_odoo_pgdata:
|
||||||
|
saat_odoo_filestore:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
saat-odoo-net:
|
||||||
|
driver: bridge
|
||||||
Reference in New Issue
Block a user