from dash import Dash, html, dcc, callback, Input, Output, State
import dash
import pandas as pd
from datetime import datetime, timedelta
import json
import random
from supabase import create_client, Client
from dotenv import load_dotenv
import os
import glob
from pathlib import Path
import os, smtplib, ssl
from email.message import EmailMessage
BASE_DIR = Path(__file__).resolve().parent
ENV_PATH = BASE_DIR / ".env"
load_dotenv(dotenv_path=ENV_PATH, override=True)
def _load_google_credentials_from_local_file():
"""Attempt to read local client_secret JSON files often downloaded from Google Console.
Looks for files named `client_secret*.json` or `client_secret.json` and extracts
client_id and client_secret if present. Returns tuple (client_id, client_secret).
"""
files = glob.glob('client_secret*.json') + glob.glob('client_secret.json')
for path in files:
try:
with open(path, 'r', encoding='utf-8') as f:
payload = json.load(f)
# file may contain 'installed' or 'web'
for key in ('installed', 'web'):
if key in payload:
info = payload[key]
cid = info.get('client_id')
secret = info.get('client_secret')
if cid and secret:
print(f"đ Loaded Google credentials from {path} (local file)")
return cid, secret
# fallback try top-level keys
if 'client_id' in payload and 'client_secret' in payload:
return payload['client_id'], payload['client_secret']
except Exception:
continue
return None, None
from flask import Flask, redirect, url_for, session
from supabase import create_client, Client
import os
# ===== INISIALISASI URUTAN YANG BENAR =====
# 1. Inisialisasi app dulu
app = Dash(__name__, suppress_callback_exceptions=True)
app.title = "SIBAL - Sistem Informasi Bimbingan Akuntansi Lanjutan"
# 2. Setup server
server = app.server
# 3. Setup SECRET_KEY
import secrets
SECRET_KEY = os.getenv('SECRET_KEY', 'sibal-secure-' + secrets.token_hex(32))
server.config['SECRET_KEY'] = SECRET_KEY
# Ensure session cookie settings are explicit for local development
# Use Lax so top-level navigations (OAuth redirects) will include the cookie
server.config['SESSION_COOKIE_SAMESITE'] = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
# For local development over HTTP, do not force Secure; in production set to True
server.config['SESSION_COOKIE_SECURE'] = False
server.config['SESSION_COOKIE_HTTPONLY'] = True
from datetime import timedelta as _td
server.config['PERMANENT_SESSION_LIFETIME'] = _td(days=7)
# 4. Setup Supabase client
supabase_url = "https://shltrwcweexbdcuogscs.supabase.co"
supabase_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNobHRyd2N3ZWV4YmRjdW9nc2NzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQwNjk4MjUsImV4cCI6MjA3OTY0NTgyNX0.tVwzjiqORJ0dAZhW_f0PneVqJyummvmPOi-WFC5Wd1I"
supabase_client = create_client(supabase_url, supabase_key)
# ===== SIMPLE AUTH SYSTEM =====
import requests
import secrets
import hashlib
import random
class SimpleUser:
def __init__(self, user_data=None):
if user_data:
self.id = user_data.get('id', '')
self.email = user_data.get('email', '')
self.name = user_data.get('name', '')
self.username = user_data.get('username', '')
self.is_authenticated = True
else:
self.id = ''
self.email = ''
self.name = ''
self.username = ''
self.is_authenticated = False
def get_id(self):
return str(self.id)
# Global current_user
current_user = SimpleUser()
# Simple user storage
users_db = {}
# Google OAuth config
# Gunakan environment variables agar tidak menyimpan secrets di repo
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET', None)
GOOGLE_REDIRECT_URI = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8051/auth/callback')
# ===== SIMPLE AUTH FUNCTIONS =====
def login_user(user, remember=False):
"""Gantikan fungsi login_user dari Flask-Login"""
global current_user
current_user = SimpleUser(user)
session['user_id'] = user['id']
session['user_email'] = user['email']
# Set active user in data layer so data dipisah per akun
try:
sibal_data.set_active_user(user.get('id'))
except Exception:
pass
# make the session permanent so it survives short redirects
try:
session.permanent = True
except Exception:
pass
print(f"â
User logged in: {user['email']}")
def authenticate_user(identifier, password):
"""Gantikan auth_system.authenticate_user"""
user = None
# Cari user by email atau username
for user_id, user_data in users_db.items():
if user_data['email'] == identifier or user_data['username'] == identifier:
user = user_data
break
if not user:
return None, "User tidak ditemukan"
# Verify password
password_hash = hashlib.sha256(password.encode()).hexdigest()
if user.get('password_hash') != password_hash:
return None, "Password salah"
return user, "Login berhasil"
# Update authentication functions untuk menggunakan tabel users yang baru
def create_user(username, email, password):
"""Create user dan simpan ke Supabase"""
try:
# Cek apakah email sudah terdaftar di tabel users
existing_users = sibal_data._get_table_data('users')
existing_email = any(user['email'] == email for user in existing_users)
existing_username = any(user['username'] == username for user in existing_users)
if existing_email:
return None, "Email sudah terdaftar"
if existing_username:
return None, "Username sudah digunakan"
# Create new user di Supabase
user_data = {
'email': email,
'username': username,
'name': username,
'auth_provider': 'local'
}
saved_user = sibal_data._insert_table_data('users', user_data)
if saved_user:
user_data['id'] = saved_user['id']
# NOTE: do NOT initialize template data automatically.
# New users should start with EMPTY data per requested behavior.
print(f"âšī¸ New user created with id {user_data['id']} (no template initialization)")
return user_data, "User created successfully"
else:
return None, "Gagal membuat user"
except Exception as e:
print(f"Error creating user: {e}")
return None, "Error sistem"
def authenticate_user(identifier, password):
"""Authenticate user dari Supabase"""
try:
# Untuk sekarang, kita skip password verification
# karena tabel users tidak ada password_hash
users_data = sibal_data._get_table_data('users')
user_data = None
for user in users_data:
if user['email'] == identifier or user['username'] == identifier:
user_data = user
break
if not user_data:
return None, "User tidak ditemukan"
# Untuk development, bypass password check
# Karena tabel users tidak memiliki password_hash
return user_data, "Login berhasil (development mode)"
except Exception as e:
print(f"Error authenticating user: {e}")
return None, "Error sistem"
# Simple user storage - HANYA SIMPAN DI MEMORY
users_db = {}
# Google OAuth config (didefinisikan sebelumnya menggunakan environment variables)
# ========== BEAUTIFUL COLOR PALETTE ==========
COLORS = {
'primary': '#473573',
'primary_light': '#5A4A8A',
'primary_dark': '#3A2B5C',
'secondary': '#53619E',
'secondary_light': '#6A77B0',
'secondary_dark': '#454D80',
'accent_teal': '#88BAC3',
'accent_teal_light': '#9DC8D0',
'accent_mint': '#D6F3EE',
'accent_mint_light': '#E6F7F4',
'accent_lavender': '#A78BFA',
'accent_coral': '#FF6B6B',
'white': '#FFFFFF',
'gray_50': '#F8FAFC',
'gray_100': '#F1F5F9',
'gray_200': '#E2E8F0',
'gray_300': '#CBD5E1',
'gray_400': '#94A3B8',
'gray_500': '#64748B',
'gray_600': '#475569',
'gray_700': '#334155',
'gray_800': '#1E293B',
'gray_900': '#0F172A',
'success': '#10B981',
'success_light': '#34D399',
'warning': '#F59E0B',
'warning_light': '#FBBF24',
'error': '#EF4444',
'error_light': '#F87171',
'info': '#3B82F6',
'info_light': '#60A5FA'
}
# ========== SISTEM KODE AKUN ==========
KODE_AKUN = {
'kas': {'kode': '1-110', 'nama': 'Kas'},
'persediaan': {'kode': '1-120', 'nama': 'Persediaan Barang Dagang'},
'perlengkapan': {'kode': '1-130', 'nama': 'Perlengkapan'},
'peralatan': {'kode': '1-210', 'nama': 'Peralatan'},
'tanah': {'kode': '1-220', 'nama': 'Tanah'},
'bangunan_gazebo': {'kode': '1-230', 'nama': 'Bangunan'},
'akumulasi_penyusutan_bangunan': {'kode': '1-231', 'nama': 'Akumulasi Depresiasi Bangunan'},
'kendaraan': {'kode': '1-240', 'nama': 'Kendaraan'},
'akumulasi_penyusutan_kendaraan': {'kode': '1-241', 'nama': 'Akumulasi Depresiasi Kendaraan'},
'akumulasi_penyusutan_peralatan': {'kode': '1-242', 'nama': 'Akumulasi Depresiasi Peralatan'},
'utang': {'kode': '2-110', 'nama': 'Utang Dagang'},
'utang_gaji': {'kode': '2-120', 'nama': 'Utang Gaji'},
'modal': {'kode': '3-110', 'nama': 'Modal'},
'pendapatan': {'kode': '4-110', 'nama': 'Penjualan'},
'pendapatan_tiket': {'kode': '4-120', 'nama': 'Pendapatan Tiket'},
'hpp': {'kode': '5-110', 'nama': 'Harga Pokok Produksi'},
'beban_gaji': {'kode': '6-110', 'nama': 'Beban Gaji'},
'beban_listrik': {'kode': '6-115', 'nama': 'Beban Listrik'},
'beban_penyusutan_bangunan': {'kode': '6-120', 'nama': 'Beban Depresiasi Bangunan'},
'beban_penyusutan_kendaraan': {'kode': '6-121', 'nama': 'Beban Depresiasi Kendaraan'},
'beban_penyusutan_peralatan': {'kode': '6-122', 'nama': 'Beban Depresiasi Peralatan'},
'beban_lainnya': {'kode': '6-130', 'nama': 'Beban Lainnya'}
}
def login_layout():
return html.Div([
html.Div([
html.Div([
html.H1("đ Login ke SIBAL", style={
'textAlign': 'center',
'color': COLORS['primary'],
'marginBottom': '10px'
}),
html.P("Sistem Informasi Bimbingan Akuntansi Lanjutan",
style={'textAlign': 'center', 'color': COLORS['gray_600'], 'marginBottom': '40px'}),
# TOMBOL GOOGLE
html.Div([
html.A(
html.Button(
[
html.Img(
src="https://developers.google.com/identity/images/g-logo.png",
style={
'width': '20px',
'height': '20px',
'marginRight': '12px',
'verticalAlign': 'middle'
}
),
"Login dengan Google"
],
style={
'width': '100%',
'padding': '15px 20px',
'fontSize': '16px',
'fontWeight': '500',
'backgroundColor': '#4285F4',
'color': 'white',
'border': 'none',
'borderRadius': '8px',
'cursor': 'pointer',
'display': 'flex',
'alignItems': 'center',
'justifyContent': 'center',
'transition': 'all 0.3s ease'
},
id='btn-google-login' # â ID UNTUK CALLBACK
),
href="/auth/login",
style={'textDecoration': 'none'}
)
], style={'marginBottom': '30px'}),
html.Div([
html.Hr(style={'margin': '20px 0'}),
html.P("Atau login dengan akun yang sudah dibuat", style={
'textAlign': 'center',
'color': COLORS['gray_500'],
'marginBottom': '20px'
})
]),
# FORM LOGIN MANUAL
html.Div([
html.Label("Username atau Email", className="form-label"),
dcc.Input(
id='login-identifier',
type='text',
placeholder='Username atau email Anda...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Password", className="form-label"),
dcc.Input(
id='login-password',
type='password',
placeholder='Password Anda...',
className="form-input"
)
], className="form-group"),
# TOMBOL LOGIN MANUAL
html.Button(
"đ Login",
id='btn-login', # â ID UNTUK CALLBACK
className="btn btn-primary",
style={'width': '100%', 'marginBottom': '15px'}
),
html.Div(id='login-alert'),
html.Hr(style={'margin': '30px 0'}),
html.Div([
html.P("Belum punya akun? ", style={'display': 'inline'}),
dcc.Link(
"Daftar di sini",
href="/signup",
style={
'color': COLORS['primary'],
'fontWeight': 'bold',
'textDecoration': 'none'
}
)
], style={'textAlign': 'center'})
], style={'padding': '50px'})
], style={
'maxWidth': '450px',
'margin': '80px auto',
'backgroundColor': 'white',
'borderRadius': '12px',
'boxShadow': '0 10px 25px rgba(0,0,0,0.1)',
'border': '1px solid #e0e0e0'
})
], style={
'backgroundColor': COLORS['gray_50'],
'minHeight': '100vh',
'padding': '20px',
'fontFamily': 'Arial, sans-serif'
})
def signup_layout():
return html.Div([
html.Div([
html.Div([
html.H1("đ Daftar SIBAL", style={
'textAlign': 'center',
'color': COLORS['primary'],
'marginBottom': '10px'
}),
html.P("Bergabung dengan sistem akuntansi kami",
style={'textAlign': 'center', 'color': COLORS['gray_600'], 'marginBottom': '40px'}),
# TOMBOL GOOGLE SIGNUP
html.Div([
html.A(
html.Button(
[
html.Img(
src="https://developers.google.com/identity/images/g-logo.png",
style={
'width': '20px',
'height': '20px',
'marginRight': '12px',
'verticalAlign': 'middle'
}
),
"Daftar dengan Google"
],
style={
'width': '100%',
'padding': '15px 20px',
'fontSize': '16px',
'fontWeight': '500',
'backgroundColor': '#34A853',
'color': 'white',
'border': 'none',
'borderRadius': '8px',
'cursor': 'pointer',
'display': 'flex',
'alignItems': 'center',
'justifyContent': 'center'
},
id='btn-google-signup' # â ID UNTUK CALLBACK
),
href="/auth/login",
style={'textDecoration': 'none'}
)
], style={'marginBottom': '30px'}),
html.Div([
html.Hr(style={'margin': '20px 0'}),
html.P("Atau daftar dengan email", style={
'textAlign': 'center',
'color': COLORS['gray_500'],
'marginBottom': '20px'
})
]),
# FORM SIGNUP MANUAL
html.Div([
html.Label("Username", className="form-label"),
dcc.Input(
id='signup-username',
type='text',
placeholder='Pilih username...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Email", className="form-label"),
dcc.Input(
id='signup-email',
type='email',
placeholder='Email Anda...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Password", className="form-label"),
dcc.Input(
id='signup-password',
type='password',
placeholder='Password minimal 6 karakter...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Konfirmasi Password", className="form-label"),
dcc.Input(
id='signup-confirm-password',
type='password',
placeholder='Ulangi password...',
className="form-input"
)
], className="form-group"),
# TOMBOL SIGNUP MANUAL
html.Button(
"â
Daftar dengan Email",
id='btn-signup', # â ID UNTUK CALLBACK
className="btn btn-success",
style={'width': '100%', 'marginBottom': '15px'}
),
html.Div(id='signup-alert'),
html.Hr(style={'margin': '30px 0'}),
html.Div([
html.P("Sudah punya akun? ", style={'display': 'inline'}),
dcc.Link(
"Login di sini",
href="/login",
style={
'color': COLORS['primary'],
'fontWeight': 'bold',
'textDecoration': 'none'
}
)
], style={'textAlign': 'center'})
], style={'padding': '50px'})
], style={
'maxWidth': '450px',
'margin': '80px auto',
'backgroundColor': 'white',
'borderRadius': '12px',
'boxShadow': '0 10px 25px rgba(0,0,0,0.1)',
'border': '1px solid #e0e0e0'
})
], style={
'backgroundColor': COLORS['gray_50'],
'minHeight': '100vh',
'padding': '20px',
'fontFamily': 'Arial, sans-serif'
})
def complete_profile_layout():
"""Halaman untuk buat username dan password setelah Google OAuth"""
oauth_user = session.get('oauth_user')
if not oauth_user:
return html.Div([
html.Div([
html.H3("â Session Expired", style={'textAlign': 'center', 'color': COLORS['error']}),
html.P("Silakan login kembali.", style={'textAlign': 'center'}),
dcc.Link(
"Kembali ke Login",
href="/login",
style={'display': 'block', 'textAlign': 'center', 'marginTop': '20px'}
)
], style={'padding': '50px'})
], style={
'maxWidth': '500px',
'margin': '100px auto',
'backgroundColor': 'white',
'borderRadius': '12px',
'boxShadow': '0 10px 25px rgba(0,0,0,0.1)'
})
return html.Div([
html.Div([
html.Div([
html.H1("đ Selamat Datang!", style={'textAlign': 'center', 'color': COLORS['primary']}),
html.P(f"Halo {oauth_user['name']}!",
style={'textAlign': 'center', 'color': COLORS['gray_600'], 'fontSize': '1.1rem'}),
html.P("Lengkapi profil Anda untuk mulai menggunakan SIBAL",
style={'textAlign': 'center', 'color': COLORS['gray_500']}),
html.Hr(style={'margin': '20px 0'}),
html.Div([
html.Label("Username", className="form-label"),
dcc.Input(
id='complete-username',
type='text',
placeholder='Pilih username...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Password", className="form-label"),
dcc.Input(
id='complete-password',
type='password',
placeholder='Buat password minimal 6 karakter...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Konfirmasi Password", className="form-label"),
dcc.Input(
id='complete-confirm-password',
type='password',
placeholder='Ulangi password...',
className="form-input"
)
], className="form-group"),
html.Button(
"â
Simpan Profil & Masuk",
id='btn-complete-profile',
className="btn btn-primary",
style={'width': '100%', 'marginTop': '20px', 'padding': '15px'}
),
html.Div(id='complete-profile-alert', style={'marginTop': '20px'})
], style={'padding': '40px'})
], style={
'maxWidth': '500px',
'margin': '50px auto',
'backgroundColor': 'white',
'borderRadius': '12px',
'boxShadow': '0 10px 25px rgba(0,0,0,0.1)',
'border': '1px solid #e0e0e0'
})
], style={
'backgroundColor': COLORS['gray_50'],
'minHeight': '100vh',
'padding': '20px'
})
def protected_layout():
"""Layout yang membutuhkan login"""
if current_user.is_authenticated:
return dashboard_layout()
else:
return login_layout()
def aset_tetap_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-building")
], className="card-icon"),
html.H1("Manajemen Aset Tetap", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.H3("Input Aset Tetap", className="form-section-title"),
html.Div([
html.Label("Jenis Aset", className="form-label"),
dcc.Dropdown(
id='jenis-aset',
options=[
{'label': 'Tanah', 'value': 'tanah'},
{'label': 'Bangunan Gazebo', 'value': 'bangunan_gazebo'},
{'label': 'Kendaraan', 'value': 'kendaraan'},
{'label': 'Peralatan', 'value': 'peralatan'}
],
placeholder='Pilih Jenis Aset',
className="form-control"
)
], className="form-group"),
html.Div([
html.Label("Nilai Perolehan (Rp)", className="form-label"),
dcc.Input(
id='nilai-aset',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Tanggal Perolehan", className="form-label"),
dcc.DatePickerSingle(
id='tanggal-perolehan',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group"),
html.Div([
html.Label("Masa Manfaat (Tahun)", className="form-label"),
dcc.Input(
id='masa-manfaat',
type='number',
placeholder='0',
min=1,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Metode Penyusutan", className="form-label"),
dcc.Dropdown(
id='metode-penyusutan',
options=[
{'label': 'Garis Lurus', 'value': 'garis_lurus'},
{'label': 'Saldo Menurun', 'value': 'saldo_menurun'}
],
value='garis_lurus',
className="form-control"
)
], className="form-group"),
html.Div([
html.Label("Nilai Residu (Rp) - Optional", className="form-label"),
dcc.Input(
id='nilai-residu',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Button(
"đž Simpan Aset Tetap",
id='btn-simpan-aset',
className="btn btn-primary"
)
], className="form-container compact-form"),
html.Div([
html.H3("Daftar Aset Tetap", className="form-section-title"),
html.Div(id='daftar-aset-tetap', className="table-container"),
html.Hr(),
html.Button(
"đ Hitung Penyusutan",
id='btn-hitung-penyusutan',
className="btn btn-success"
)
], className="data-container"),
], className="form-row"),
], className="glass-card"),
html.Div([
html.H3("Kalkulator Penyusutan", className="card-title"),
html.Div(id='kalkulator-penyusutan', className="table-container")
], className="glass-card"),
], className="main-container")
app.index_string = f'''
{{%metas%}}
{{%title%}}
{{%favicon%}}
{{%css%}}
{{%app_entry%}}
'''
class SIBALData:
def __init__(self):
# Setup Supabase client
self.url = "https://shltrwcweexbdcuogscs.supabase.co"
self.key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNobHRyd2N3ZWV4YmRjdW9nc2NzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQwNjk4MjUsImV4cCI6MjA3OTY0NTgyNX0.tVwzjiqORJ0dAZhW_f0PneVqJyummvmPOi-WFC5Wd1I"
try:
self.client: Client = create_client(self.url, self.key)
print("â
Supabase client connected successfully")
except Exception as e:
print(f"â Supabase client initialization failed: {e}")
self.client = None
# Inisialisasi struktur data
self.init_data_structures()
# Load data dari Supabase
self.load_all_data()
def init_data_structures(self):
"""Initialize semua struktur data"""
# Data transaksi (semua pengguna)
self._all_transaksi_pemasukan = []
self._all_transaksi_pengeluaran = []
self._all_jurnal_umum = []
self._all_jurnal_penyesuaian = []
self._all_kartu_persediaan = []
# Active user id (digunakan untuk memfilter data per-akun)
self.active_user_id = None
# Data master (untuk sementara di memory)
self.master_persediaan = [
{'kode': 'IK001', 'nama': 'Ikan Bawal Segar', 'satuan': 'kg', 'harga_beli': 25000, 'harga_jual': 35000}
]
self.suppliers = []
# Per-user storage untuk suppliers dan buku_besar_pembantu
self._all_suppliers = {}
self._all_buku_besar_pembantu = {}
# Data aset tetap
self.aset_tetap = {
'tanah': {'nilai_awal': 0, 'penyusutan': 0, 'masa_manfaat': 0, 'tahun_pembelian': datetime.now().year},
'bangunan_gazebo': {'nilai_awal': 0, 'penyusutan': 0, 'masa_manfaat': 10, 'tahun_pembelian': datetime.now().year},
'kendaraan': {'nilai_awal': 0, 'penyusutan': 0, 'masa_manfaat': 5, 'tahun_pembelian': datetime.now().year},
'peralatan': {'nilai_awal': 0, 'penyusutan': 0, 'masa_manfaat': 3, 'tahun_pembelian': datetime.now().year}
}
# Daftar akun untuk dropdown dengan kode
self.daftar_akun = [
{'label': f"{KODE_AKUN['kas']['kode']} - {KODE_AKUN['kas']['nama']}", 'value': 'kas'},
{'label': f"{KODE_AKUN['persediaan']['kode']} - {KODE_AKUN['persediaan']['nama']}", 'value': 'persediaan'},
{'label': f"{KODE_AKUN['perlengkapan']['kode']} - {KODE_AKUN['perlengkapan']['nama']}", 'value': 'perlengkapan'},
{'label': f"{KODE_AKUN['utang']['kode']} - {KODE_AKUN['utang']['nama']}", 'value': 'utang'},
{'label': f"{KODE_AKUN['modal']['kode']} - {KODE_AKUN['modal']['nama']}", 'value': 'modal'},
{'label': f"{KODE_AKUN['pendapatan']['kode']} - {KODE_AKUN['pendapatan']['nama']}", 'value': 'pendapatan'},
{'label': f"{KODE_AKUN['pendapatan_tiket']['kode']} - {KODE_AKUN['pendapatan_tiket']['nama']}", 'value': 'pendapatan_tiket'},
{'label': f"{KODE_AKUN['hpp']['kode']} - {KODE_AKUN['hpp']['nama']}", 'value': 'hpp'},
{'label': f"{KODE_AKUN['tanah']['kode']} - {KODE_AKUN['tanah']['nama']}", 'value': 'tanah'},
{'label': f"{KODE_AKUN['bangunan_gazebo']['kode']} - {KODE_AKUN['bangunan_gazebo']['nama']}", 'value': 'bangunan_gazebo'},
{'label': f"{KODE_AKUN['kendaraan']['kode']} - {KODE_AKUN['kendaraan']['nama']}", 'value': 'kendaraan'},
{'label': f"{KODE_AKUN['peralatan']['kode']} - {KODE_AKUN['peralatan']['nama']}", 'value': 'peralatan'},
{'label': f"{KODE_AKUN['akumulasi_penyusutan_bangunan']['kode']} - {KODE_AKUN['akumulasi_penyusutan_bangunan']['nama']}", 'value': 'akumulasi_penyusutan_bangunan'},
{'label': f"{KODE_AKUN['akumulasi_penyusutan_kendaraan']['kode']} - {KODE_AKUN['akumulasi_penyusutan_kendaraan']['nama']}", 'value': 'akumulasi_penyusutan_kendaraan'},
{'label': f"{KODE_AKUN['akumulasi_penyusutan_peralatan']['kode']} - {KODE_AKUN['akumulasi_penyusutan_peralatan']['nama']}", 'value': 'akumulasi_penyusutan_peralatan'},
{'label': f"{KODE_AKUN['beban_gaji']['kode']} - {KODE_AKUN['beban_gaji']['nama']}", 'value': 'beban_gaji'},
{'label': f"{KODE_AKUN['beban_listrik']['kode']} - {KODE_AKUN['beban_listrik']['nama']}", 'value': 'beban_listrik'},
{'label': f"{KODE_AKUN['beban_penyusutan_bangunan']['kode']} - {KODE_AKUN['beban_penyusutan_bangunan']['nama']}", 'value': 'beban_penyusutan_bangunan'},
{'label': f"{KODE_AKUN['beban_penyusutan_kendaraan']['kode']} - {KODE_AKUN['beban_penyusutan_kendaraan']['nama']}", 'value': 'beban_penyusutan_kendaraan'},
{'label': f"{KODE_AKUN['beban_penyusutan_peralatan']['kode']} - {KODE_AKUN['beban_penyusutan_peralatan']['nama']}", 'value': 'beban_penyusutan_peralatan'},
{'label': f"{KODE_AKUN['beban_lainnya']['kode']} - {KODE_AKUN['beban_lainnya']['nama']}", 'value': 'beban_lainnya'}
]
def _get_table_data(self, table_name):
"""Helper function to get all data from a table"""
if not self.client:
print(f"â Client not available for {table_name}")
return []
try:
response = self.client.table(table_name).select("*").execute()
print(f"â
Loaded {len(response.data)} records from {table_name}")
return response.data
except Exception as e:
print(f"â Error getting {table_name}: {e}")
return []
def _insert_table_data(self, table_name, data):
"""Helper function to insert data into a table"""
if not self.client:
print(f"â Client not available for insert to {table_name}")
return None
try:
response = self.client.table(table_name).insert(data).execute()
if response.data:
print(f"â
Inserted data to {table_name}")
return response.data[0]
else:
print(f"â No data returned from insert to {table_name}")
return None
except Exception as e:
print(f"â Error inserting to {table_name}: {e}")
return None
def load_all_data(self):
"""Memuat semua data dari Supabase"""
print("đ Loading all data from Supabase...")
try:
# Load data transaksi (simpan di storage global, akan difilter per user saat diakses)
self._all_transaksi_pemasukan = self._get_table_data('transaksi_pemasukan')
self._all_transaksi_pengeluaran = self._get_table_data('transaksi_pengeluaran')
self._all_jurnal_umum = self._get_table_data('jurnal_umum')
self._all_jurnal_penyesuaian = self._get_table_data('jurnal_penyesuaian')
self._all_kartu_persediaan = self._get_table_data('kartu_persediaan')
# Load aset tetap
assets_data = self._get_table_data('assets')
for asset in assets_data:
if asset['jenis_aset'] in self.aset_tetap:
self.aset_tetap[asset['jenis_aset']] = {
'nilai_awal': float(asset['nilai_awal']) if asset['nilai_awal'] else 0,
'penyusutan': float(asset['penyusutan']) if asset['penyusutan'] else 0,
'masa_manfaat': int(asset['masa_manfaat']) if asset['masa_manfaat'] else 0,
'tahun_pembelian': int(asset['tahun_pembelian']) if asset['tahun_pembelian'] else datetime.now().year
}
print("â
All data loaded successfully from Supabase")
print(f"đ Statistics: {len(self._all_transaksi_pemasukan)} pemasukan, {len(self._all_transaksi_pengeluaran)} pengeluaran")
print(f"đ Statistics: {len(self._all_jurnal_umum)} jurnal, {len(self._all_kartu_persediaan)} kartu persediaan")
except Exception as e:
print(f"â Error loading all data from Supabase: {e}")
# ------------------ User-scoped access helpers ------------------
def set_active_user(self, user_id):
"""Set active user id untuk memfilter data yang ditampilkan/diakses."""
if user_id:
self.active_user_id = str(user_id)
else:
self.active_user_id = None
@property
def transaksi_pemasukan(self):
if not self.active_user_id:
return []
return [t for t in self._all_transaksi_pemasukan if str(t.get('user_id')) == str(self.active_user_id)]
@property
def transaksi_pengeluaran(self):
if not self.active_user_id:
return []
return [t for t in self._all_transaksi_pengeluaran if str(t.get('user_id')) == str(self.active_user_id)]
@property
def jurnal_umum(self):
if not self.active_user_id:
return []
return [j for j in self._all_jurnal_umum if str(j.get('user_id')) == str(self.active_user_id)]
@property
def jurnal_penyesuaian(self):
if not self.active_user_id:
return []
return [j for j in self._all_jurnal_penyesuaian if str(j.get('user_id')) == str(self.active_user_id)]
@property
def kartu_persediaan(self):
if not self.active_user_id:
return []
return [k for k in self._all_kartu_persediaan if str(k.get('user_id')) == str(self.active_user_id)]
# Suppliers dan buku besar pembantu per-user
@property
def suppliers(self):
uid = str(self.active_user_id) if self.active_user_id else None
if not uid:
return []
if uid not in self._all_suppliers:
self._all_suppliers[uid] = []
return self._all_suppliers[uid]
@suppliers.setter
def suppliers(self, value):
uid = str(self.active_user_id) if self.active_user_id else None
if not uid:
return
self._all_suppliers[uid] = value if isinstance(value, list) else list(value)
@property
def buku_besar_pembantu(self):
uid = str(self.active_user_id) if self.active_user_id else None
if not uid:
return {}
if uid not in self._all_buku_besar_pembantu:
self._all_buku_besar_pembantu[uid] = {}
return self._all_buku_besar_pembantu[uid]
@buku_besar_pembantu.setter
def buku_besar_pembantu(self, value):
uid = str(self.active_user_id) if self.active_user_id else None
if not uid:
return
self._all_buku_besar_pembantu[uid] = value if isinstance(value, dict) else dict(value)
def save_data(self):
"""Menyimpan data ke Supabase - untuk backward compatibility"""
self.save_all_data()
def save_all_data(self):
"""Menyimpan semua data ke Supabase"""
try:
print("đž Saving all data to Supabase...")
# Simpan transaksi pemasukan (semua storage, tapi sertakan user_id)
for trans in self._all_transaksi_pemasukan:
if 'id' not in trans:
data_to_save = {
'tanggal': trans['tanggal'],
'jenis': trans['jenis'],
'jumlah': float(trans['jumlah']),
'quantity': int(trans.get('quantity', 0)),
'hpp': float(trans.get('hpp', 0)),
'keterangan': trans.get('keterangan', ''),
'ref': trans.get('ref', ''),
'kode_akun_debit': trans.get('kode_akun_debit', ''),
'kode_akun_kredit': trans.get('kode_akun_kredit', ''),
'user_id': trans.get('user_id', self.active_user_id)
}
saved = self._insert_table_data('transaksi_pemasukan', data_to_save)
if saved:
trans['id'] = saved['id']
# Simpan transaksi pengeluaran
for trans in self._all_transaksi_pengeluaran:
if 'id' not in trans:
data_to_save = {
'tanggal': trans['tanggal'],
'jenis': trans['jenis'],
'kode_barang': trans.get('kode_barang', ''),
'jumlah': float(trans['jumlah']),
'quantity': int(trans.get('quantity', 0)),
'metode_bayar': trans.get('metode_bayar', 'tunai'),
'supplier': trans.get('supplier', ''),
'keterangan': trans.get('keterangan', ''),
'ref': trans.get('ref', ''),
'kode_akun_debit': trans.get('kode_akun_debit', ''),
'kode_akun_kredit': trans.get('kode_akun_kredit', ''),
'user_id': trans.get('user_id', self.active_user_id)
}
saved = self._insert_table_data('transaksi_pengeluaran', data_to_save)
if saved:
trans['id'] = saved['id']
# Simpan jurnal umum
for jurnal in self._all_jurnal_umum:
if 'id' not in jurnal:
data_to_save = {
'tanggal': jurnal['tanggal'],
'keterangan': jurnal['keterangan'],
'ref': jurnal.get('ref', ''),
'akun_debit': jurnal['akun_debit'],
'kode_akun_debit': jurnal['kode_akun_debit'],
'jumlah_debit': float(jurnal['jumlah_debit']),
'akun_kredit': jurnal['akun_kredit'],
'kode_akun_kredit': jurnal['kode_akun_kredit'],
'jumlah_kredit': float(jurnal['jumlah_kredit']),
'user_id': jurnal.get('user_id', self.active_user_id)
}
saved = self._insert_table_data('jurnal_umum', data_to_save)
if saved:
jurnal['id'] = saved['id']
# Simpan jurnal penyesuaian
for jurnal in self._all_jurnal_penyesuaian:
if 'id' not in jurnal:
data_to_save = {
'tanggal': jurnal['tanggal'],
'keterangan': jurnal['keterangan'],
'ref': jurnal.get('ref', ''),
'akun_debit': jurnal['akun_debit'],
'kode_akun_debit': jurnal['kode_akun_debit'],
'jumlah_debit': float(jurnal['jumlah_debit']),
'akun_kredit': jurnal['akun_kredit'],
'kode_akun_kredit': jurnal['kode_akun_kredit'],
'jumlah_kredit': float(jurnal['jumlah_kredit']),
'user_id': jurnal.get('user_id', self.active_user_id)
}
saved = self._insert_table_data('jurnal_penyesuaian', data_to_save)
if saved:
jurnal['id'] = saved['id']
# Simpan kartu persediaan
for item in self._all_kartu_persediaan:
if 'id' not in item:
data_to_save = {
'tanggal': item['tanggal'],
'kode_barang': item['kode_barang'],
'nama_barang': item['nama_barang'],
'masuk_qty': int(item['masuk_qty']),
'masuk_harga': float(item['masuk_harga']),
'masuk_total': float(item['masuk_total']),
'keluar_qty': int(item['keluar_qty']),
'keluar_harga': float(item['keluar_harga']),
'keluar_total': float(item['keluar_total']),
'saldo_qty': int(item['saldo_qty']),
'saldo_harga': float(item['saldo_harga']),
'saldo_total': float(item['saldo_total']),
'keterangan': item.get('keterangan', ''),
'user_id': item.get('user_id', self.active_user_id)
}
saved = self._insert_table_data('kartu_persediaan', data_to_save)
if saved:
item['id'] = saved['id']
# Simpan aset tetap
for jenis_aset, data in self.aset_tetap.items():
if data['nilai_awal'] > 0: # Hanya simpan aset yang sudah diisi
asset_data = {
'jenis_aset': jenis_aset,
'nilai_awal': float(data['nilai_awal']),
'penyusutan': float(data['penyusutan']),
'masa_manfaat': int(data['masa_manfaat']),
'tahun_pembelian': int(data['tahun_pembelian'])
}
# Cek apakah aset sudah ada
existing_assets = self._get_table_data('assets')
existing_asset = next((a for a in existing_assets if a['jenis_aset'] == jenis_aset), None)
if existing_asset:
# Update existing
try:
self.client.table('assets').update(asset_data).eq('id', existing_asset['id']).execute()
print(f"â
Updated asset: {jenis_aset}")
except Exception as e:
print(f"â Error updating asset {jenis_aset}: {e}")
else:
# Insert new
saved = self._insert_table_data('assets', asset_data)
if saved:
print(f"â
Inserted new asset: {jenis_aset}")
print("â
All data saved successfully to Supabase")
except Exception as e:
print(f"â Error saving all data to Supabase: {e}")
def initialize_user_from_template(self, user_id, template_path='sibal_data.json'):
"""Initialize a new user's data by copying from a local template JSON.
This will attach `user_id` to each record and save to Supabase and in-memory storage.
"""
try:
if not os.path.exists(template_path):
print(f"â ī¸ Template file not found: {template_path}")
return False
with open(template_path, 'r', encoding='utf-8') as f:
# file may have markdown fences in repo, try to load JSON content
content = f.read()
# strip possible ```json fences
content = content.strip()
if content.startswith('```'):
# remove first and last fences
parts = content.split('\n')
# find first line that starts with ``` and remove it
if parts[0].startswith('```'):
parts = parts[1:]
if parts and parts[-1].startswith('```'):
parts = parts[:-1]
content = '\n'.join(parts)
data = json.loads(content)
# Create deterministic random generator per user to make templates unique
uid = str(user_id)
seed = int(hashlib.sha256(uid.encode('utf-8')).hexdigest(), 16) % (2**32)
rng = random.Random(seed)
# per-user overall scale (makes differences more pronounced)
user_scale = 0.5 + rng.random() * 2.5 # range 0.5 .. 3.0
def unique_ref(orig_ref):
if not orig_ref:
orig_ref = 'REF'
return f"{orig_ref}-{uid[:6]}-{rng.randint(100,999)}"
def jitter_number(value, pct=0.08, apply_scale=True):
try:
v = float(value)
except Exception:
return value
base = v * (user_scale if apply_scale else 1.0)
change = (rng.random() * 2 - 1) * pct
newv = base * (1 + change)
# round sensible for money/ints
if float(value).is_integer():
return int(round(newv))
return float(round(newv, 2))
# Helper to insert list of records into table and in-memory _all_ list
def insert_list_unique(table_name, records, target_list_name):
if not records:
return
# Shuffle and take a variable sample size so different users may get different subsets
recs = list(records)
rng.shuffle(recs)
sample_ratio = rng.uniform(0.5, 1.2)
sample_size = max(1, int(len(recs) * sample_ratio))
recs = recs[:sample_size]
# Optionally add a few synthetic variants per user
extra_copies = rng.randint(0, max(0, int(len(recs) * 0.3)))
for _ in range(extra_copies):
pick = dict(rng.choice(records))
recs.append(pick)
for rec in recs:
rec = dict(rec) # copy
# Make some fields unique/per-user
if 'ref' in rec:
rec['ref'] = unique_ref(rec.get('ref'))
# jitter numeric fields more strongly and apply user scale
for num_field in ['jumlah', 'masuk_total', 'masuk_harga', 'keluar_total', 'saldo_total', 'jumlah_debit', 'jumlah_kredit', 'nilai_awal']:
if num_field in rec:
rec[num_field] = jitter_number(rec[num_field], pct=0.12)
# jitter quantity fields
for num_field in ['quantity', 'masuk_qty', 'keluar_qty', 'saldo_qty']:
if num_field in rec:
try:
rec[num_field] = int(jitter_number(rec[num_field], pct=0.25))
except Exception:
pass
# shift dates
for k in list(rec.keys()):
if 'tanggal' in k and isinstance(rec[k], str):
try:
dt = datetime.fromisoformat(rec[k])
dt = dt + timedelta(days=rng.randint(-30, 30))
rec[k] = dt.date().isoformat()
except Exception:
pass
# supplier uniqueness
if 'supplier' in rec and rec.get('supplier'):
rec['supplier'] = f"{rec.get('supplier')}-{uid[-4:]}"
rec['user_id'] = uid
# Try to insert robustly: if DB rejects unknown column, remove it and retry
saved = None
if not self.client:
saved = None
else:
attempts = 0
data_to_try = dict(rec)
while attempts < 5:
try:
resp = self.client.table(table_name).insert(data_to_try).execute()
if resp and getattr(resp, 'data', None):
saved = resp.data[0]
break
except Exception as e:
msg = str(e)
import re
m = re.search(r"Could not find the '([a-zA-Z0-9_]+)' column", msg)
if m:
col = m.group(1)
if col in data_to_try:
del data_to_try[col]
attempts += 1
continue
# fallback: stop retrying
break
if saved:
rec['id'] = saved.get('id')
# Always append to in-memory storage so per-user view exists even if DB insert failed
getattr(self, target_list_name).append(rec)
insert_list_unique('transaksi_pemasukan', data.get('transaksi_pemasukan', []), '_all_transaksi_pemasukan')
insert_list_unique('transaksi_pengeluaran', data.get('transaksi_pengeluaran', []), '_all_transaksi_pengeluaran')
insert_list_unique('jurnal_umum', data.get('jurnal_umum', []), '_all_jurnal_umum')
insert_list_unique('jurnal_penyesuaian', data.get('jurnal_penyesuaian', []), '_all_jurnal_penyesuaian')
insert_list_unique('kartu_persediaan', data.get('kartu_persediaan', []), '_all_kartu_persediaan')
# suppliers: store per-user list and append uid suffix so names are unique per user
sups = data.get('suppliers', [])
self._all_suppliers[uid] = [f"{s}_{uid[:6]}" for s in sups]
# buku_besar_pembantu: copy and prefix with user_id (and adapt supplier names)
bbp = data.get('buku_besar_pembantu', {})
self._all_buku_besar_pembantu[uid] = {}
for sup, items in bbp.items():
sup_uid = f"{sup}_{uid[:6]}"
self._all_buku_besar_pembantu[uid][sup_uid] = []
for item in items:
item_copy = dict(item)
item_copy['user_id'] = uid
# jitter numeric fields in helper
for num_field in ['debit', 'kredit', 'saldo']:
if num_field in item_copy:
item_copy[num_field] = jitter_number(item_copy[num_field])
self._all_buku_besar_pembantu[uid][sup_uid].append(item_copy)
# aset_tetap: store under user context in assets table, jitter nilai_awal a bit
aset = data.get('aset_tetap', {})
for jenis, asetdata in aset.items():
asetdata_copy = dict(asetdata)
asetdata_copy['jenis_aset'] = jenis
# jitter nilai_awal
if 'nilai_awal' in asetdata_copy:
asetdata_copy['nilai_awal'] = jitter_number(asetdata_copy['nilai_awal'], pct=0.15)
asetdata_copy['user_id'] = uid
# insert with same robust approach
if self.client:
attempts = 0
data_to_try = dict(asetdata_copy)
while attempts < 5:
try:
resp = self.client.table('assets').insert(data_to_try).execute()
break
except Exception as e:
msg = str(e)
import re
m = re.search(r"Could not find the '([a-zA-Z0-9_]+)' column", msg)
if m:
col = m.group(1)
if col in data_to_try:
del data_to_try[col]
attempts += 1
continue
break
print(f"â
Initialized unique template data for user {user_id}")
return True
except Exception as e:
print(f"â Error initializing user from template: {e}")
return False
def kurangi_persediaan(self, kode_barang, qty, tanggal, keterangan):
"""Mengurangi persediaan saat penjualan dengan metode FIFO"""
try:
# Cari barang di master
barang = next((item for item in self.master_persediaan if item['kode'] == kode_barang), None)
if not barang:
print(f"Barang dengan kode {kode_barang} tidak ditemukan")
return False
# Hitung saldo terakhir
saldo_terakhir = 0
saldo_harga = 0
if self.kartu_persediaan:
transaksi_barang = [t for t in self.kartu_persediaan if t['kode_barang'] == kode_barang]
if transaksi_barang:
saldo_terakhir = transaksi_barang[-1]['saldo_qty']
saldo_harga = transaksi_barang[-1]['saldo_harga']
if saldo_terakhir < qty:
print(f"Stok tidak cukup. Stok tersedia: {saldo_terakhir}, butuh: {qty}")
return False
# Hitung dengan metode FIFO
total_keluar = 0
sisa_qty = qty
transaksi_masuk = [t for t in self.kartu_persediaan
if t['kode_barang'] == kode_barang and t['masuk_qty'] > 0]
for trans in transaksi_masuk:
if sisa_qty <= 0:
break
if trans['saldo_qty'] > 0:
qty_keluar = min(sisa_qty, trans['saldo_qty'])
harga_keluar = trans['masuk_harga'] # FIFO: pakai harga pembelian pertama
total_keluar += qty_keluar * harga_keluar
sisa_qty -= qty_keluar
# Buat entri kartu persediaan
saldo_qty_baru = saldo_terakhir - qty
saldo_harga_baru = saldo_harga # Harga rata-rata tidak berubah untuk FIFO
entri_persediaan = {
'tanggal': tanggal,
'kode_barang': kode_barang,
'nama_barang': barang['nama'],
'masuk_qty': 0,
'masuk_harga': 0,
'masuk_total': 0,
'keluar_qty': qty,
'keluar_harga': total_keluar / qty if qty > 0 else 0,
'keluar_total': total_keluar,
'saldo_qty': saldo_qty_baru,
'saldo_harga': saldo_harga_baru,
'saldo_total': saldo_qty_baru * saldo_harga_baru,
'keterangan': f'Penjualan - {keterangan}'
}
entri_persediaan['user_id'] = self.active_user_id
self._all_kartu_persediaan.append(entri_persediaan)
return True
except Exception as e:
print(f"Error mengurangi persediaan: {e}")
return False
# TAMBAHKAN JUGA FUNGSI INI:
def get_harga_pokok(self, kode_barang):
"""Mendapatkan harga pokok dari master persediaan"""
barang = next((item for item in self.master_persediaan if item['kode'] == kode_barang), None)
return barang['harga_beli'] if barang else 0
sibal_data = SIBALData()
def create_top_navigation():
# Gunakan try-except untuk handle kasus current_user None
try:
is_authenticated = current_user.is_authenticated
except:
is_authenticated = False
if is_authenticated:
user_section = html.Div([
html.Div([
html.Div("đ¤", className="avatar"),
html.Div([
html.Div(current_user.username, style={'fontWeight': '600', 'fontSize': '0.95rem'}),
html.Div(current_user.email, style={'fontSize': '0.8rem', 'opacity': '0.7'})
])
], className="user-menu"),
html.A(
html.Div([
html.I(className="fas fa-sign-out-alt"),
" Logout"
], className="nav-link"),
href="/auth/logout",
style={'textDecoration': 'none'}
)
], style={'display': 'flex', 'alignItems': 'center', 'gap': '10px'})
else:
user_section = html.Div([
dcc.Link([
html.I(className="fas fa-sign-in-alt"),
" Login"
], href="/login", className="nav-link"),
dcc.Link([
html.I(className="fas fa-user-plus"),
" Daftar"
], href="/signup", className="nav-link")
], style={'display': 'flex', 'alignItems': 'center', 'gap': '10px'})
return html.Div([
html.Div([
html.Div([
html.Div([
html.Img(
src="/assets/logo-sibal-png.png",
style={
'width': '100%',
'height': '100%',
'objectFit': 'contain'
}
)
], className="brand-logo"),
html.Div("SIBAL", className="brand-text")
], className="nav-brand"),
html.Div([
dcc.Link([
html.I(className="fas fa-home"),
" Dashboard"
], href="/", className="nav-link"),
dcc.Link([
html.I(className="fas fa-calculator"),
" Akuntansi Dasar"
], href="/akuntansi-dasar", className="nav-link"),
dcc.Link([
html.I(className="fas fa-chart-bar"),
" Laporan & Analisis"
], href="/laporan-analisis", className="nav-link"),
], className="nav-links"),
user_section
], className="nav-container"),
], className="glass-nav")
def create_akuntansi_sub_nav():
return html.Div([
html.Div([
dcc.Link([
html.I(className="fas fa-edit"),
" Transaksi"
], href="/transaksi", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-box"),
" Kartu Persediaan"
], href="/kartu-persediaan", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-book"),
" Jurnal Umum"
], href="/jurnal-umum", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-book-open"),
" Buku Besar"
], href="/buku-besar", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-users"),
" Buku Besar Pembantu"
], href="/buku-besar-pembantu", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-building"),
" Aset Tetap"
], href="/aset-tetap", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-balance-scale"),
" Neraca Saldo"
], href="/neraca-saldo", className="sub-nav-link"),
], className="sub-nav-container"),
], className="sub-nav")
def create_laporan_sub_nav():
return html.Div([
html.Div([
dcc.Link([
html.I(className="fas fa-sync-alt"),
" Jurnal Penyesuaian"
], href="/jurnal-penyesuaian", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-chart-line"),
" Neraca Setelah Penyesuaian"
], href="/neraca-setelah-penyesuaian", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-money-bill-wave"),
" Laporan Laba Rugi"
], href="/laporan-laba-rugi", className="sub-nav-link"),
dcc.Link([
html.I(className="fas fa-file-alt"),
" Laporan Keuangan"
], href="/laporan-keuangan", className="sub-nav-link"),
], className="sub-nav-container"),
], className="sub-nav")
def dashboard_layout():
total_pemasukan = sum(t.get('jumlah', 0) for t in sibal_data.transaksi_pemasukan)
total_pengeluaran = sum(t.get('jumlah', 0) for t in sibal_data.transaksi_pengeluaran)
total_jurnal = len(sibal_data.jurnal_umum)
total_persediaan = len(sibal_data.kartu_persediaan)
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-gem")
], className="card-icon"),
html.Div([
html.H1("Dashboard SIBAL", className="card-title"),
html.P("Sistem Informasi Berbasis Akuntansi Ikan Bawal", className="card-subtitle")
])
], className="card-header")
], className="glass-card"),
html.Div([
html.Div([
html.Div("Rp {:,.0f}".format(total_pemasukan), className="stat-value"),
html.Div("Total Pendapatan", className="stat-label"),
html.I(className="fas fa-arrow-up stat-icon")
], className="stat-card"),
html.Div([
html.Div("Rp {:,.0f}".format(total_pengeluaran), className="stat-value"),
html.Div("Total Pengeluaran", className="stat-label"),
html.I(className="fas fa-arrow-down stat-icon")
], className="stat-card"),
html.Div([
html.Div("{:d}".format(total_jurnal), className="stat-value"),
html.Div("Jurnal Umum", className="stat-label"),
html.I(className="fas fa-book stat-icon")
], className="stat-card"),
html.Div([
html.Div("{:d}".format(total_persediaan), className="stat-value"),
html.Div("Kartu Persediaan", className="stat-label"),
html.I(className="fas fa-box stat-icon")
], className="stat-card"),
], className="stats-grid"),
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-rocket")
], className="card-icon"),
html.H2("Aksi Cepat", className="card-title")
], className="card-header"),
html.Div([
dcc.Link([
html.Div([
html.Div([
html.I(className="fas fa-calculator")
], className="action-icon"),
html.H3("Akuntansi Dasar", className="action-title"),
html.P("Kelola transaksi, jurnal, dan buku besar dengan fitur lengkap", className="action-desc")
])
], href="/akuntansi-dasar", className="action-card"),
dcc.Link([
html.Div([
html.Div([
html.I(className="fas fa-chart-bar")
], className="action-icon"),
html.H3("Laporan & Analisis", className="action-title"),
html.P("Generate laporan keuangan lengkap dan analisis mendalam", className="action-desc")
])
], href="/laporan-analisis", className="action-card"),
], className="actions-grid"),
], className="glass-card"),
], className="main-container")
def format_rupiah(amount):
"""Format number to Rupiah currency dengan pemisah ribuan"""
if amount == 0:
return ""
return f"Rp{amount:,.0f}".replace(",", ".")
# ========== LAYOUT FUNCTIONS YANG DIPERBAIKI ==========
def transaksi_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-edit")
], className="card-icon"),
html.H1("Transaksi Pemancingan Bawal", className="card-title")
], className="card-header")
], className="glass-card"),
html.Div([
html.H4("Pilih Tanggal Transaksi:", className="form-label"),
dcc.DatePickerSingle(
id='selected-date',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="glass-card"),
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-arrow-down")
], className="card-icon"),
html.H2("Pemasukan - Pendapatan Pemancingan", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.H3("Input Transaksi Pendapatan", className="form-section-title"),
dcc.Dropdown(
id='pemasukan-jenis',
options=[
{'label': 'Tiket Masuk', 'value': 'tiket_masuk'},
{'label': 'Penjualan Ikan Bawal', 'value': 'penjualan_ikan'},
{'label': 'Setoran Modal', 'value': 'modal'}
],
placeholder='Pilih Jenis Transaksi',
className="form-control"
),
# SESUDAH:
html.Div([
html.Label("Harga Jual per Unit", className="form-label"),
dcc.Input(
id='pemasukan-harga-jual',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Quantity", className="form-label"),
dcc.Input(
id='pemasukan-qty',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Quantity (kg) - untuk penjualan ikan", className="form-label"),
dcc.Input(
id='pemasukan-qty',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("HPP per kg (untuk penjualan ikan)", className="form-label"),
dcc.Input(
id='pemasukan-hpp',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Keterangan Transaksi", className="form-label"),
dcc.Input(
id='pemasukan-keterangan',
type='text',
placeholder='Keterangan...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Nomor Referensi", className="form-label"),
dcc.Input(
id='pemasukan-ref',
type='text',
placeholder='REF-...',
className="form-input"
)
], className="form-group"),
html.Button(
"â Tambah Transaksi",
id='btn-tambah-pemasukan',
className="btn btn-success"
)
], className="form-container compact-form"),
html.Div([
html.H3("Daftar Transaksi Pendapatan", className="form-section-title"),
html.Div(id='daftar-pemasukan', className="data-container"),
html.Button(
"đī¸ Hapus Transaksi Terpilih",
id='btn-hapus-pemasukan',
className="btn btn-danger"
),
html.Hr(),
html.Button(
"đ Akumulasi ke Jurnal",
id='btn-akumulasi-pemasukan',
className="btn btn-primary"
)
], className="data-container"),
], className="form-row"),
], className="glass-card"),
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-arrow-up")
], className="card-icon"),
html.H2("Pengeluaran - Pembelian & Beban", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.H3("Input Pengeluaran", className="form-section-title"),
dcc.Dropdown(
id='pengeluaran-jenis',
options=[
{'label': 'Pembelian Persediaan', 'value': 'pembelian_persediaan'},
{'label': 'Beban Gaji', 'value': 'beban_gaji'},
{'label': 'Beban Listrik', 'value': 'beban_listrik'},
{'label': 'Beban Penyusutan Bangunan', 'value': 'beban_penyusutan_bangunan'},
{'label': 'Beban Penyusutan Kendaraan', 'value': 'beban_penyusutan_kendaraan'},
{'label': 'Beban Penyusutan Peralatan', 'value': 'beban_penyusutan_peralatan'},
{'label': 'Beban Lainnya', 'value': 'beban_lainnya'}
],
placeholder='Pilih Jenis Pengeluaran',
className="form-control"
),
dcc.Dropdown(
id='pengeluaran-barang',
options=[{'label': f"{item['kode']} - {item['nama']}", 'value': item['kode']}
for item in sibal_data.master_persediaan],
placeholder='Pilih Barang (hanya untuk pembelian persediaan)',
className="form-control"
),
# SESUDAH:
html.Div([
html.Label("HPP per Unit", className="form-label"),
dcc.Input(
id='pengeluaran-hpp',
type='number',
placeholder='0',
min=0,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Quantity", className="form-label"),
dcc.Input(
id='pengeluaran-qty',
type='number',
placeholder='0',
min=1,
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Quantity (kg) - hanya untuk pembelian persediaan", className="form-label"),
dcc.Input(
id='pengeluaran-qty',
type='number',
placeholder='0',
min=1,
className="form-input"
)
], className="form-group"),
dcc.Dropdown(
id='pengeluaran-metode',
options=[
{'label': 'Tunai', 'value': 'tunai'},
{'label': 'Kredit', 'value': 'kredit'}
],
placeholder='Metode Pembayaran',
className="form-control"
),
html.Div([
html.Label("Nama Supplier (jika kredit)", className="form-label"),
dcc.Input(
id='pengeluaran-supplier',
type='text',
placeholder='Nama supplier...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Keterangan Pengeluaran", className="form-label"),
dcc.Input(
id='pengeluaran-keterangan',
type='text',
placeholder='Keterangan...',
className="form-input"
)
], className="form-group"),
html.Div([
html.Label("Nomor Referensi", className="form-label"),
dcc.Input(
id='pengeluaran-ref',
type='text',
placeholder='REF-...',
className="form-input"
)
], className="form-group"),
html.Button(
"â Tambah Pengeluaran",
id='btn-tambah-pengeluaran',
className="btn btn-danger"
)
], className="form-container compact-form"),
html.Div([
html.H3("Daftar Pengeluaran", className="form-section-title"),
html.Div(id='daftar-pengeluaran', className="data-container"),
html.Button(
"đī¸ Hapus Pengeluaran Terpilih",
id='btn-hapus-pengeluaran',
className="btn btn-danger"
),
html.Hr(),
html.Button(
"đ Akumulasi ke Jurnal",
id='btn-akumulasi-pengeluaran',
className="btn btn-primary"
)
], className="data-container"),
], className="form-row"),
], className="glass-card"),
html.Div([
html.H3("đ Summary Akumulasi", className="card-title"),
html.Div(id='summary-akumulasi', className="summary-container")
], className="glass-card"),
], className="main-container")
def jurnal_umum_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-book")
], className="card-icon"),
html.H1("Jurnal Umum", className="card-title")
], className="card-header"),
html.Div([
html.Button(
"đ Refresh Data",
id='btn-refresh-jurnal',
className="btn btn-primary"
),
html.Hr(),
html.H3("Daftar Jurnal Umum", className="form-section-title"),
html.Div(id='tabel-jurnal-umum', className="table-container"),
html.Hr(),
html.H3("Rekapitulasi Jurnal Umum", className="form-section-title"),
html.Div(id='rekapitulasi-jurnal', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def kartu_persediaan_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-box")
], className="card-icon"),
html.H1("Kartu Persediaan Ikan Bawal", className="card-title")
], className="card-header"),
html.Div([
html.Button(
"đ Refresh Data",
id='btn-refresh-persediaan',
className="btn btn-primary",
style={'marginBottom': '20px'}
),
html.Div(id='tabel-kartu-persediaan', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def buku_besar_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-book-open")
], className="card-icon"),
html.H1("Buku Besar", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.H3("Pilih Akun Buku Besar", className="form-section-title"),
dcc.Dropdown(
id='dropdown-akun-buku-besar',
options=sibal_data.daftar_akun,
placeholder='Pilih Akun Buku Besar',
className="form-control"
)
], className="form-container compact-form"),
html.Div(id='detail-buku-besar', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def buku_besar_pembantu_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-users")
], className="card-icon"),
html.H1("Buku Besar Pembantu - Utang", className="card-title")
], className="card-header"),
html.Div([
# TOMBOL REFRESH dengan callback yang benar
html.Div([
html.Button(
"đ Refresh Data",
id='btn-refresh-buku-pembantu',
className="btn btn-primary",
style={'marginBottom': '20px'}
)
], style={'textAlign': 'center'}),
html.H3("Daftar Supplier", className="form-section-title"),
html.Div(id='daftar-supplier', className="data-container"),
html.Hr(),
html.H3("Detail Utang per Supplier", className="form-section-title"),
html.Div(id='detail-buku-besar-pembantu', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def neraca_saldo_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-balance-scale")
], className="card-icon"),
html.H1("Neraca Saldo", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.Label("Periode Neraca Saldo", className="form-label"),
dcc.DatePickerSingle(
id='tanggal-neraca-saldo',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group"),
html.Button(
"đ Generate Neraca Saldo",
id='btn-generate-neraca-saldo',
className="btn btn-primary"
),
html.Div(id='tabel-neraca-saldo', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def jurnal_penyesuaian_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-sync-alt")
], className="card-icon"),
html.H1("Jurnal Penyesuaian", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.Label("Periode Jurnal Penyesuaian", className="form-label"),
dcc.DatePickerSingle(
id='tanggal-penyesuaian',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group"),
html.Button(
"đ Generate Jurnal Penyesuaian",
id='btn-generate-penyesuaian',
className="btn btn-primary"
),
html.Div(id='daftar-jurnal-penyesuaian', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def neraca_setelah_penyesuaian_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-chart-line")
], className="card-icon"),
html.H1("Neraca Setelah Penyesuaian", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.Label("Pilih Tanggal Neraca:", className="form-label"),
dcc.DatePickerSingle(
id='tanggal-neraca-penyesuaian',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group"),
html.Button(
"đ Generate Neraca Setelah Penyesuaian",
id='btn-generate-neraca-penyesuaian',
className="btn btn-primary"
),
html.Div(id='tabel-neraca-penyesuaian', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def laporan_laba_rugi_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-money-bill-wave")
], className="card-icon"),
html.H1("Laporan Laba Rugi", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.Label("Periode Laporan Laba Rugi", className="form-label"),
dcc.DatePickerSingle(
id='tanggal-laba-rugi',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group"),
html.Button(
"đ Generate Laporan Laba Rugi",
id='btn-generate-laba-rugi',
className="btn btn-primary"
),
html.Div(id='tabel-laba-rugi', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def neraca_lajur_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-table")
], className="card-icon"),
html.H1("Neraca Lajur", className="card-title"),
html.P("Worksheet - Laporan Keuangan Komprehensif", className="card-subtitle")
], className="card-header"),
html.Div([
html.Div([
html.Label("Periode Neraca Lajur:", className="form-label"),
dcc.DatePickerRange(
id='periode-neraca-lajur',
start_date=datetime.now().date().replace(day=1), # awal bulan
end_date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group", style={'marginBottom': '20px'}),
html.Button(
"đ Generate Neraca Lajur",
id='btn-generate-neraca-lajur',
className="btn btn-primary",
style={'marginBottom': '20px'}
),
html.Div(id='tabel-neraca-lajur', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def neraca_lajur_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-table")
], className="card-icon"),
html.H1("Neraca Lajur", className="card-title"),
html.P("Worksheet - Laporan Keuangan Komprehensif", className="card-subtitle")
], className="card-header"),
html.Div([
html.Div([
html.Label("Periode Neraca Lajur:", className="form-label"),
dcc.DatePickerRange(
id='periode-neraca-lajur',
start_date=datetime.now().date().replace(day=1), # awal bulan
end_date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group", style={'marginBottom': '20px'}),
html.Button(
"đ Generate Neraca Lajur",
id='btn-generate-neraca-lajur',
className="btn btn-primary",
style={'marginBottom': '20px'}
),
html.Div(id='tabel-neraca-lajur', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
def laporan_keuangan_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-file-alt")
], className="card-icon"),
html.H1("Laporan Keuangan Lengkap", className="card-title")
], className="card-header"),
html.Div([
html.Div([
html.Label("Pilih Tanggal Laporan:", className="form-label"),
dcc.DatePickerSingle(
id='tanggal-laporan-keuangan',
date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group"),
html.Button(
"đ Generate Laporan Keuangan Lengkap",
id='btn-generate-laporan-keuangan',
className="btn btn-primary"
),
html.Div(id='tabel-laporan-keuangan', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
# ==================== CALLBACKS YANG DIPERBAIKI ====================
# Callback untuk complete profile
@app.callback(
[Output('complete-profile-alert', 'children'),
Output('complete-username', 'value'),
Output('complete-password', 'value'),
Output('complete-confirm-password', 'value')],
Input('btn-complete-profile', 'n_clicks'),
[State('complete-username', 'value'),
State('complete-password', 'value'),
State('complete-confirm-password', 'value')],
prevent_initial_call=True
)
def handle_complete_profile(n_clicks, username, password, confirm_password):
if n_clicks and n_clicks > 0:
oauth_user = session.get('oauth_user')
if not oauth_user:
return create_alert("Session expired. Silakan login kembali.", "error"), "", "", ""
if not all([username, password, confirm_password]):
return create_alert("Harap lengkapi semua field.", "error"), dash.no_update, "", ""
if password != confirm_password:
return create_alert("Password dan konfirmasi password tidak cocok.", "error"), dash.no_update, "", ""
if len(password) < 6:
return create_alert("Password minimal 6 karakter.", "error"), dash.no_update, "", ""
# Create user dengan fungsi yang sudah diperbaiki
user, message = create_user(username, oauth_user['email'], password)
if not user:
return create_alert(message, "error"), dash.no_update, "", ""
# Login user dengan fungsi yang sudah diperbaiki
login_user(user, remember=True)
# Clear oauth session
session.pop('oauth_user', None)
return html.Div([
html.P("â
Profil berhasil disimpan! Redirecting...",
style={'color': COLORS['success'], 'textAlign': 'center', 'fontWeight': 'bold'}),
dcc.Location(href='/', id='redirect-home', refresh=True)
]), "", "", ""
sibal_data.save_all_data()
return dash.no_update, dash.no_update, dash.no_update, dash.no_update
from flask import request
import json
from authlib.integrations.flask_client import OAuth
# Setup OAuth
oauth = OAuth(server)
# In-memory store for temporarily keeping oauth_user keyed by state.
# This avoids losing the user data when browser cookies are restricted during OAuth redirects.
OAUTH_STATE_STORE = {}
# Konfigurasi Google OAuth - GANTI dengan credentials Anda
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
GOOGLE_REDIRECT_URI = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8051/auth/callback')
# If env vars missing, try local client_secret JSON files (untracked)
if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET:
cid, csec = _load_google_credentials_from_local_file()
if cid and csec:
GOOGLE_CLIENT_ID = GOOGLE_CLIENT_ID or cid
GOOGLE_CLIENT_SECRET = GOOGLE_CLIENT_SECRET or csec
# also set in environment so other parts can read it
os.environ.setdefault('GOOGLE_CLIENT_ID', GOOGLE_CLIENT_ID)
os.environ.setdefault('GOOGLE_CLIENT_SECRET', GOOGLE_CLIENT_SECRET)
# Debug info (do not print secrets)
print(f"đ Google Client ID set: {bool(GOOGLE_CLIENT_ID)}")
print(f"đ Google Client Secret set: {bool(GOOGLE_CLIENT_SECRET)}")
try:
google = oauth.register(
name='SIBAL',
client_id=GOOGLE_CLIENT_ID,
client_secret=GOOGLE_CLIENT_SECRET,
server_metadata_url='https://accounts.google.com/.well-known/openid_configuration',
client_kwargs={
'scope': 'openid email profile',
'redirect_uri': 'http://localhost:8051/auth/callback' # PASTIKAN INI SAMA
}
)
print("â
Google OAuth configured successfully")
except Exception as e:
print(f"â Google OAuth configuration failed: {e}")
google = None
@server.route('/auth/login')
def auth_login():
"""Redirect ke Google OAuth"""
try:
# Generate state untuk security
state = secrets.token_urlsafe(16)
# Store state in both session and in-memory store as a fallback
session['oauth_state'] = state
# capture action from query param so callback can behave accordingly
try:
action = request.args.get('action')
except Exception:
action = None
OAUTH_STATE_STORE[state] = {'action': action}
try:
# Keep session persistent for the OAuth flow
session.permanent = True
except Exception:
pass
# Build Google OAuth URL
base_url = "https://accounts.google.com/o/oauth2/v2/auth"
params = {
'client_id': GOOGLE_CLIENT_ID,
'redirect_uri': GOOGLE_REDIRECT_URI,
'response_type': 'code',
'scope': 'openid email profile',
'access_type': 'offline',
'prompt': 'select_account',
'state': state
}
auth_url = f"{base_url}?{'&'.join([f'{k}={v}' for k, v in params.items()])}"
print(f"đ Redirecting to Google OAuth")
return redirect(auth_url)
except Exception as e:
print(f"â Auth error: {e}")
return redirect('/login?error=auth_failed')
@server.route('/auth/callback')
def auth_callback():
"""Handle callback dari Google OAuth - SELALU redirect ke complete profile"""
try:
print("đĩ Processing Google OAuth callback...")
# Dump session and args for debugging session persistence
try:
print("đ Session before processing:", dict(session))
except Exception:
print("đ Session not printable or empty")
try:
print("đ Callback args:", dict(request.args))
except Exception:
pass
# Verify state - allow if the state matches session or is present in the in-memory store
state = request.args.get('state')
sess_state = session.get('oauth_state')
if not state:
print("â Missing state parameter")
return redirect('/login?error=invalid_state')
if state != sess_state and state not in OAUTH_STATE_STORE:
print("â Invalid state parameter")
return redirect('/login?error=invalid_state')
# Get authorization code
code = request.args.get('code')
if not code:
print("â No authorization code")
return redirect('/login?error=no_code')
# Token exchange dengan Google
token_url = "https://oauth2.googleapis.com/token"
token_data = {
'client_id': GOOGLE_CLIENT_ID,
'client_secret': GOOGLE_CLIENT_SECRET,
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': GOOGLE_REDIRECT_URI
}
print("đ Exchanging code for token...")
token_response = requests.post(token_url, data=token_data)
token_json = token_response.json()
if 'error' in token_json:
print(f"â Token exchange error: {token_json}")
return redirect('/login?error=token_failed')
access_token = token_json.get('access_token')
print(f"â
Token received successfully")
# Get user info dari Google
userinfo_url = "https://www.googleapis.com/oauth2/v3/userinfo"
headers = {'Authorization': f'Bearer {access_token}'}
userinfo_response = requests.get(userinfo_url, headers=headers)
userinfo = userinfo_response.json()
print(f"â
User info received: {userinfo['email']}")
# SELALU redirect ke complete profile, bahkan jika user sudah ada
oauth_user_obj = {
'id': userinfo['sub'],
'email': userinfo['email'],
'name': userinfo.get('name', userinfo['email'].split('@')[0]),
'picture': userinfo.get('picture', ''),
'auth_provider': 'google'
}
# Store oauth_user in the in-memory fallback store keyed by state
try:
if state in OAUTH_STATE_STORE and isinstance(OAUTH_STATE_STORE[state], dict):
OAUTH_STATE_STORE[state]['oauth_user'] = oauth_user_obj
else:
OAUTH_STATE_STORE[state] = {'action': None, 'oauth_user': oauth_user_obj}
print(f"đ Stored oauth_user in OAUTH_STATE_STORE for state {state}")
except Exception as e:
print(f"â ī¸ Failed to store oauth_user in OAUTH_STATE_STORE: {e}")
# Also set in session if possible
try:
session['oauth_user'] = oauth_user_obj
session.permanent = True
except Exception:
pass
# Decide behavior based on action stored for this state ('signup' or 'login')
try:
stored = OAUTH_STATE_STORE.get(state, {}) if state else {}
action = stored.get('action') if isinstance(stored, dict) else None
users = sibal_data._get_table_data('users')
existing = next((u for u in users if u.get('email') == userinfo.get('email')), None)
if action == 'signup':
# create user if not exists, but DO NOT login automatically
if existing:
print(f"đ User already exists for signup email: {existing.get('email')}")
else:
uname = (userinfo.get('email') or '').split('@')[0]
user_payload = {
'email': userinfo.get('email'),
'username': uname,
'name': userinfo.get('name', uname),
'auth_provider': 'google'
}
created = sibal_data._insert_table_data('users', user_payload)
if created:
print(f"â
Created new user (signup) in DB for {created.get('email')}")
# Per-request: do NOT initialize template data on signup; leave data empty
else:
print("â Failed to create user record in DB during signup flow")
# cleanup and redirect to login page with flag
try:
OAUTH_STATE_STORE.pop(state, None)
except Exception:
pass
try:
session.pop('oauth_user', None)
except Exception:
pass
return redirect('/login?registered=google')
else:
# Default/login flow: if user exists, login; if not, redirect to complete-profile
if existing:
try:
login_user(existing, remember=True)
try:
OAUTH_STATE_STORE.pop(state, None)
except Exception:
pass
try:
session.pop('oauth_user', None)
except Exception:
pass
print(f"đ Logged in user via Google: {existing.get('email')}")
return redirect('/')
except Exception as e:
print(f"â Failed to login user after Google callback: {e}")
return redirect('/login?error=login_failed')
else:
# No existing user: require complete profile (username/password)
print("âšī¸ No existing user for this Google account; redirecting to complete-profile to create account")
# ensure oauth_user is in store for recovery
if state and isinstance(OAUTH_STATE_STORE.get(state), dict):
OAUTH_STATE_STORE[state]['oauth_user'] = oauth_user_obj
return redirect(f'/complete-profile?state={state}')
except Exception as e:
print(f"â Error creating/fetching user after OAuth: {e}")
import traceback
traceback.print_exc()
return redirect('/login?error=callback_failed')
except Exception as e:
print(f"â Callback error: {e}")
import traceback
traceback.print_exc()
return redirect('/login?error=callback_failed')
@server.route('/auth/logout')
def auth_logout():
"""Logout user"""
global current_user
current_user = SimpleUser()
# Clear active user from data layer
try:
sibal_data.set_active_user(None)
except Exception:
pass
session.clear()
return redirect('/login')
@server.route('/complete-profile')
def complete_profile_page():
"""Route untuk halaman complete profile"""
return redirect('/') # Dash akan handle rendering
# Halaman verifikasi OTP dan callback
def verify_otp_layout():
return html.Div([
html.Div([
html.H1(
"đ§ Verifikasi Email",
style={'textAlign': 'center', 'color': COLORS['primary']}
),
html.P(
"Masukkan kode verifikasi yang dikirim ke email Anda.",
style={'textAlign': 'center', 'color': COLORS['gray_600']}
),
html.Div([
html.Label("Kode OTP", className="form-label"),
dcc.Input(
id='input-otp',
type='text',
placeholder='Masukkan kode 6 digit...',
className='form-input'
)
], className='form-group'),
html.Div([
html.Button(
'â
Verifikasi',
id='btn-verify-otp',
className='btn btn-primary',
style={'width': '48%', 'marginRight': '4%'}
),
html.Button(
'đ Kirim Ulang',
id='btn-resend-otp',
className='btn btn-secondary',
style={'width': '48%'}
),
], style={'display': 'flex', 'gap': '8px'}),
html.Div(id='verify-alert', style={'marginTop': '20px'})
], style={
'padding': '40px',
'maxWidth': '480px',
'margin': '80px auto',
'backgroundColor': 'white',
'borderRadius': '12px',
'boxShadow': '0 10px 25px rgba(0,0,0,0.08)'
})
], style={
'backgroundColor': COLORS['gray_50'],
'minHeight': '100vh',
'padding': '20px'
})
@app.callback(
[Output('verify-alert', 'children'), Output('input-otp', 'value')],
[Input('btn-verify-otp', 'n_clicks'), Input('btn-resend-otp', 'n_clicks')],
[State('input-otp', 'value')],
prevent_initial_call=True
)
def handle_verify_otp(n_verify, n_resend, otp_value):
ctx = dash.callback_context
if not ctx.triggered:
return dash.no_update, dash.no_update
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
pending = None
try:
pending = session.get('pending_verification')
except Exception:
pending = None
if not pending:
return create_alert('Tidak ada permintaan verifikasi yang sedang berjalan.', 'error'), ''
# Resend OTP
if button_id == 'btn-resend-otp' and n_resend:
otp_code = str(random.randint(100000, 999999))
expires_at = (datetime.now() + timedelta(minutes=10)).isoformat()
try:
session['pending_verification']['otp'] = otp_code
session['pending_verification']['expires_at'] = expires_at
except Exception as e:
print(f"Failed to update session for resend: {e}")
sent = send_otp_email(pending.get('email'), otp_code)
if sent:
return create_alert('Kode OTP telah dikirim ulang ke email Anda.', 'success'), ''
else:
return create_alert('(Catatan: SMTP tidak dikonfigurasi - OTP dicetak di konsol).', 'info'), ''
# Verify OTP
if button_id == 'btn-verify-otp' and n_verify:
if not otp_value:
return create_alert('Masukkan kode OTP.', 'error'), ''
# Check expiry
try:
expires_at = datetime.fromisoformat(pending.get('expires_at'))
if datetime.now() > expires_at:
return create_alert('Kode OTP kedaluwarsa. Silakan kirim ulang.', 'error'), ''
except Exception:
pass
if str(pending.get('otp')) == str(otp_value).strip():
# Try to update DB flag
try:
user_id = pending.get('user_id')
if user_id and supabase_client:
supabase_client.table('users').update({'otp_verified': True}).eq('id', user_id).execute()
except Exception as e:
print(f"Failed to update user verification: {e}")
try:
session.pop('pending_verification', None)
except Exception:
pass
return html.Div([html.P('â
Verifikasi berhasil! Silakan login.', style={'color': COLORS['success']}), dcc.Location(href='/login', id='redirect-after-verify', refresh=True)]), ''
else:
return create_alert('Kode OTP salah. Coba lagi.', 'error'), ''
return dash.no_update, dash.no_update
# Layout utama
app.layout = html.Div([
dcc.Location(id='url', refresh=False),
html.Div(id='top-navigation'),
html.Div(id='sub-navigation'),
html.Div(id='page-content')
])
@app.callback(
[Output('top-navigation', 'children'),
Output('page-content', 'children'),
Output('sub-navigation', 'children')],
Input('url', 'pathname')
)
def display_page(pathname):
print(f"đ Navigating to: {pathname}")
try:
print("đ Current session keys:", list(session.keys()))
except Exception:
pass
# Update top navigation
top_nav = create_top_navigation()
# Check authentication
is_authenticated = current_user.is_authenticated
# Handle complete-profile route
if pathname == '/complete-profile':
# Try to recover oauth_user from query state first (fallback store), then session
try:
state = request.args.get('state')
except Exception:
state = None
if state and state in OAUTH_STATE_STORE:
# move oauth_user into session for the Dash rendering code
try:
session['oauth_user'] = OAUTH_STATE_STORE.pop(state)
session.permanent = True
print(f"đ Restored oauth_user from OAUTH_STATE_STORE for state {state}")
except Exception:
pass
if session.get('oauth_user'):
return top_nav, complete_profile_layout(), None
else:
return top_nav, login_layout(), None
# Always allow access to login/signup/verification pages
if pathname in ['/login', '/signup', '/verify-otp']:
if pathname == '/login':
return top_nav, login_layout(), None
elif pathname == '/signup':
return top_nav, signup_layout(), None
else:
return top_nav, verify_otp_layout(), None
# Redirect to login if not authenticated
if not is_authenticated:
print("đ User not authenticated, redirecting to login")
return top_nav, login_layout(), None
# User is authenticated, show requested page
sub_nav = None
akuntansi_pages = [
'/akuntansi-dasar', '/transaksi', '/kartu-persediaan', '/jurnal-umum',
'/buku-besar', '/buku-besar-pembantu', '/neraca-saldo', '/aset-tetap'
]
laporan_pages = [
'/laporan-analisis', '/jurnal-penyesuaian', '/neraca-setelah-penyesuaian',
'/laporan-laba-rugi', '/laporan-keuangan'
]
if pathname in akuntansi_pages:
sub_nav = create_akuntansi_sub_nav()
elif pathname in laporan_pages:
sub_nav = create_laporan_sub_nav()
# Tentukan konten berdasarkan pathname
if pathname == '/akuntansi-dasar':
content = html.Div([
html.H1("đ Akuntansi Dasar", style={'textAlign': 'center', 'marginBottom': '30px'}),
html.P("Pilih menu dari sub-navigation di atas", style={'textAlign': 'center'})
])
elif pathname == '/laporan-analisis':
content = html.Div([
html.H1("đ Laporan & Analisis", style={'textAlign': 'center', 'marginBottom': '30px'}),
html.P("Pilih menu dari sub-navigation di atas", style={'textAlign': 'center'})
])
elif pathname == '/transaksi':
content = transaksi_layout()
elif pathname == '/kartu-persediaan':
content = kartu_persediaan_layout()
elif pathname == '/jurnal-umum':
content = jurnal_umum_layout()
elif pathname == '/buku-besar':
content = buku_besar_layout()
elif pathname == '/buku-besar-pembantu':
content = buku_besar_pembantu_layout() # Pastikan ini yang dipanggil
elif pathname == '/neraca-saldo':
content = neraca_saldo_layout()
elif pathname == '/aset-tetap':
content = aset_tetap_layout()
elif pathname == '/jurnal-penyesuaian':
content = jurnal_penyesuaian_layout()
elif pathname == '/neraca-setelah-penyesuaian':
content = neraca_setelah_penyesuaian_layout()
elif pathname == '/laporan-laba-rugi':
content = laporan_laba_rugi_layout()
elif pathname == '/laporan-keuangan':
content = laporan_keuangan_layout()
else:
content = dashboard_layout()
sub_nav = None
return top_nav, content, sub_nav
# Callback untuk login
@app.callback(
[Output('login-alert', 'children'),
Output('login-identifier', 'value'),
Output('login-password', 'value')],
[Input('btn-login', 'n_clicks'),
Input('btn-google-login', 'n_clicks')],
[State('login-identifier', 'value'),
State('login-password', 'value')],
prevent_initial_call=True
)
def handle_login(n_login, n_google, identifier, password):
ctx = dash.callback_context
if not ctx.triggered:
return dash.no_update, dash.no_update, dash.no_update
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
if button_id == 'btn-login' and n_login:
if not identifier or not password:
return create_alert("Harap isi username dan password", "error"), dash.no_update, dash.no_update
# GUNAKAN FUNGSI authenticate_user YANG SUDAH KITA BUAT
user, message = authenticate_user(identifier, password)
if user:
# GUNAKAN FUNGSI login_user YANG SUDAH KITA BUAT
login_user(user, remember=True)
return html.Div([
html.P("â
Login berhasil! Redirecting...", style={'color': COLORS['success']}),
dcc.Location(href='/', id='redirect-dashboard', refresh=True)
], style={'textAlign': 'center'}), "", ""
else:
return create_alert(message, "error"), dash.no_update, ""
elif button_id == 'btn-google-login' and n_google:
# Untuk Google OAuth, kita perlu redirect ke Flask route
return html.Div([
html.P("Mengarahkan ke Google...", style={'color': COLORS['info']}),
dcc.Location(id='google-redirect', href='/auth/login?action=login', refresh=True)
]), dash.no_update, dash.no_update
return dash.no_update, dash.no_update, dash.no_update
# Callback untuk signup
@app.callback(
[Output('signup-alert', 'children'),
Output('signup-username', 'value'),
Output('signup-email', 'value'),
Output('signup-password', 'value'),
Output('signup-confirm-password', 'value')],
[Input('btn-signup', 'n_clicks'),
Input('btn-google-signup', 'n_clicks')],
[State('signup-username', 'value'),
State('signup-email', 'value'),
State('signup-password', 'value'),
State('signup-confirm-password', 'value')],
prevent_initial_call=True
)
def handle_signup(n_signup, n_google, username, email, password, confirm_password):
ctx = dash.callback_context
if not ctx.triggered:
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
if button_id == 'btn-signup' and n_signup:
if not all([username, email, password, confirm_password]):
return create_alert("Harap isi semua field", "error"), dash.no_update, dash.no_update, dash.no_update, dash.no_update
if password != confirm_password:
return create_alert("Password dan konfirmasi password tidak cocok", "error"), dash.no_update, dash.no_update, "", ""
if len(password) < 6:
return create_alert("Password minimal 6 karakter", "error"), dash.no_update, dash.no_update, "", ""
# GUNAKAN FUNGSI create_user YANG SUDAH KITA BUAT
user, message = create_user(username, email, password)
if user:
# Buat OTP dan simpan ke session untuk verifikasi
otp_code = str(random.randint(100000, 999999))
expires_at = (datetime.now() + timedelta(minutes=10)).isoformat()
try:
session['pending_verification'] = {
'user_id': user.get('id'),
'email': user.get('email'),
'username': user.get('username'),
'otp': otp_code,
'expires_at': expires_at
}
session.permanent = True
except Exception as e:
print(f"Failed to save pending verification in session: {e}")
# Kirim email OTP (fungsi send_otp_email ada di file ini)
sent = send_otp_email(user.get('email'), otp_code)
# Simpan OTP ke Supabase pada kolom users. Jika kolom tidak ada, ignore.
try:
uid = user.get('id')
if uid and supabase_client:
upd = {}
upd['otp'] = int(otp_code)
upd['otp_expires_at'] = expires_at
upd['otp_verified'] = False
try:
supabase_client.table('users').update(upd).eq('id', uid).execute()
except Exception as e:
# jika kolom tidak ada, coba hanya simpan otp
try:
supabase_client.table('users').update({'otp': otp_code}).eq('id', uid).execute()
except Exception:
print(f"[OTP] Warning: failed to save OTP to users table: {e}")
except Exception as e:
print(f"[OTP] Warning saving OTP to DB: {e}")
info_msg = "Kode verifikasi telah dikirim ke email Anda. Periksa inbox (atau folder spam)."
if not sent:
info_msg = "(Catatan: SMTP tidak dikonfigurasi. OTP dicetak ke konsol saat pengembangan.)"
# Redirect ke halaman verifikasi OTP
return html.Div([
html.P(f"â
Pendaftaran berhasil! {info_msg}", style={'color': COLORS['success']}),
dcc.Location(href='/verify-otp', id='redirect-verify-otp', refresh=True)
], style={'textAlign': 'center'}), "", "", "", ""
else:
return create_alert(message, "error"), dash.no_update, dash.no_update, "", ""
elif button_id == 'btn-google-signup' and n_google:
# Untuk Google OAuth, redirect ke Flask route
return html.Div([
html.P("Mengarahkan ke Google...", style={'color': COLORS['info']}),
dcc.Location(id='google-signup-redirect', href='/auth/login?action=signup', refresh=True)
]), dash.no_update, dash.no_update, dash.no_update, dash.no_update
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
def create_alert(message, alert_type):
colors = {
'success': COLORS['success'],
'error': COLORS['error'],
'warning': COLORS['warning'],
'info': COLORS['info']
}
return html.Div([
html.P(message, style={
'color': colors.get(alert_type, COLORS['gray_600']),
'padding': '10px',
'borderRadius': '5px',
'backgroundColor': f"{colors.get(alert_type, COLORS['gray_200'])}20",
'border': f"1px solid {colors.get(alert_type, COLORS['gray_300'])}"
})
])
def send_otp_email(to_email: str, otp_code: str) -> bool:
smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
smtp_port = int(os.getenv("SMTP_PORT", "587"))
smtp_user = os.getenv("SMTP_USER")
smtp_pass = os.getenv("SMTP_PASS")
sender_name = os.getenv("SMTP_SENDER_NAME", "SIBAL")
print("[SMTP DEBUG] host:", smtp_host, "port:", smtp_port, "user:", smtp_user)
if not smtp_user or not smtp_pass:
print("[OTP] SMTP not configured")
return False
try:
msg = EmailMessage()
msg["Subject"] = "SIBAL - Kode Verifikasi (OTP)"
msg["From"] = f"{sender_name} <{smtp_user}>"
msg["To"] = to_email
msg.set_content(
f"Halo,\n\nKode OTP kamu: {otp_code}\n"
f"Berlaku 10 menit.\n\nSalam,\n{sender_name}"
)
context = ssl.create_default_context()
with smtplib.SMTP(smtp_host, smtp_port, timeout=20) as server:
server.ehlo()
server.starttls(context=context)
server.ehlo()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
print(f"â
Sent OTP to {to_email}")
return True
except Exception as e:
print("â Failed to send OTP email:", repr(e))
return False
otp_code = str(random.randint(100000, 999999))
expires_at = (datetime.now() + timedelta(minutes=10)).isoformat()
def handle_signup(n_clicks, username, email, password, confirm_password):
if not n_clicks:
return dash.no_update
# 1) Insert user dulu
insert_res = supabase_client.table("users").insert({
"email": email,
"username": username,
"name": username,
"password_hash": hash_password(password),
"auth_provider": "local"
}).execute()
user_id = insert_res.data[0]["id"]
# 2) Generate OTP
otp_code = str(random.randint(100000, 999999))
expires_at = (datetime.now() + timedelta(minutes=10)).isoformat()
# 3) Simpan OTP ke user itu
supabase_client.table("users").update({
"otp": int(otp_code),
"otp_verified": False,
"otp_expires_at": expires_at
}).eq("id", user_id).execute()
# 4) Kirim OTP ke Gmail user
sent = send_otp_email(email, otp_code)
if sent:
session["pending_verification"] = {
"email": email,
"otp": otp_code,
"expires_at": expires_at,
"user_id": user_id
}
return create_alert("OTP sudah dikirim ke email kamu. Cek inbox/spam.", "success")
else:
return create_alert("Gagal kirim OTP. Periksa konfigurasi SMTP.", "error")
@app.callback(
Output('pemasukan-jumlah-display', 'children'),
[Input('pemasukan-harga-jual', 'value'),
Input('pemasukan-qty', 'value')]
)
def hitung_jumlah_pemasukan(harga_jual, qty):
"""Menghitung jumlah otomatis dari harga jual à quantity"""
if harga_jual and qty:
jumlah = harga_jual * qty
return html.Div([
html.Strong(f"Total Jumlah: Rp {jumlah:,.0f}",
style={'color': COLORS['success'], 'fontSize': '1.1rem'})
])
return html.Div("Total Jumlah: Rp 0", style={'color': COLORS['gray_500']})
# Tambahkan ini di form pemasukan (setelah input quantity):
html.Div(id='pemasukan-jumlah-display', style={'marginBottom': '15px'})
@app.callback(
[Output('pemasukan-harga-jual', 'value'),
Output('pemasukan-qty', 'value'),
Output('pemasukan-hpp', 'value'),
Output('pemasukan-keterangan', 'value'),
Output('pemasukan-ref', 'value')],
Input('btn-tambah-pemasukan', 'n_clicks'),
[State('selected-date', 'date'),
State('pemasukan-jenis', 'value'),
State('pemasukan-harga-jual', 'value'),
State('pemasukan-qty', 'value'),
State('pemasukan-hpp', 'value'),
State('pemasukan-keterangan', 'value'),
State('pemasukan-ref', 'value')],
prevent_initial_call=True
)
def tambah_pemasukan(n_clicks, tanggal, jenis, harga_jual, qty, hpp, keterangan, ref):
if n_clicks and n_clicks > 0 and harga_jual and qty and harga_jual > 0 and qty > 0:
print(f"â Adding new pemasukan: {jenis} - {harga_jual} Ã {qty}")
# HITUNG JUMLAH OTOMATIS
jumlah = harga_jual * qty
# Tentukan kode akun
if jenis == 'tiket_masuk':
kode_debit = KODE_AKUN['kas']['kode']
kode_kredit = KODE_AKUN['pendapatan_tiket']['kode']
elif jenis == 'penjualan_ikan':
kode_debit = KODE_AKUN['kas']['kode']
kode_kredit = KODE_AKUN['pendapatan']['kode']
else: # modal
kode_debit = KODE_AKUN['kas']['kode']
kode_kredit = KODE_AKUN['modal']['kode']
# Buat transaksi baru
transaksi_baru = {
'tanggal': tanggal,
'jenis': jenis,
'jumlah': jumlah, # Pakai jumlah yang dihitung otomatis
'harga_jual': harga_jual, # Simpan harga jual per unit
'quantity': qty,
'hpp': hpp if hpp else 0,
'keterangan': keterangan if keterangan else f'Pendapatan {jenis}',
'ref': ref if ref else f"REF-P-{datetime.now().strftime('%Y%m%d%H%M%S')}",
'tipe': 'pemasukan',
'kode_akun_debit': kode_debit,
'kode_akun_kredit': kode_kredit
}
# Simpan ke memory (scoped ke user)
transaksi_baru['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_transaksi_pemasukan.append(transaksi_baru)
# â
SIMPAN KE DATABASE
try:
sibal_data.save_all_data()
print(f"â
Pemasukan saved to database: {jenis} - {qty} Ã {harga_jual} = Rp{jumlah:,}")
except Exception as e:
print(f"â Failed to save pemasukan: {e}")
return '', '', '', '', ''
return dash.no_update
def tambah_pengeluaran(n_clicks, tanggal, jenis, kode_barang, hpp, qty, metode, supplier, keterangan, ref):
if n_clicks and n_clicks > 0 and hpp and qty and hpp > 0 and qty > 0 and jenis:
print(f"â Adding new pengeluaran: {jenis} - {hpp} Ã {qty}")
# HITUNG JUMLAH OTOMATIS
jumlah = hpp * qty
# Default values
metode = metode if metode else 'tunai'
supplier = supplier if supplier else ''
# Tentukan kode akun
akun_debit_mapping = {
'pembelian_persediaan': KODE_AKUN['persediaan']['kode'],
'beban_gaji': KODE_AKUN['beban_gaji']['kode'],
'beban_listrik': KODE_AKUN['beban_listrik']['kode'],
'beban_penyusutan_bangunan': KODE_AKUN['beban_penyusutan_bangunan']['kode'],
'beban_penyusutan_kendaraan': KODE_AKUN['beban_penyusutan_kendaraan']['kode'],
'beban_penyusutan_peralatan': KODE_AKUN['beban_penyusutan_peralatan']['kode'],
'beban_lainnya': KODE_AKUN['beban_lainnya']['kode']
}
kode_akun_debit = akun_debit_mapping.get(jenis, KODE_AKUN['beban_lainnya']['kode'])
kode_akun_kredit = KODE_AKUN['kas']['kode'] if metode == 'tunai' else KODE_AKUN['utang']['kode']
# â
UPDATE PERSEDIAAN JIKA PEMBELIAN PERSEDIAAN
if jenis == 'pembelian_persediaan' and kode_barang:
success = update_persediaan_pembelian(
kode_barang, qty, hpp, tanggal, keterangan, supplier
)
if success:
print(f"â
Persediaan updated for pembelian: {kode_barang}")
else:
print(f"â Failed to update persediaan for pembelian")
# â
TAMBAHKAN SUPPLIER KE DAFTAR JIKA KREDIT
if metode == 'kredit' and supplier and supplier not in sibal_data.suppliers:
sibal_data.suppliers.append(supplier)
print(f"â
Supplier added: {supplier}")
# Buat transaksi baru
transaksi_baru = {
'tanggal': tanggal,
'jenis': jenis,
'kode_barang': kode_barang if jenis == 'pembelian_persediaan' else '',
'jumlah': jumlah,
'hpp': hpp,
'quantity': qty,
'metode_bayar': metode,
'supplier': supplier if metode == 'kredit' else '',
'keterangan': keterangan if keterangan else f'Pengeluaran - {jenis}',
'ref': ref if ref else f"REF-K-{datetime.now().strftime('%Y%m%d%H%M%S')}",
'tipe': 'pengeluaran',
'kode_akun_debit': kode_akun_debit,
'kode_akun_kredit': kode_akun_kredit
}
# Simpan ke memory (scoped ke user)
transaksi_baru['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_transaksi_pengeluaran.append(transaksi_baru)
# â
SIMPAN KE DATABASE
try:
sibal_data.save_all_data()
print(f"â
Pengeluaran saved to database: {jenis} - {qty} Ã {hpp} = Rp{jumlah:,}")
except Exception as e:
print(f"â Failed to save pengeluaran: {e}")
return '', '', '', '', ''
return dash.no_update
@app.callback(
Output('pengeluaran-jumlah-display', 'children'),
[Input('pengeluaran-hpp', 'value'),
Input('pengeluaran-qty', 'value')]
)
def hitung_jumlah_pengeluaran(hpp, qty):
"""Menghitung jumlah otomatis dari HPP Ã quantity"""
if hpp and qty:
jumlah = hpp * qty
return html.Div([
html.Strong(f"Total Jumlah: Rp {jumlah:,.0f}",
style={'color': COLORS['error'], 'fontSize': '1.1rem'})
])
return html.Div("Total Jumlah: Rp 0", style={'color': COLORS['gray_500']})
# Tambahkan ini di form pengeluaran (setelah input quantity):
html.Div(id='pengeluaran-jumlah-display', style={'marginBottom': '15px'})
@app.callback(
[Output('pengeluaran-hpp', 'value'),
Output('pengeluaran-qty', 'value'),
Output('pengeluaran-keterangan', 'value'),
Output('pengeluaran-supplier', 'value'),
Output('pengeluaran-ref', 'value')],
Input('btn-tambah-pengeluaran', 'n_clicks'), # PASTIKAN ID INI ADA DI LAYOUT
[State('selected-date', 'date'),
State('pengeluaran-jenis', 'value'),
State('pengeluaran-barang', 'value'),
State('pengeluaran-hpp', 'value'),
State('pengeluaran-qty', 'value'),
State('pengeluaran-metode', 'value'),
State('pengeluaran-supplier', 'value'),
State('pengeluaran-keterangan', 'value'),
State('pengeluaran-ref', 'value')],
prevent_initial_call=True
)
def tambah_pengeluaran(n_clicks, tanggal, jenis, kode_barang, hpp, qty, metode, supplier, keterangan, ref):
if n_clicks and n_clicks > 0 and hpp and qty and hpp > 0 and qty > 0 and jenis:
print(f"â Adding new pengeluaran: {jenis} - {hpp} Ã {qty}")
# HITUNG JUMLAH OTOMATIS
jumlah = hpp * qty
# Default values
metode = metode if metode else 'tunai'
# Tentukan kode akun
akun_debit_mapping = {
'pembelian_persediaan': {'nama': 'Persediaan Barang Dagang', 'kode': KODE_AKUN['persediaan']['kode']},
'beban_gaji': {'nama': 'Beban Gaji', 'kode': KODE_AKUN['beban_gaji']['kode']},
'beban_listrik': {'nama': 'Beban Listrik', 'kode': KODE_AKUN['beban_listrik']['kode']},
'beban_penyusutan_bangunan': {'nama': 'Beban Depresiasi Bangunan', 'kode': KODE_AKUN['beban_penyusutan_bangunan']['kode']},
'beban_penyusutan_kendaraan': {'nama': 'Beban Depresiasi Kendaraan', 'kode': KODE_AKUN['beban_penyusutan_kendaraan']['kode']},
'beban_penyusutan_peralatan': {'nama': 'Beban Depresiasi Peralatan', 'kode': KODE_AKUN['beban_penyusutan_peralatan']['kode']},
'beban_lainnya': {'nama': 'Beban Lainnya', 'kode': KODE_AKUN['beban_lainnya']['kode']}
}
kode_akun_debit = akun_debit_mapping.get(jenis, {'nama': 'Beban Lainnya', 'kode': KODE_AKUN['beban_lainnya']['kode']})
kode_akun_kredit = KODE_AKUN['kas']['kode'] if metode == 'tunai' else KODE_AKUN['utang']['kode']
# â
UPDATE PERSEDIAAN JIKA PEMBELIAN PERSEDIAAN
if jenis == 'pembelian_persediaan' and kode_barang:
print(f"đ Memanggil update_persediaan_pembelian: {kode_barang}, {qty}, {hpp}")
success = update_persediaan_pembelian(
kode_barang, qty, hpp, tanggal, keterangan, supplier
)
if success:
print(f"â
Persediaan updated for pembelian: {kode_barang}")
else:
print(f"â Failed to update persediaan for pembelian")
else:
print(f"âšī¸ Bukan pembelian persediaan, skip update persediaan")
# â
TAMBAHKAN SUPPLIER KE DAFTAR JIKA KREDIT
if metode == 'kredit' and supplier and supplier not in sibal_data.suppliers:
sibal_data.suppliers.append(supplier)
print(f"â
Supplier added: {supplier}")
# Inisialisasi buku besar pembantu untuk supplier baru
if supplier not in sibal_data.buku_besar_pembantu:
sibal_data.buku_besar_pembantu[supplier] = []
# Buat transaksi baru
transaksi_baru = {
'tanggal': tanggal,
'jenis': jenis,
'kode_barang': kode_barang if jenis == 'pembelian_persediaan' else '',
'jumlah': jumlah,
'hpp': hpp,
'quantity': qty,
'metode_bayar': metode,
'supplier': supplier if metode == 'kredit' else '',
'keterangan': keterangan if keterangan else f'Pengeluaran - {jenis}',
'ref': ref if ref else f"REF-K-{datetime.now().strftime('%Y%m%d%H%M%S')}",
'tipe': 'pengeluaran',
'kode_akun_debit': kode_akun_debit['kode'],
'kode_akun_kredit': kode_akun_kredit
}
# Simpan ke memory (scoped ke user)
transaksi_baru['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_transaksi_pengeluaran.append(transaksi_baru)
# â
TAMBAHKAN KE BUKU BESAR PEMBANTU JIKA KREDIT
if metode == 'kredit' and supplier:
if supplier not in sibal_data.buku_besar_pembantu:
sibal_data.buku_besar_pembantu[supplier] = []
# Hitung saldo terakhir
saldo_terakhir = 0
if sibal_data.buku_besar_pembantu[supplier]:
saldo_terakhir = sibal_data.buku_besar_pembantu[supplier][-1]['saldo']
saldo_baru = saldo_terakhir + jumlah
# Tambahkan transaksi ke buku besar pembantu
sibal_data.buku_besar_pembantu[supplier].append({
'tanggal': tanggal,
'keterangan': f"{jenis} - {keterangan}",
'debit': 0,
'kredit': jumlah,
'saldo': saldo_baru
})
print(f"â
Added to buku besar pembantu: {supplier} - Rp {jumlah:,}")
# â
SIMPAN KE DATABASE
try:
sibal_data.save_all_data()
print(f"â
Pengeluaran saved to database: {jenis} - {qty} Ã {hpp} = Rp{jumlah:,}")
except Exception as e:
print(f"â Failed to save pengeluaran: {e}")
return '', '', '', '', ''
return dash.no_update
@app.callback(
Output('daftar-pemasukan', 'children', allow_duplicate=True),
Input('btn-hapus-pemasukan', 'n_clicks'),
[State('selected-date', 'date'),
State({'type': 'pemasukan-check', 'index': dash.ALL}, 'value')],
prevent_initial_call=True
)
def hapus_pemasukan(n_clicks, tanggal, checklists):
if n_clicks and n_clicks > 0:
transaksi_hari_ini = [t for t in sibal_data.transaksi_pemasukan if t['tanggal'] == tanggal]
indices_to_delete = []
for i, checked in enumerate(checklists):
if checked and 'selected' in checked and i < len(transaksi_hari_ini):
indices_to_delete.append(i)
if indices_to_delete:
# Hapus dari belakang untuk menghindari index error
deleted_count = 0
for i in sorted(indices_to_delete, reverse=True):
if i < len(transaksi_hari_ini):
transaksi_to_delete = transaksi_hari_ini[i]
sibal_data.transaksi_pemasukan.remove(transaksi_to_delete)
deleted_count += 1
sibal_data.save_all_data()
return update_daftar_transaksi(tanggal, None, None, None, None)[0]
return dash.no_update
@app.callback(
Output('daftar-pengeluaran', 'children', allow_duplicate=True),
Input('btn-hapus-pengeluaran', 'n_clicks'),
[State('selected-date', 'date'),
State({'type': 'pengeluaran-check', 'index': dash.ALL}, 'value')],
prevent_initial_call=True
)
def hapus_pengeluaran(n_clicks, tanggal, checklists):
if n_clicks and n_clicks > 0:
transaksi_hari_ini = [t for t in sibal_data.transaksi_pengeluaran if t['tanggal'] == tanggal]
indices_to_delete = []
for i, checked in enumerate(checklists):
if checked and 'selected' in checked and i < len(transaksi_hari_ini):
indices_to_delete.append(i)
if indices_to_delete:
# Hapus dari belakang untuk menghindari index error
deleted_count = 0
for i in sorted(indices_to_delete, reverse=True):
if i < len(transaksi_hari_ini):
transaksi_to_delete = transaksi_hari_ini[i]
sibal_data.transaksi_pengeluaran.remove(transaksi_to_delete)
deleted_count += 1
sibal_data.save_all_data()
return update_daftar_transaksi(tanggal, None, None, None, None)[1]
return dash.no_update
@app.callback(
[Output('daftar-pemasukan', 'children'),
Output('daftar-pengeluaran', 'children')],
[Input('selected-date', 'date'),
Input('btn-tambah-pemasukan', 'n_clicks'),
Input('btn-tambah-pengeluaran', 'n_clicks'),
Input('btn-hapus-pemasukan', 'n_clicks'),
Input('btn-hapus-pengeluaran', 'n_clicks')]
)
def update_daftar_transaksi(tanggal, n_pemasukan, n_pengeluaran, n_hapus_pemasukan, n_hapus_pengeluaran):
"""Update daftar transaksi untuk tanggal terpilih"""
# Tampilkan transaksi pemasukan untuk tanggal terpilih
transaksi_pemasukan_hari_ini = [t for t in sibal_data.transaksi_pemasukan if t['tanggal'] == tanggal]
if transaksi_pemasukan_hari_ini:
items_pemasukan = []
for i, trans in enumerate(transaksi_pemasukan_hari_ini):
harga_jual = trans.get('harga_jual', 0)
quantity = trans.get('quantity', 0)
jumlah = trans.get('jumlah', 0)
items_pemasukan.append(html.Div([
dcc.Checklist(
id={'type': 'pemasukan-check', 'index': i},
options=[{'label': '', 'value': 'selected'}],
value=[],
style={'display': 'inline-block', 'marginRight': '10px'}
),
html.Strong(f"{trans.get('jenis', 'N/A').replace('_', ' ').title()}"),
html.Br(),
html.Small(f"Qty: {quantity} Ã Rp {harga_jual:,} = Rp {jumlah:,}"),
html.Br(),
html.Small(f"Ref: {trans.get('ref', '-')} | {trans.get('keterangan', '')}"),
html.Hr(style={'margin': '5px 0'})
]))
daftar_pemasukan = items_pemasukan
else:
daftar_pemasukan = html.P("Belum ada transaksi pendapatan untuk tanggal ini", style={'color': '#6c757d'})
# Tampilkan transaksi pengeluaran untuk tanggal terpilih
transaksi_pengeluaran_hari_ini = [t for t in sibal_data.transaksi_pengeluaran if t['tanggal'] == tanggal]
if transaksi_pengeluaran_hari_ini:
items_pengeluaran = []
for i, trans in enumerate(transaksi_pengeluaran_hari_ini):
hpp = trans.get('hpp', 0)
quantity = trans.get('quantity', 0)
jumlah = trans.get('jumlah', 0)
jenis_label = trans['jenis'].replace('_', ' ').title()
metode = trans.get('metode_bayar', 'tunai')
items_pengeluaran.append(html.Div([
dcc.Checklist(
id={'type': 'pengeluaran-check', 'index': i},
options=[{'label': '', 'value': 'selected'}],
value=[],
style={'display': 'inline-block', 'marginRight': '10px'}
),
html.Strong(f"{jenis_label}"),
html.Br(),
html.Small(f"Qty: {quantity} Ã Rp {hpp:,} = Rp {jumlah:,}"),
html.Br(),
html.Small(f"{trans.get('keterangan', 'N/A')} | Metode: {metode.title()}"),
html.Hr(style={'margin': '5px 0'})
]))
daftar_pengeluaran = items_pengeluaran
else:
daftar_pengeluaran = html.P("Belum ada transaksi pengeluaran untuk tanggal ini", style={'color': '#6c757d'})
return daftar_pemasukan, daftar_pengeluaran
def update_persediaan_pembelian(kode_barang, qty, harga, tanggal, keterangan, supplier=""):
"""Update kartu persediaan saat pembelian - sistem sederhana"""
try:
# Cari barang di master
barang = next((item for item in sibal_data.master_persediaan if item['kode'] == kode_barang), None)
if not barang:
print(f"â Barang dengan kode {kode_barang} tidak ditemukan")
return False
# Hitung saldo terakhir
transaksi_sebelumnya = [t for t in sibal_data.kartu_persediaan if t['kode_barang'] == kode_barang]
saldo_qty_sebelumnya = 0
saldo_total_sebelumnya = 0
if transaksi_sebelumnya:
last_trans = transaksi_sebelumnya[-1]
saldo_qty_sebelumnya = last_trans['saldo_qty']
saldo_total_sebelumnya = last_trans['saldo_total']
# Hitung saldo baru
saldo_qty_baru = saldo_qty_sebelumnya + qty
saldo_total_baru = saldo_total_sebelumnya + (qty * harga)
saldo_harga_baru = saldo_total_baru / saldo_qty_baru if saldo_qty_baru > 0 else 0
print(f"đ PEMBELIAN: {saldo_qty_sebelumnya} + {qty} = {saldo_qty_baru} kg")
print(f"đ NILAI: Rp{saldo_total_sebelumnya:,} + Rp{qty * harga:,} = Rp{saldo_total_baru:,}")
# Buat entri baru
entri_baru = {
'tanggal': tanggal,
'kode_barang': kode_barang,
'nama_barang': barang['nama'],
'masuk_qty': qty,
'masuk_harga': harga,
'masuk_total': qty * harga,
'keluar_qty': 0,
'keluar_harga': 0,
'keluar_total': 0,
'saldo_qty': saldo_qty_baru,
'saldo_harga': saldo_harga_baru,
'saldo_total': saldo_total_baru,
'keterangan': f'Pembelian - {keterangan}' + (f' - {supplier}' if supplier else '')
}
entri_baru['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_kartu_persediaan.append(entri_baru)
print(f"â
BERHASIL: {kode_barang} +{qty}kg, Saldo: {saldo_qty_baru}kg @ Rp{saldo_harga_baru:,.0f}")
return True
except Exception as e:
print(f"â GAGAL: {e}")
return False
def update_persediaan_penjualan(kode_barang, qty, tanggal, keterangan):
"""Update kartu persediaan saat penjualan - sistem sederhana"""
try:
# Cari barang di master
barang = next((item for item in sibal_data.master_persediaan if item['kode'] == kode_barang), None)
if not barang:
print(f"â Barang dengan kode {kode_barang} tidak ditemukan")
return False, 0
# Hitung saldo terakhir
transaksi_sebelumnya = [t for t in sibal_data.kartu_persediaan if t['kode_barang'] == kode_barang]
if not transaksi_sebelumnya:
print(f"â Tidak ada stok untuk {kode_barang}")
return False, 0
last_trans = transaksi_sebelumnya[-1]
saldo_qty_sebelumnya = last_trans['saldo_qty']
saldo_total_sebelumnya = last_trans['saldo_total']
saldo_harga_sebelumnya = last_trans['saldo_harga']
# Cek stok cukup
if saldo_qty_sebelumnya < qty:
print(f"â Stok tidak cukup: {saldo_qty_sebelumnya} < {qty}")
return False, 0
# Hitung HPP
hpp_total = qty * saldo_harga_sebelumnya
# Hitung saldo baru
saldo_qty_baru = saldo_qty_sebelumnya - qty
saldo_total_baru = saldo_total_sebelumnya - hpp_total
saldo_harga_baru = saldo_total_baru / saldo_qty_baru if saldo_qty_baru > 0 else 0
print(f"đ PENJUALAN: {saldo_qty_sebelumnya} - {qty} = {saldo_qty_baru} kg")
print(f"đ NILAI: Rp{saldo_total_sebelumnya:,} - Rp{hpp_total:,} = Rp{saldo_total_baru:,}")
# Buat entri baru
entri_baru = {
'tanggal': tanggal,
'kode_barang': kode_barang,
'nama_barang': barang['nama'],
'masuk_qty': 0,
'masuk_harga': 0,
'masuk_total': 0,
'keluar_qty': qty,
'keluar_harga': saldo_harga_sebelumnya,
'keluar_total': hpp_total,
'saldo_qty': saldo_qty_baru,
'saldo_harga': saldo_harga_baru,
'saldo_total': saldo_total_baru,
'keterangan': f'Penjualan - {keterangan}'
}
entri_baru['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_kartu_persediaan.append(entri_baru)
print(f"â
BERHASIL: {kode_barang} -{qty}kg, HPP: Rp{hpp_total:,}, Saldo: {saldo_qty_baru}kg")
return True, hpp_total
except Exception as e:
print(f"â GAGAL: {e}")
return False, 0
def hitung_ulang_semua_saldo():
"""Hitung ulang semua saldo dari transaksi pertama - untuk memperbaiki data yang rusak"""
print("đ MENGHITUNG ULANG SEMUA SALDO...")
# Kelompokkan berdasarkan kode barang
transaksi_per_barang = {}
for item in sibal_data.kartu_persediaan:
kode = item['kode_barang']
if kode not in transaksi_per_barang:
transaksi_per_barang[kode] = []
transaksi_per_barang[kode].append(item)
# Urutkan setiap barang berdasarkan tanggal
for kode_barang in transaksi_per_barang:
transaksi_per_barang[kode_barang].sort(key=lambda x: x['tanggal'])
# Hapus semua data kartu persediaan
sibal_data.kartu_persediaan.clear()
# Hitung ulang dari transaksi pertama
for kode_barang, transaksi_list in transaksi_per_barang.items():
saldo_qty = 0
saldo_total = 0
for i, transaksi in enumerate(transaksi_list):
# Simpan data asli masuk/keluar
masuk_qty = transaksi['masuk_qty']
masuk_harga = transaksi['masuk_harga']
masuk_total = transaksi['masuk_total']
keluar_qty = transaksi['keluar_qty']
keluar_harga = transaksi['keluar_harga']
keluar_total = transaksi['keluar_total']
# Update saldo
saldo_qty += masuk_qty - keluar_qty
saldo_total += masuk_total - keluar_total
saldo_harga = saldo_total / saldo_qty if saldo_qty > 0 else 0
# Buat entri baru dengan saldo yang benar
entri_baru = {
'tanggal': transaksi['tanggal'],
'kode_barang': kode_barang,
'nama_barang': transaksi['nama_barang'],
'masuk_qty': masuk_qty,
'masuk_harga': masuk_harga,
'masuk_total': masuk_total,
'keluar_qty': keluar_qty,
'keluar_harga': keluar_harga,
'keluar_total': keluar_total,
'saldo_qty': saldo_qty,
'saldo_harga': saldo_harga,
'saldo_total': saldo_total,
'keterangan': transaksi['keterangan']
}
entri_baru['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_kartu_persediaan.append(entri_baru)
print(f"đ {kode_barang} Transaksi {i+1}: Saldo = {saldo_qty}kg @ Rp{saldo_harga:,.0f}")
print("â
SELESAI menghitung ulang semua saldo")
# Callback untuk menampilkan jurnal umum dengan format yang rapi
@app.callback(
[Output('tabel-jurnal-umum', 'children'),
Output('rekapitulasi-jurnal', 'children')],
Input('btn-refresh-jurnal', 'n_clicks')
)
def update_jurnal_umum(n_clicks):
# Tampilkan jurnal umum dalam format yang rapi
if sibal_data.jurnal_umum:
jurnal_entries = []
for jurnal in sibal_data.jurnal_umum:
# Entry untuk debit
jurnal_entries.append(html.Div([
html.Span(jurnal['tanggal'], style={'fontWeight': 'bold', 'minWidth': '100px', 'display': 'inline-block'}),
html.Span(jurnal['akun_debit'], style={'minWidth': '200px', 'display': 'inline-block'}),
html.Span("", style={'minWidth': '100px', 'display': 'inline-block'}),
html.Span(format_rupiah(jurnal['jumlah_debit']),
style={'textAlign': 'right', 'minWidth': '150px', 'display': 'inline-block', 'paddingLeft': '20px'}),
html.Span("", style={'minWidth': '150px', 'display': 'inline-block'})
], className="jurnal-entry"))
# Entry untuk kredit (dengan indentasi)
jurnal_entries.append(html.Div([
html.Span("", style={'minWidth': '100px', 'display': 'inline-block'}),
html.Span(jurnal['akun_kredit'],
style={'minWidth': '200px', 'display': 'inline-block', 'paddingLeft': '40px', 'color': COLORS['secondary']}),
html.Span("", style={'minWidth': '100px', 'display': 'inline-block'}),
html.Span("", style={'minWidth': '150px', 'display': 'inline-block'}),
html.Span(format_rupiah(jurnal['jumlah_kredit']),
style={'textAlign': 'right', 'minWidth': '150px', 'display': 'inline-block', 'color': COLORS['secondary']})
], className="jurnal-entry jurnal-kredit"))
# Tambahkan spasi antara jurnal
jurnal_entries.append(html.Div(style={'height': '10px'}))
tabel_jurnal = html.Div(jurnal_entries)
# Buat rekapitulasi jurnal
rekapitulasi_data = {}
for jurnal in sibal_data.jurnal_umum:
# Debit
akun_debit = f"{jurnal['kode_akun_debit']} - {jurnal['akun_debit']}"
if akun_debit not in rekapitulasi_data:
rekapitulasi_data[akun_debit] = {'debit': 0, 'kredit': 0}
rekapitulasi_data[akun_debit]['debit'] += jurnal['jumlah_debit']
# Kredit
akun_kredit = f"{jurnal['kode_akun_kredit']} - {jurnal['akun_kredit']}"
if akun_kredit not in rekapitulasi_data:
rekapitulasi_data[akun_kredit] = {'debit': 0, 'kredit': 0}
rekapitulasi_data[akun_kredit]['kredit'] += jurnal['jumlah_kredit']
# Buat tabel rekapitulasi
header_rekapitulasi = html.Tr([
html.Th("Akun Debit"),
html.Th("Kode"),
html.Th("Debit"),
html.Th("Akun Kredit"),
html.Th("Kode"),
html.Th("Kredit")
])
rows_rekapitulasi = []
total_debit = 0
total_kredit = 0
# Gabungkan data debit dan kredit dalam satu baris
akun_list = list(rekapitulasi_data.keys())
max_rows = max(len([akun for akun in akun_list if rekapitulasi_data[akun]['debit'] > 0]),
len([akun for akun in akun_list if rekapitulasi_data[akun]['kredit'] > 0]))
debit_akun = [akun for akun in akun_list if rekapitulasi_data[akun]['debit'] > 0]
kredit_akun = [akun for akun in akun_list if rekapitulasi_data[akun]['kredit'] > 0]
for i in range(max_rows):
debit_row = debit_akun[i] if i < len(debit_akun) else ""
kredit_row = kredit_akun[i] if i < len(kredit_akun) else ""
debit_kode = debit_row.split(' - ')[0] if debit_row else ""
debit_nama = debit_row.split(' - ')[1] if debit_row else ""
debit_jumlah = rekapitulasi_data[debit_row]['debit'] if debit_row else 0
kredit_kode = kredit_row.split(' - ')[0] if kredit_row else ""
kredit_nama = kredit_row.split(' - ')[1] if kredit_row else ""
kredit_jumlah = rekapitulasi_data[kredit_row]['kredit'] if kredit_row else 0
total_debit += debit_jumlah
total_kredit += kredit_jumlah
rows_rekapitulasi.append(html.Tr([
html.Td(debit_nama),
html.Td(debit_kode),
html.Td(format_rupiah(debit_jumlah) if debit_jumlah > 0 else ""),
html.Td(kredit_nama),
html.Td(kredit_kode),
html.Td(format_rupiah(kredit_jumlah) if kredit_jumlah > 0 else "")
]))
# Baris total
rows_rekapitulasi.append(html.Tr([
html.Td("TOTAL", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_debit), style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td("TOTAL", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_kredit), style={'fontWeight': 'bold', 'borderTop': '2px solid #000'})
]))
tabel_rekapitulasi = html.Table(
[header_rekapitulasi] + rows_rekapitulasi,
className="modern-table"
)
return tabel_jurnal, tabel_rekapitulasi
else:
return html.P("Belum ada data jurnal umum", style={'color': '#6c757d'}), html.P("Belum ada data rekapitulasi", style={'color': '#6c757d'})
@app.callback(
[Output('summary-akumulasi', 'children'),
Output('selected-date', 'date')],
[Input('btn-akumulasi-pemasukan', 'n_clicks'),
Input('btn-akumulasi-pengeluaran', 'n_clicks')],
[State('selected-date', 'date')],
prevent_initial_call=True
)
def akumulasi_ke_jurnal(n_pemasukan, n_pengeluaran, tanggal):
ctx = dash.callback_context
if not ctx.triggered:
return html.P("Klik tombol akumulasi untuk mencatat transaksi ke jurnal umum"), dash.no_update
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
jurnal_created = []
print(f"đ§ DEBUG: Processing accumulation for {button_id} on date {tanggal}")
# Hitung tanggal berikutnya
if tanggal:
current_date = datetime.strptime(tanggal, '%Y-%m-%d')
next_date = current_date + timedelta(days=1)
next_date_str = next_date.strftime('%Y-%m-%d')
else:
next_date_str = datetime.now().date().isoformat()
if button_id == 'btn-akumulasi-pemasukan' and n_pemasukan:
# Akumulasi pemasukan
transaksi_pemasukan = [t for t in sibal_data.transaksi_pemasukan if t['tanggal'] == tanggal]
print(f"đ DEBUG: Found {len(transaksi_pemasukan)} pemasukan transactions")
for trans in transaksi_pemasukan:
print(f"đ DEBUG: Processing pemasukan - {trans['jenis']} - Rp {trans['jumlah']:,}")
if trans['jenis'] == 'tiket_masuk':
jurnal = {
'tanggal': tanggal,
'keterangan': f"{trans['keterangan']}",
'ref': trans.get('ref', ''),
'akun_debit': 'Kas',
'kode_akun_debit': KODE_AKUN['kas']['kode'],
'jumlah_debit': trans['jumlah'],
'akun_kredit': 'Pendapatan Tiket',
'kode_akun_kredit': KODE_AKUN['pendapatan_tiket']['kode'],
'jumlah_kredit': trans['jumlah']
}
jurnal['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(jurnal)
jurnal_created.append(jurnal)
print(f"â
Created jurnal for tiket masuk")
# Dalam callback akumulasi_ke_jurnal, perbaiki bagian penjualan ikan:
elif trans['jenis'] == 'penjualan_ikan':
# Jurnal penjualan
jurnal_penjualan = {
'tanggal': tanggal,
'keterangan': f"{trans['keterangan']}",
'ref': trans.get('ref', ''),
'akun_debit': 'Kas',
'kode_akun_debit': KODE_AKUN['kas']['kode'],
'jumlah_debit': trans['jumlah'],
'akun_kredit': 'Penjualan',
'kode_akun_kredit': KODE_AKUN['pendapatan']['kode'],
'jumlah_kredit': trans['jumlah']
}
jurnal_penjualan['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(jurnal_penjualan)
jurnal_created.append(jurnal_penjualan)
print(f"â
Created jurnal for penjualan")
# Jurnal HPP dan pengurangan persediaan
kode_barang = 'IK001' # Ikan Bawal Segar
qty_terjual = trans.get('quantity', 0)
print(f"đ§ DEBUG: Attempting to reduce inventory - {qty_terjual}kg")
if qty_terjual > 0:
# Kurangi persediaan fisik dengan fungsi yang sudah diperbaiki
success, total_hpp = update_persediaan_penjualan(kode_barang, qty_terjual, tanggal, trans['keterangan'])
if success:
print(f"â
Successfully reduced inventory, HPP: Rp{total_hpp:,}")
# Buat jurnal HPP
jurnal_hpp = {
'tanggal': tanggal,
'keterangan': f"HPP - {trans['keterangan']}",
'ref': trans.get('ref', '') + '-HPP',
'akun_debit': 'Harga Pokok Produksi',
'kode_akun_debit': KODE_AKUN['hpp']['kode'],
'jumlah_debit': total_hpp,
'akun_kredit': 'Persediaan Barang Dagang',
'kode_akun_kredit': KODE_AKUN['persediaan']['kode'],
'jumlah_kredit': total_hpp
}
jurnal_hpp['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(jurnal_hpp)
jurnal_created.append(jurnal_hpp)
print(f"â
Created HPP jurnal: Rp {total_hpp:,}")
else:
print(f"â Failed to reduce inventory")
elif trans['jenis'] == 'modal':
jurnal = {
'tanggal': tanggal,
'keterangan': f"{trans['keterangan']}",
'ref': trans.get('ref', ''),
'akun_debit': 'Kas',
'kode_akun_debit': KODE_AKUN['kas']['kode'],
'jumlah_debit': trans['jumlah'],
'akun_kredit': 'Modal',
'kode_akun_kredit': KODE_AKUN['modal']['kode'],
'jumlah_kredit': trans['jumlah']
}
jurnal['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(jurnal)
jurnal_created.append(jurnal)
print(f"â
Created jurnal for modal")
# Simpan data setelah semua jurnal dibuat
if jurnal_created:
sibal_data.save_all_data()
return html.Div([
html.H4("â
Akumulasi Pemasukan Berhasil!", style={'color': '#28a745'}),
html.P(f"{len(jurnal_created)} jurnal telah dibuat untuk transaksi pemasukan tanggal {tanggal}"),
html.P(f"Tanggal otomatis berubah ke {next_date_str}", style={'fontWeight': 'bold', 'color': '#007bff'})
]), next_date_str
else:
return html.Div([
html.H4("â Tidak ada transaksi pemasukan", style={'color': '#dc3545'}),
html.P(f"Tidak ada transaksi pemasukan untuk tanggal {tanggal}"),
html.P("Silakan buat transaksi pemasukan terlebih dahulu")
]), dash.no_update
elif button_id == 'btn-akumulasi-pengeluaran' and n_pengeluaran:
# Akumulasi pengeluaran
transaksi_pengeluaran = [t for t in sibal_data.transaksi_pengeluaran if t['tanggal'] == tanggal]
print(f"đ DEBUG: Found {len(transaksi_pengeluaran)} pengeluaran transactions")
for trans in transaksi_pengeluaran:
print(f"đ DEBUG: Processing pengeluaran - {trans['jenis']} - Rp {trans['jumlah']:,}")
# Tentukan akun debit berdasarkan jenis pengeluaran
akun_debit_mapping = {
'pembelian_persediaan': {'nama': 'Persediaan Barang Dagang', 'kode': KODE_AKUN['persediaan']['kode']},
'beban_gaji': {'nama': 'Beban Gaji', 'kode': KODE_AKUN['beban_gaji']['kode']},
'beban_listrik': {'nama': 'Beban Listrik', 'kode': KODE_AKUN['beban_listrik']['kode']},
'beban_penyusutan_bangunan': {'nama': 'Beban Depresiasi Bangunan', 'kode': KODE_AKUN['beban_penyusutan_bangunan']['kode']},
'beban_penyusutan_kendaraan': {'nama': 'Beban Depresiasi Kendaraan', 'kode': KODE_AKUN['beban_penyusutan_kendaraan']['kode']},
'beban_penyusutan_peralatan': {'nama': 'Beban Depresiasi Peralatan', 'kode': KODE_AKUN['beban_penyusutan_peralatan']['kode']},
'beban_lainnya': {'nama': 'Beban Lainnya', 'kode': KODE_AKUN['beban_lainnya']['kode']}
}
akun_debit = akun_debit_mapping.get(trans['jenis'], {'nama': 'Beban Lainnya', 'kode': KODE_AKUN['beban_lainnya']['kode']})
# Tentukan akun kredit berdasarkan metode pembayaran
if trans.get('metode_bayar') == 'tunai':
akun_kredit = {'nama': 'Kas', 'kode': KODE_AKUN['kas']['kode']}
else:
akun_kredit = {'nama': 'Utang Dagang', 'kode': KODE_AKUN['utang']['kode']}
jurnal = {
'tanggal': tanggal,
'keterangan': f"{trans['keterangan']}",
'ref': trans.get('ref', ''),
'akun_debit': akun_debit['nama'],
'kode_akun_debit': akun_debit['kode'],
'jumlah_debit': trans['jumlah'],
'akun_kredit': akun_kredit['nama'],
'kode_akun_kredit': akun_kredit['kode'],
'jumlah_kredit': trans['jumlah']
}
jurnal['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(jurnal)
jurnal_created.append(jurnal)
print(f"â
Created jurnal for {trans['jenis']}")
# TAMBAHKAN KE BUKU BESAR PEMBANTU JIKA KREDIT
if trans.get('metode_bayar') == 'kredit' and trans.get('supplier'):
supplier = trans['supplier']
print(f"đ§ DEBUG: Adding to buku besar pembantu for supplier: {supplier}")
# Inisialisasi jika belum ada
if supplier not in sibal_data.buku_besar_pembantu:
sibal_data.buku_besar_pembantu[supplier] = []
# Hitung saldo terakhir
saldo_terakhir = 0
if sibal_data.buku_besar_pembantu[supplier]:
saldo_terakhir = sibal_data.buku_besar_pembantu[supplier][-1]['saldo']
saldo_baru = saldo_terakhir + trans['jumlah']
# Tambahkan transaksi
sibal_data.buku_besar_pembantu[supplier].append({
'tanggal': tanggal,
'keterangan': f"{trans['jenis']} - {trans['keterangan']}",
'debit': 0,
'kredit': trans['jumlah'],
'saldo': saldo_baru
})
print(f"â
Added to buku besar pembantu: {supplier} - Rp {trans['jumlah']:,}")
# Simpan data setelah semua jurnal dibuat
if jurnal_created:
sibal_data.save_all_data()
return html.Div([
html.H4("â
Akumulasi Pengeluaran Berhasil!", style={'color': '#28a745'}),
html.P(f"{len(jurnal_created)} jurnal telah dibuat untuk transaksi pengeluaran tanggal {tanggal}"),
html.P(f"Tanggal otomatis berubah ke {next_date_str}", style={'fontWeight': 'bold', 'color': '#007bff'})
]), next_date_str
else:
return html.Div([
html.H4("â Tidak ada transaksi pengeluaran", style={'color': '#dc3545'}),
html.P(f"Tidak ada transaksi pengeluaran untuk tanggal {tanggal}"),
html.P("Silakan buat transaksi pengeluaran terlebih dahulu")
]), dash.no_update
return html.P("Klik tombol akumulasi untuk mencatat transaksi ke jurnal"), dash.no_update
# Fungsi untuk akumulasi pengeluaran ke jurnal
def akumulasi_pengeluaran_ke_jurnal(tanggal):
transaksi_hari_ini = [t for t in sibal_data.transaksi_pengeluaran if t['tanggal'] == tanggal]
if transaksi_hari_ini:
for trans in transaksi_hari_ini:
jenis = trans['jenis']
jumlah = trans['jumlah']
metode = trans['metode_bayar']
keterangan = trans['keterangan']
ref = trans.get('ref', '')
# Tentukan akun berdasarkan kode akun yang sudah disimpan
akun_debit = trans.get('kode_akun_debit', '')
akun_debit_nama = next((v['nama'] for k, v in KODE_AKUN.items() if v['kode'] == akun_debit), 'Beban Lainnya')
akun_kredit = trans.get('kode_akun_kredit', '')
akun_kredit_nama = next((v['nama'] for k, v in KODE_AKUN.items() if v['kode'] == akun_kredit), 'Kas')
# Buat jurnal
entri_jurnal = {
'tanggal': tanggal,
'keterangan': f"{keterangan}",
'ref': ref,
'akun_debit': akun_debit_nama,
'kode_akun_debit': akun_debit,
'jumlah_debit': jumlah,
'akun_kredit': akun_kredit_nama,
'kode_akun_kredit': akun_kredit,
'jumlah_kredit': jumlah
}
entri_jurnal['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(entri_jurnal)
sibal_data.save_data()
return html.Div([
html.H4("â
Akumulasi Berhasil!", style={'color': '#28a745'}),
html.P(f"Transaksi pengeluaran tanggal {tanggal} telah dicatat ke Jurnal Umum")
])
return html.P("Belum ada transaksi pengeluaran untuk diakumulasi")
@app.callback(
Output('tabel-kartu-persediaan', 'children'),
Input('btn-refresh-persediaan', 'n_clicks')
)
def update_kartu_persediaan(n_clicks):
"""Update kartu persediaan dengan perhitungan real-time"""
print(f"đ Memperbarui kartu persediaan...")
# HITUNG ULANG SALDO SEBELUM MENAMPILKAN
hitung_ulang_semua_saldo()
if sibal_data.kartu_persediaan:
# Kelompokkan berdasarkan kode barang
transaksi_per_barang = {}
for item in sibal_data.kartu_persediaan:
kode = item['kode_barang']
if kode not in transaksi_per_barang:
transaksi_per_barang[kode] = []
transaksi_per_barang[kode].append(item)
all_tables = []
for kode_barang, transaksi_list in transaksi_per_barang.items():
# Urutkan transaksi berdasarkan tanggal
transaksi_list.sort(key=lambda x: x['tanggal'])
barang_pertama = transaksi_list[0]
# BUAT HEADER
header = html.Tr([
html.Th("Tanggal", style={'backgroundColor': COLORS['primary'], 'color': 'white', 'padding': '8px'}),
html.Th("Keterangan", style={'backgroundColor': COLORS['primary'], 'color': 'white', 'padding': '8px'}),
html.Th("Masuk", style={'backgroundColor': COLORS['success'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("", style={'backgroundColor': COLORS['success'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("", style={'backgroundColor': COLORS['success'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("Keluar", style={'backgroundColor': COLORS['error'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("", style={'backgroundColor': COLORS['error'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("", style={'backgroundColor': COLORS['error'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("Saldo", style={'backgroundColor': COLORS['info'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("", style={'backgroundColor': COLORS['info'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'}),
html.Th("", style={'backgroundColor': COLORS['info'], 'color': 'white', 'padding': '8px', 'textAlign': 'center'})
])
sub_header = html.Tr([
html.Th("", style={'backgroundColor': COLORS['primary'], 'color': 'white'}),
html.Th("", style={'backgroundColor': COLORS['primary'], 'color': 'white'}),
html.Th("Qty", style={'backgroundColor': COLORS['success_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Harga", style={'backgroundColor': COLORS['success_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Total", style={'backgroundColor': COLORS['success_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Qty", style={'backgroundColor': COLORS['error_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Harga", style={'backgroundColor': COLORS['error_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Total", style={'backgroundColor': COLORS['error_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Qty", style={'backgroundColor': COLORS['info_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Harga", style={'backgroundColor': COLORS['info_light'], 'color': 'white', 'textAlign': 'center'}),
html.Th("Total", style={'backgroundColor': COLORS['info_light'], 'color': 'white', 'textAlign': 'center'})
])
rows = []
total_masuk_qty = 0
total_masuk_nilai = 0
total_keluar_qty = 0
total_keluar_nilai = 0
for item in transaksi_list:
def format_angka(angka):
return f"{angka:,.0f}" if angka > 0 else ""
def format_rupiah(angka):
return f"Rp{angka:,.0f}".replace(",", ".") if angka > 0 else ""
rows.append(html.Tr([
html.Td(item['tanggal'], style={'padding': '6px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'fontSize': '12px'}),
html.Td(item['keterangan'], style={'padding': '6px', 'border': '1px solid #dee2e6', 'fontSize': '12px'}),
html.Td(format_angka(item['masuk_qty']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e8f5e8', 'fontSize': '12px'}),
html.Td(format_rupiah(item['masuk_harga']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e8f5e8', 'fontSize': '12px'}),
html.Td(format_rupiah(item['masuk_total']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e8f5e8', 'fontSize': '12px'}),
html.Td(format_angka(item['keluar_qty']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#f8d7da', 'fontSize': '12px'}),
html.Td(format_rupiah(item['keluar_harga']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#f8d7da', 'fontSize': '12px'}),
html.Td(format_rupiah(item['keluar_total']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#f8d7da', 'fontSize': '12px'}),
html.Td(format_angka(item['saldo_qty']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#d1ecf1', 'fontWeight': 'bold', 'fontSize': '12px'}),
html.Td(format_rupiah(item['saldo_harga']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#d1ecf1', 'fontSize': '12px'}),
html.Td(format_rupiah(item['saldo_total']), style={'padding': '6px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#d1ecf1', 'fontWeight': 'bold', 'fontSize': '12px'})
]))
total_masuk_qty += item['masuk_qty']
total_masuk_nilai += item['masuk_total']
total_keluar_qty += item['keluar_qty']
total_keluar_nilai += item['keluar_total']
# BARIS TOTAL
last_trans = transaksi_list[-1]
rows.append(html.Tr([
html.Td("TOTAL", colSpan=2, style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'borderTop': '2px solid #000', 'fontSize': '12px'}),
html.Td(f"{total_masuk_qty}", style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'}),
html.Td("", style={'padding': '8px', 'border': '1px solid #dee2e6', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_masuk_nilai), style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'}),
html.Td(f"{total_keluar_qty}", style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'}),
html.Td("", style={'padding': '8px', 'border': '1px solid #dee2e6', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_keluar_nilai), style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'}),
html.Td(f"{last_trans['saldo_qty']}", style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'}),
html.Td(format_rupiah(last_trans['saldo_harga']), style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'}),
html.Td(format_rupiah(last_trans['saldo_total']), style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'borderTop': '2px solid #000', 'textAlign': 'right', 'fontSize': '12px'})
]))
table = html.Div([
html.H4(f"đĻ {kode_barang} - {barang_pertama['nama_barang']}",
style={'color': COLORS['primary'], 'marginBottom': '10px', 'fontSize': '16px'}),
html.Table(
[header, sub_header] + rows,
style={
'width': '100%',
'borderCollapse': 'collapse',
'border': '1px solid #dee2e6',
'fontSize': '11px'
}
)
], style={'marginBottom': '20px', 'overflowX': 'auto'})
all_tables.append(table)
return html.Div(all_tables)
else:
return html.Div([
html.P("đ Belum ada data kartu persediaan",
style={'textAlign': 'center', 'color': COLORS['gray_600'], 'fontSize': '16px', 'marginBottom': '10px'}),
html.P("đĄ Buat transaksi pembelian persediaan terlebih dahulu",
style={'textAlign': 'center', 'color': COLORS['gray_500'], 'fontSize': '14px'})
])
@app.callback(
Output('detail-buku-besar', 'children'),
Input('dropdown-akun-buku-besar', 'value')
)
def update_buku_besar(akun):
if akun:
# Cari nama akun dari value
akun_nama = next((item['label'].split(' - ')[1] for item in sibal_data.daftar_akun if item['value'] == akun), akun)
akun_kode = next((item['label'].split(' - ')[0] for item in sibal_data.daftar_akun if item['value'] == akun), "")
# Hitung saldo
saldo = 0
transaksi_akun = []
# Gabungkan semua jurnal
semua_jurnal = sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian
for jurnal in semua_jurnal:
if jurnal['kode_akun_debit'] == akun_kode:
saldo += jurnal['jumlah_debit']
transaksi_akun.append({
'tanggal': jurnal['tanggal'],
'keterangan': jurnal['keterangan'],
'debit': jurnal['jumlah_debit'],
'kredit': 0,
'saldo': saldo
})
elif jurnal['kode_akun_kredit'] == akun_kode:
saldo -= jurnal['jumlah_kredit']
transaksi_akun.append({
'tanggal': jurnal['tanggal'],
'keterangan': jurnal['keterangan'],
'debit': 0,
'kredit': jurnal['jumlah_kredit'],
'saldo': saldo
})
if transaksi_akun:
header = html.Tr([
html.Th("Tanggal"),
html.Th("Keterangan"),
html.Th("Debit"),
html.Th("Kredit"),
html.Th("Saldo")
])
rows = []
for trans in transaksi_akun:
rows.append(html.Tr([
html.Td(trans['tanggal']),
html.Td(trans['keterangan']),
html.Td(format_rupiah(trans['debit']) if trans['debit'] > 0 else ""),
html.Td(format_rupiah(trans['kredit']) if trans['kredit'] > 0 else ""),
html.Td(format_rupiah(trans['saldo']))
]))
return html.Div([
html.H4(f"Buku Besar: {akun_nama} ({akun_kode})", style={'marginBottom': '20px'}),
html.Table([header] + rows, className="modern-table")
])
else:
return html.P(f"Belum ada transaksi untuk akun {akun_nama}", style={'color': '#6c757d'})
else:
return html.P("Pilih akun untuk melihat buku besar", style={'color': '#6c757d'})
# Tambahkan callback khusus untuk auto-refresh setelah transaksi
@app.callback(
Output('btn-refresh-persediaan', 'children'), # Update tombol refresh sebagai trigger
[Input('btn-tambah-pengeluaran', 'n_clicks'),
Input('btn-akumulasi-pengeluaran', 'n_clicks')],
prevent_initial_call=True
)
def trigger_persediaan_refresh(n_pengeluaran, n_akumulasi):
"""Trigger refresh kartu persediaan setelah transaksi"""
ctx = dash.callback_context
if ctx.triggered:
print(f"đ Triggering persediaan refresh from {ctx.triggered[0]['prop_id']}")
return "đ Refresh Data (Updated)"
return "đ Refresh Data"
@app.callback(
[Output('daftar-supplier', 'children'),
Output('detail-buku-besar-pembantu', 'children')],
Input('btn-refresh-buku-pembantu', 'n_clicks')
)
def refresh_buku_besar_pembantu(n_clicks):
"""Refresh data buku besar pembantu"""
print(f"đ§ DEBUG: Refreshing buku besar pembantu...")
# PROSES DATA UTANG DARI TRANSAKSI KREDIT
sibal_data.buku_besar_pembantu = {} # Reset dulu
# Kumpulkan semua supplier dari transaksi kredit
suppliers_set = set()
for trans in sibal_data.transaksi_pengeluaran:
if trans.get('metode_bayar') == 'kredit' and trans.get('supplier'):
suppliers_set.add(trans['supplier'])
sibal_data.suppliers = list(suppliers_set)
# Proses transaksi untuk setiap supplier
for supplier in sibal_data.suppliers:
sibal_data.buku_besar_pembantu[supplier] = []
# Cari semua transaksi kredit untuk supplier ini
transaksi_supplier = [t for t in sibal_data.transaksi_pengeluaran
if t.get('supplier') == supplier and t.get('metode_bayar') == 'kredit']
saldo = 0
for trans in transaksi_supplier:
jumlah = trans['jumlah']
saldo += jumlah
# Tambahkan transaksi utang
sibal_data.buku_besar_pembantu[supplier].append({
'tanggal': trans['tanggal'],
'keterangan': f"{trans['jenis']} - {trans['keterangan']}",
'debit': 0, # Pembayaran akan mengurangi utang (debit)
'kredit': jumlah, # Utang bertambah (kredit)
'saldo': saldo
})
# Daftar supplier
if sibal_data.suppliers:
daftar_supplier = html.Div([
html.H4("đ Daftar Supplier dengan Utang", style={'color': COLORS['primary'], 'marginBottom': '20px'}),
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-building", style={'fontSize': '2rem', 'color': COLORS['secondary']}),
html.Div([
html.Strong(supplier, style={'fontSize': '1.1rem'}),
html.Br(),
html.Small(f"Total Utang: {format_rupiah(sum(t['kredit'] - t['debit'] for t in sibal_data.buku_besar_pembantu.get(supplier, [])))}")
], style={'marginLeft': '10px'})
], style={'display': 'flex', 'alignItems': 'center', 'padding': '15px'})
], style={
'backgroundColor': COLORS['white'],
'borderRadius': '10px',
'boxShadow': '0 2px 5px rgba(0,0,0,0.1)',
'marginBottom': '10px',
'borderLeft': f'4px solid {COLORS["secondary"]}'
})
for supplier in sibal_data.suppliers
if supplier in sibal_data.buku_besar_pembantu and sibal_data.buku_besar_pembantu[supplier]
])
])
else:
daftar_supplier = html.Div([
html.I(className="fas fa-info-circle", style={'fontSize': '3rem', 'color': COLORS['gray_400'], 'display': 'block', 'textAlign': 'center', 'marginBottom': '10px'}),
html.P("Belum ada supplier dengan utang", style={'textAlign': 'center', 'color': COLORS['gray_600']})
])
# Detail per supplier
detail_supplier = []
for supplier in sibal_data.suppliers:
if supplier in sibal_data.buku_besar_pembantu and sibal_data.buku_besar_pembantu[supplier]:
transaksi = sibal_data.buku_besar_pembantu[supplier]
header = html.Tr([
html.Th("Tanggal", style={'backgroundColor': COLORS['primary'], 'color': 'white'}),
html.Th("Keterangan", style={'backgroundColor': COLORS['primary'], 'color': 'white'}),
html.Th("Pembayaran (Debit)", style={'backgroundColor': COLORS['primary'], 'color': 'white'}),
html.Th("Utang (Kredit)", style={'backgroundColor': COLORS['primary'], 'color': 'white'}),
html.Th("Saldo Utang", style={'backgroundColor': COLORS['primary'], 'color': 'white'})
])
rows = []
saldo = 0
for t in transaksi:
saldo = saldo + t['kredit'] - t['debit']
rows.append(html.Tr([
html.Td(t['tanggal']),
html.Td(t['keterangan']),
html.Td(format_rupiah(t['debit']) if t['debit'] > 0 else "-",
style={'color': COLORS['success'], 'textAlign': 'right'}),
html.Td(format_rupiah(t['kredit']) if t['kredit'] > 0 else "-",
style={'color': COLORS['error'], 'textAlign': 'right'}),
html.Td(format_rupiah(saldo),
style={
'fontWeight': 'bold',
'color': COLORS['error'] if saldo > 0 else COLORS['success'],
'textAlign': 'right'
})
]))
total_utang = saldo
detail_supplier.append(html.Div([
html.H4(f"đ Buku Besar Pembantu: {supplier}",
style={'color': COLORS['secondary'], 'marginBottom': '20px'}),
html.Table([header] + rows, className="modern-table"),
html.Div([
html.Strong(f"Total Utang: {format_rupiah(total_utang)}"),
], style={
'padding': '15px',
'backgroundColor': COLORS['error_light'] if total_utang > 0 else COLORS['success_light'],
'color': 'white',
'borderRadius': '8px',
'textAlign': 'center',
'marginTop': '15px',
'fontSize': '1.1rem'
}),
html.Hr(style={'margin': '30px 0', 'borderColor': COLORS['gray_300']})
], style={'marginBottom': '30px'}))
if not detail_supplier:
detail_supplier = html.Div([
html.I(className="fas fa-receipt", style={'fontSize': '3rem', 'color': COLORS['gray_400'], 'display': 'block', 'textAlign': 'center', 'marginBottom': '10px'}),
html.P("Belum ada transaksi utang", style={'textAlign': 'center', 'color': COLORS['gray_600']})
])
else:
detail_supplier = html.Div(detail_supplier)
return daftar_supplier, detail_supplier
def bayar_utang_supplier(supplier, jumlah, tanggal, keterangan=""):
"""Mencatat pembayaran utang kepada supplier"""
try:
if supplier not in sibal_data.buku_besar_pembantu:
print(f"â Supplier {supplier} tidak ditemukan")
return False
# Hitung saldo terakhir
saldo_terakhir = 0
if sibal_data.buku_besar_pembantu[supplier]:
saldo_terakhir = sibal_data.buku_besar_pembantu[supplier][-1]['saldo']
if jumlah > saldo_terakhir:
print(f"â Jumlah pembayaran melebihi utang. Utang: {saldo_terakhir}, Bayar: {jumlah}")
return False
saldo_baru = saldo_terakhir - jumlah
# Tambahkan transaksi pembayaran
sibal_data.buku_besar_pembantu[supplier].append({
'tanggal': tanggal,
'keterangan': keterangan if keterangan else f'Pembayaran utang',
'debit': jumlah, # Pembayaran mengurangi utang
'kredit': 0,
'saldo': saldo_baru
})
# Buat jurnal untuk pembayaran
jurnal_pembayaran = {
'tanggal': tanggal,
'keterangan': f'Pembayaran utang kepada {supplier}',
'ref': f'PAY-{supplier}',
'akun_debit': KODE_AKUN['utang']['nama'],
'kode_akun_debit': KODE_AKUN['utang']['kode'],
'jumlah_debit': jumlah,
'akun_kredit': KODE_AKUN['kas']['nama'],
'kode_akun_kredit': KODE_AKUN['kas']['kode'],
'jumlah_kredit': jumlah
}
jurnal_pembayaran['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(jurnal_pembayaran)
print(f"â
Pembayaran utang {supplier}: Rp{jumlah:,}, sisa: Rp{saldo_baru:,}")
sibal_data.save_all_data()
return True
except Exception as e:
print(f"â Error bayar utang: {e}")
return False
@app.callback(
Output('hidden-trigger-buku-pembantu', 'children'),
[Input('url', 'pathname'),
Input('btn-refresh-buku-pembantu', 'n_clicks')],
prevent_initial_call=True
)
def trigger_buku_pembantu_update(pathname, n_clicks):
"""Trigger untuk memuat ulang data buku besar pembantu"""
if pathname == '/buku-besar-pembantu' or n_clicks:
# Process ulang data utang dari transaksi
sibal_data.buku_besar_pembantu = {}
sibal_data.suppliers = []
# Kumpulkan semua supplier dari transaksi kredit
suppliers_set = set()
for trans in sibal_data.transaksi_pengeluaran:
if trans.get('metode_bayar') == 'kredit' and trans.get('supplier'):
suppliers_set.add(trans['supplier'])
sibal_data.suppliers = list(suppliers_set)
# Process transaksi untuk setiap supplier
for supplier in sibal_data.suppliers:
sibal_data.buku_besar_pembantu[supplier] = []
# Cari semua transaksi kredit untuk supplier ini
transaksi_supplier = [t for t in sibal_data.transaksi_pengeluaran
if t.get('supplier') == supplier and t.get('metode_bayar') == 'kredit']
saldo = 0
for trans in transaksi_supplier:
jumlah = trans['jumlah']
saldo += jumlah
# Tambahkan transaksi utang
sibal_data.buku_besar_pembantu[supplier].append({
'tanggal': trans['tanggal'],
'keterangan': f"{trans['jenis']} - {trans['keterangan']}",
'debit': 0,
'kredit': jumlah,
'saldo': saldo
})
return f"updated-{datetime.now().timestamp()}"
return dash.no_update
# Callback untuk menyimpan aset tetap
@app.callback(
[Output('nilai-aset', 'value'),
Output('masa-manfaat', 'value'),
Output('nilai-residu', 'value')],
Input('btn-simpan-aset', 'n_clicks'),
[State('jenis-aset', 'value'),
State('nilai-aset', 'value'),
State('tanggal-perolehan', 'date'),
State('masa-manfaat', 'value'),
State('metode-penyusutan', 'value'),
State('nilai-residu', 'value')],
prevent_initial_call=True
)
def simpan_aset_tetap(n_clicks, jenis_aset, nilai_aset, tanggal_perolehan, masa_manfaat, metode, nilai_residu):
if n_clicks and n_clicks > 0 and jenis_aset and nilai_aset and masa_manfaat:
# Simpan ke data aset tetap
sibal_data.aset_tetap[jenis_aset] = {
'nilai_awal': nilai_aset,
'penyusutan': 0, # Akan dihitung nanti
'masa_manfaat': masa_manfaat,
'tahun_pembelian': datetime.fromisoformat(tanggal_perolehan).year,
'metode_penyusutan': metode,
'nilai_residu': nilai_residu if nilai_residu else 0,
'tanggal_perolehan': tanggal_perolehan
}
# Buat jurnal untuk pembelian aset
if jenis_aset == 'tanah':
kode_akun_debit = KODE_AKUN['tanah']['kode']
nama_akun_debit = KODE_AKUN['tanah']['nama']
elif jenis_aset == 'bangunan_gazebo':
kode_akun_debit = KODE_AKUN['bangunan_gazebo']['kode']
nama_akun_debit = KODE_AKUN['bangunan_gazebo']['nama']
elif jenis_aset == 'kendaraan':
kode_akun_debit = KODE_AKUN['kendaraan']['kode']
nama_akun_debit = KODE_AKUN['kendaraan']['nama']
else: # peralatan
kode_akun_debit = KODE_AKUN['peralatan']['kode']
nama_akun_debit = KODE_AKUN['peralatan']['nama']
# Jurnal pembelian aset
entri_jurnal = {
'tanggal': tanggal_perolehan,
'keterangan': f'Pembelian {nama_akun_debit}',
'ref': f'AST-{jenis_aset.upper()}',
'akun_debit': nama_akun_debit,
'kode_akun_debit': kode_akun_debit,
'jumlah_debit': nilai_aset,
'akun_kredit': KODE_AKUN['kas']['nama'],
'kode_akun_kredit': KODE_AKUN['kas']['kode'],
'jumlah_kredit': nilai_aset
}
entri_jurnal['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_umum.append(entri_jurnal)
sibal_data.save_data()
return '', '', ''
return dash.no_update
# Callback untuk menampilkan daftar aset tetap
@app.callback(
Output('daftar-aset-tetap', 'children'),
[Input('btn-simpan-aset', 'n_clicks'),
Input('btn-hitung-penyusutan', 'n_clicks')]
)
def update_daftar_aset(n_clicks_simpan, n_clicks_hitung):
if sibal_data.aset_tetap:
header = html.Tr([
html.Th("Jenis Aset"),
html.Th("Nilai Awal"),
html.Th("Penyusutan"),
html.Th("Nilai Buku"),
html.Th("Masa Manfaat"),
html.Th("Tahun Pembelian")
])
rows = []
for jenis_aset, data in sibal_data.aset_tetap.items():
if data['nilai_awal'] > 0: # Hanya tampilkan aset yang sudah diisi
nilai_buku = data['nilai_awal'] - data['penyusutan']
rows.append(html.Tr([
html.Td(jenis_aset.replace('_', ' ').title()),
html.Td(format_rupiah(data['nilai_awal'])),
html.Td(format_rupiah(data['penyusutan'])),
html.Td(format_rupiah(nilai_buku)),
html.Td(f"{data['masa_manfaat']} tahun"),
html.Td(data['tahun_pembelian'])
]))
if rows:
return html.Table([header] + rows, className="modern-table")
return html.P("Belum ada data aset tetap", style={'color': '#6c757d'})
@app.callback(
Output('tabel-neraca-saldo', 'children'),
Input('btn-generate-neraca-saldo', 'n_clicks'),
State('tanggal-neraca-saldo', 'date'),
prevent_initial_call=True
)
def generate_neraca_saldo(n_clicks, tanggal):
if n_clicks and n_clicks > 0:
# Hitung saldo setiap akun dari jurnal umum
saldo_akun = {}
for jurnal in sibal_data.jurnal_umum:
# Proses akun debit
kode_debit = jurnal['kode_akun_debit']
if kode_debit not in saldo_akun:
saldo_akun[kode_debit] = {'debit': 0, 'kredit': 0, 'nama': jurnal['akun_debit']}
saldo_akun[kode_debit]['debit'] += jurnal['jumlah_debit']
# Proses akun kredit
kode_kredit = jurnal['kode_akun_kredit']
if kode_kredit not in saldo_akun:
saldo_akun[kode_kredit] = {'debit': 0, 'kredit': 0, 'nama': jurnal['akun_kredit']}
saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit']
# Buat tabel neraca saldo
header = html.Tr([
html.Th("Kode Akun"),
html.Th("Nama Akun"),
html.Th("Debit"),
html.Th("Kredit")
])
rows = []
total_debit = 0
total_kredit = 0
for kode_akun, data in sorted(saldo_akun.items()):
saldo_debit = max(0, data['debit'] - data['kredit'])
saldo_kredit = max(0, data['kredit'] - data['debit'])
total_debit += saldo_debit
total_kredit += saldo_kredit
rows.append(html.Tr([
html.Td(kode_akun),
html.Td(data['nama']),
html.Td(format_rupiah(saldo_debit) if saldo_debit > 0 else ""),
html.Td(format_rupiah(saldo_kredit) if saldo_kredit > 0 else "")
]))
# Baris total
rows.append(html.Tr([
html.Td("TOTAL", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_debit), style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_kredit), style={'fontWeight': 'bold', 'borderTop': '2px solid #000'})
]))
return html.Table([header] + rows, className="modern-table")
return html.P("Klik 'Generate Neraca Saldo' untuk melihat neraca saldo", style={'color': '#6c757d'})
# Callback untuk menghitung penyusutan
@app.callback(
Output('kalkulator-penyusutan', 'children'),
Input('btn-hitung-penyusutan', 'n_clicks'),
prevent_initial_call=True
)
def hitung_penyusutan(n_clicks):
if n_clicks and n_clicks > 0:
jurnal_penyesuaian_aset = []
for jenis_aset, data in sibal_data.aset_tetap.items():
if data['nilai_awal'] > 0 and jenis_aset != 'tanah': # Tanah tidak disusutkan
# Hitung penyusutan tahunan dengan metode garis lurus
nilai_penyusutan = data['nilai_awal'] / data['masa_manfaat']
# Update data penyusutan
sibal_data.aset_tetap[jenis_aset]['penyusutan'] += nilai_penyusutan
# Buat jurnal penyesuaian untuk penyusutan
if jenis_aset == 'bangunan_gazebo':
kode_akun_beban = KODE_AKUN['beban_penyusutan_bangunan']['kode']
nama_akun_beban = KODE_AKUN['beban_penyusutan_bangunan']['nama']
kode_akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_bangunan']['kode']
nama_akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_bangunan']['nama']
elif jenis_aset == 'kendaraan':
kode_akun_beban = KODE_AKUN['beban_penyusutan_kendaraan']['kode']
nama_akun_beban = KODE_AKUN['beban_penyusutan_kendaraan']['nama']
kode_akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_kendaraan']['kode']
nama_akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_kendaraan']['nama']
else: # peralatan
kode_akun_beban = KODE_AKUN['beban_penyusutan_peralatan']['kode']
nama_akun_beban = KODE_AKUN['beban_penyusutan_peralatan']['nama']
kode_akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_peralatan']['kode']
nama_akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_peralatan']['nama']
jurnal_penyesuaian = {
'tanggal': datetime.now().date().isoformat(),
'keterangan': f'Penyusutan {jenis_aset.replace("_", " ").title()}',
'ref': f'DEP-{jenis_aset.upper()}',
'akun_debit': nama_akun_beban,
'kode_akun_debit': kode_akun_beban,
'jumlah_debit': nilai_penyusutan,
'akun_kredit': nama_akun_akumulasi,
'kode_akun_kredit': kode_akun_akumulasi,
'jumlah_kredit': nilai_penyusutan
}
jurnal_penyesuaian_aset.append(jurnal_penyesuaian)
jurnal_penyesuaian['user_id'] = current_user.get_id() if current_user and current_user.get_id() else None
sibal_data._all_jurnal_penyesuaian.append(jurnal_penyesuaian)
sibal_data.save_data()
# Tampilkan hasil perhitungan
if jurnal_penyesuaian_aset:
header = html.Tr([
html.Th("Jenis Aset"),
html.Th("Penyusutan Tahunan"),
html.Th("Akun Beban"),
html.Th("Akun Akumulasi")
])
rows = []
for jurnal in jurnal_penyesuaian_aset:
jenis_aset = jurnal['keterangan'].split(' ')[1]
rows.append(html.Tr([
html.Td(jenis_aset),
html.Td(format_rupiah(jurnal['jumlah_debit'])),
html.Td(jurnal['akun_debit']),
html.Td(jurnal['akun_kredit'])
]))
return html.Div([
html.H4("â
Penyusutan Berhasil Dihitung!", style={'color': '#28a745'}),
html.Table([header] + rows, className="modern-table")
])
return html.P("Klik 'Hitung Penyusutan' untuk menghitung penyusutan aset", style={'color': '#6c757d'})
@app.callback(
Output('daftar-jurnal-penyesuaian', 'children'),
Input('btn-generate-penyesuaian', 'n_clicks'),
State('tanggal-penyesuaian', 'date'),
prevent_initial_call=True
)
def generate_jurnal_penyesuaian(n_clicks, tanggal):
if n_clicks and n_clicks > 0:
# Otomatis buat jurnal penyesuaian untuk penyusutan aset
jurnal_penyesuaian = []
# 1. Jurnal penyesuaian untuk penyusutan aset
for jenis_aset, data in sibal_data.aset_tetap.items():
if data['nilai_awal'] > 0 and jenis_aset != 'tanah':
# Hitung penyusutan
nilai_penyusutan = data['nilai_awal'] / data['masa_manfaat']
if jenis_aset == 'bangunan_gazebo':
akun_beban = KODE_AKUN['beban_penyusutan_bangunan']['nama']
akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_bangunan']['nama']
elif jenis_aset == 'kendaraan':
akun_beban = KODE_AKUN['beban_penyusutan_kendaraan']['nama']
akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_kendaraan']['nama']
else: # peralatan
akun_beban = KODE_AKUN['beban_penyusutan_peralatan']['nama']
akun_akumulasi = KODE_AKUN['akumulasi_penyusutan_peralatan']['nama']
jurnal_penyesuaian.append({
'tanggal': tanggal,
'keterangan': f'Penyusutan {jenis_aset.replace("_", " ").title()}',
'akun_debit': akun_beban,
'debit': nilai_penyusutan,
'akun_kredit': akun_akumulasi,
'kredit': nilai_penyusutan
})
# 2. Jurnal penyesuaian untuk beban yang masih harus dibayar
# (Contoh: Beban gaji yang belum dibayar)
total_beban_gaji = sum(t['jumlah'] for t in sibal_data.transaksi_pengeluaran
if t['jenis'] == 'beban_gaji' and t['metode_bayar'] == 'kredit')
if total_beban_gaji > 0:
jurnal_penyesuaian.append({
'tanggal': tanggal,
'keterangan': 'Beban gaji yang masih harus dibayar',
'akun_debit': KODE_AKUN['beban_gaji']['nama'],
'debit': total_beban_gaji,
'akun_kredit': KODE_AKUN['utang_gaji']['nama'],
'kredit': total_beban_gaji
})
# Tampilkan jurnal penyesuaian
if jurnal_penyesuaian:
header = html.Tr([
html.Th("Tanggal"),
html.Th("Keterangan"),
html.Th("Akun Debit"),
html.Th("Debit"),
html.Th("Akun Kredit"),
html.Th("Kredit")
])
rows = []
for jurnal in jurnal_penyesuaian:
rows.append(html.Tr([
html.Td(jurnal['tanggal']),
html.Td(jurnal['keterangan']),
html.Td(jurnal['akun_debit']),
html.Td(format_rupiah(jurnal['debit'])),
html.Td(jurnal['akun_kredit']),
html.Td(format_rupiah(jurnal['kredit']))
]))
return html.Table([header] + rows, className="modern-table")
else:
return html.P("Tidak ada jurnal penyesuaian yang diperlukan", style={'color': '#6c757d'})
return html.P("Klik 'Generate Jurnal Penyesuaian' untuk membuat jurnal penyesuaian", style={'color': '#6c757d'})
@app.callback(
Output('tabel-neraca-penyesuaian', 'children'),
Input('btn-generate-neraca-penyesuaian', 'n_clicks'),
State('tanggal-neraca-penyesuaian', 'date'),
prevent_initial_call=True
)
def generate_neraca_setelah_penyesuaian(n_clicks, tanggal):
if n_clicks and n_clicks > 0:
# Hitung saldo setiap akun dari jurnal umum + jurnal penyesuaian
saldo_akun = {}
# Proses jurnal umum
for jurnal in sibal_data.jurnal_umum:
kode_debit = jurnal['kode_akun_debit']
if kode_debit not in saldo_akun:
saldo_akun[kode_debit] = {
'debit': 0,
'kredit': 0,
'nama': jurnal['akun_debit'],
'tipe': 'aktiva' if kode_debit.startswith('1') else 'pasiva' if kode_debit.startswith('2') else 'modal' if kode_debit.startswith('3') else 'pendapatan' if kode_debit.startswith('4') else 'beban'
}
saldo_akun[kode_debit]['debit'] += jurnal['jumlah_debit']
kode_kredit = jurnal['kode_akun_kredit']
if kode_kredit not in saldo_akun:
saldo_akun[kode_kredit] = {
'debit': 0,
'kredit': 0,
'nama': jurnal['akun_kredit'],
'tipe': 'aktiva' if kode_kredit.startswith('1') else 'pasiva' if kode_kredit.startswith('2') else 'modal' if kode_kredit.startswith('3') else 'pendapatan' if kode_kredit.startswith('4') else 'beban'
}
saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit']
# Proses jurnal penyesuaian
for jurnal in sibal_data.jurnal_penyesuaian:
kode_debit = jurnal['kode_akun_debit']
if kode_debit not in saldo_akun:
saldo_akun[kode_debit] = {
'debit': 0,
'kredit': 0,
'nama': jurnal['akun_debit'],
'tipe': 'aktiva' if kode_debit.startswith('1') else 'pasiva' if kode_debit.startswith('2') else 'modal' if kode_debit.startswith('3') else 'pendapatan' if kode_debit.startswith('4') else 'beban'
}
saldo_akun[kode_debit]['debit'] += jurnal['jumlah_debit']
kode_kredit = jurnal['kode_akun_kredit']
if kode_kredit not in saldo_akun:
saldo_akun[kode_kredit] = {
'debit': 0,
'kredit': 0,
'nama': jurnal['akun_kredit'],
'tipe': 'aktiva' if kode_kredit.startswith('1') else 'pasiva' if kode_kredit.startswith('2') else 'modal' if kode_kredit.startswith('3') else 'pendapatan' if kode_kredit.startswith('4') else 'beban'
}
saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit']
# Kelompokkan akun berdasarkan tipe
aktiva = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'aktiva'}
pasiva = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'pasiva'}
modal = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'modal'}
pendapatan = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'pendapatan'}
beban = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'beban'}
# Hitung total aktiva
total_aktiva = 0
rows_aktiva = []
for kode, data in sorted(aktiva.items()):
saldo = data['debit'] - data['kredit']
if saldo != 0: # Hanya tampilkan akun dengan saldo
total_aktiva += saldo
rows_aktiva.append(html.Tr([
html.Td(kode),
html.Td(data['nama']),
html.Td(format_rupiah(saldo))
]))
# Hitung total pasiva + modal
total_pasiva_modal = 0
rows_pasiva = []
rows_modal = []
for kode, data in sorted(pasiva.items()):
saldo = data['kredit'] - data['debit']
if saldo != 0:
total_pasiva_modal += saldo
rows_pasiva.append(html.Tr([
html.Td(kode),
html.Td(data['nama']),
html.Td(format_rupiah(saldo))
]))
for kode, data in sorted(modal.items()):
saldo = data['kredit'] - data['debit']
if saldo != 0:
total_pasiva_modal += saldo
rows_modal.append(html.Tr([
html.Td(kode),
html.Td(data['nama']),
html.Td(format_rupiah(saldo))
]))
# Hitung laba/rugi dari pendapatan dan beban
total_pendapatan = sum(data['kredit'] - data['debit'] for data in pendapatan.values())
total_beban = sum(data['debit'] - data['kredit'] for data in beban.values())
laba_rugi = total_pendapatan - total_beban
# Tambahkan laba/rugi ke modal
if laba_rugi != 0:
total_pasiva_modal += laba_rugi
rows_modal.append(html.Tr([
html.Td(""),
html.Td("Laba (Rugi) Berjalan", style={'fontStyle': 'italic'}),
html.Td(format_rupiah(laba_rugi))
]))
# Buat tabel neraca
return html.Div([
html.H4(f"Neraca Setelah Penyesuaian per {tanggal}",
style={'textAlign': 'center', 'marginBottom': '30px', 'color': COLORS['primary']}),
html.Div([
html.Div([
html.H5("AKTIVA", style={'color': COLORS['primary'], 'marginBottom': '20px'}),
html.Table([
html.Tr([html.Th("Kode"), html.Th("Nama Akun"), html.Th("Saldo")])
] + rows_aktiva + [
html.Tr([
html.Td("", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_aktiva),
style={'fontWeight': 'bold', 'borderTop': '2px solid #000'})
])
], className="modern-table", style={'width': '100%'})
], style={'flex': 1, 'marginRight': '20px'}),
html.Div([
html.H5("PASIVA & MODAL", style={'color': COLORS['secondary'], 'marginBottom': '20px'}),
html.H6("Kewajiban", style={'color': COLORS['gray_600'], 'marginTop': '15px'}),
html.Table([
html.Tr([html.Th("Kode"), html.Th("Nama Akun"), html.Th("Saldo")])
] + rows_pasiva, className="modern-table", style={'width': '100%'}),
html.H6("Modal", style={'color': COLORS['gray_600'], 'marginTop': '15px'}),
html.Table([
html.Tr([html.Th("Kode"), html.Th("Nama Akun"), html.Th("Saldo")])
] + rows_modal + [
html.Tr([
html.Td("", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_pasiva_modal),
style={'fontWeight': 'bold', 'borderTop': '2px solid #000'})
])
], className="modern-table", style={'width': '100%'})
], style={'flex': 1})
], style={'display': 'flex', 'gap': '20px'}),
html.Div([
html.H5("KESEIMBANGAN NERACA", style={
'textAlign': 'center',
'color': COLORS['success'] if abs(total_aktiva - total_pasiva_modal) < 0.01 else COLORS['error'],
'marginTop': '30px'
}),
html.P(f"Total Aktiva: {format_rupiah(total_aktiva)}",
style={'textAlign': 'center', 'fontWeight': 'bold'}),
html.P(f"Total Pasiva + Modal: {format_rupiah(total_pasiva_modal)}",
style={'textAlign': 'center', 'fontWeight': 'bold'}),
html.P("â
Neraca Seimbang" if abs(total_aktiva - total_pasiva_modal) < 0.01 else "â Neraca Tidak Seimbang",
style={'textAlign': 'center', 'fontWeight': 'bold',
'color': COLORS['success'] if abs(total_aktiva - total_pasiva_modal) < 0.01 else COLORS['error']})
])
])
return html.P("Klik 'Generate Neraca Setelah Penyesuaian' untuk melihat neraca", style={'color': '#6c757d'})
@app.callback(
Output('tabel-laba-rugi', 'children'),
Input('btn-generate-laba-rugi', 'n_clicks'),
State('tanggal-laba-rugi', 'date'),
prevent_initial_call=True
)
def generate_laporan_laba_rugi(n_clicks, tanggal):
if n_clicks and n_clicks > 0:
# Hitung saldo setiap akun dari jurnal umum + jurnal penyesuaian
saldo_akun = {}
# Proses semua jurnal
semua_jurnal = sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian
for jurnal in semua_jurnal:
# Akun debit
kode_debit = jurnal['kode_akun_debit']
if kode_debit not in saldo_akun:
saldo_akun[kode_debit] = {
'debit': 0,
'kredit': 0,
'nama': jurnal['akun_debit'],
'tipe': 'pendapatan' if kode_debit.startswith('4') else 'beban' if kode_debit.startswith('5') or kode_debit.startswith('6') else 'lainnya'
}
saldo_akun[kode_debit]['debit'] += jurnal['jumlah_debit']
# Akun kredit
kode_kredit = jurnal['kode_akun_kredit']
if kode_kredit not in saldo_akun:
saldo_akun[kode_kredit] = {
'debit': 0,
'kredit': 0,
'nama': jurnal['akun_kredit'],
'tipe': 'pendapatan' if kode_kredit.startswith('4') else 'beban' if kode_kredit.startswith('5') or kode_kredit.startswith('6') else 'lainnya'
}
saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit']
# Kelompokkan pendapatan dan beban
pendapatan = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'pendapatan'}
beban = {k: v for k, v in saldo_akun.items() if v['tipe'] == 'beban'}
# Hitung total pendapatan
total_pendapatan = 0
rows_pendapatan = []
for kode, data in sorted(pendapatan.items()):
saldo = data['kredit'] - data['debit'] # Pendapatan di kredit
if saldo != 0:
total_pendapatan += saldo
rows_pendapatan.append(html.Tr([
html.Td(kode),
html.Td(data['nama']),
html.Td(format_rupiah(saldo))
]))
# Hitung total beban
total_beban = 0
rows_beban = []
for kode, data in sorted(beban.items()):
saldo = data['debit'] - data['kredit'] # Beban di debit
if saldo != 0:
total_beban += saldo
rows_beban.append(html.Tr([
html.Td(kode),
html.Td(data['nama']),
html.Td(format_rupiah(saldo))
]))
# Hitung laba/rugi
laba_rugi = total_pendapatan - total_beban
margin_keuntungan = (laba_rugi / total_pendapatan * 100) if total_pendapatan > 0 else 0
return html.Div([
html.H4(f"Laporan Laba Rugi per {tanggal}",
style={'textAlign': 'center', 'marginBottom': '30px', 'color': COLORS['primary']}),
# Bagian Pendapatan
html.Div([
html.H5("PENDAPATAN", style={'color': COLORS['success'], 'marginBottom': '15px'}),
html.Table([
html.Tr([html.Th("Kode"), html.Th("Jenis Pendapatan"), html.Th("Jumlah")])
] + rows_pendapatan + [
html.Tr([
html.Td("", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_pendapatan),
style={'fontWeight': 'bold', 'borderTop': '2px solid #000'})
])
], className="modern-table")
], style={'marginBottom': '30px'}),
# Bagian Beban
html.Div([
html.H5("BEBAN", style={'color': COLORS['error'], 'marginBottom': '15px'}),
html.Table([
html.Tr([html.Th("Kode"), html.Th("Jenis Beban"), html.Th("Jumlah")])
] + rows_beban + [
html.Tr([
html.Td("", colSpan=2, style={'fontWeight': 'bold', 'borderTop': '2px solid #000'}),
html.Td(format_rupiah(total_beban),
style={'fontWeight': 'bold', 'borderTop': '2px solid #000'})
])
], className="modern-table")
], style={'marginBottom': '30px'}),
# Hasil Laba/Rugi
html.Div([
html.H4("HASIL USAHA", style={
'textAlign': 'center',
'color': COLORS['success'] if laba_rugi >= 0 else COLORS['error'],
'padding': '20px',
'backgroundColor': COLORS['accent_mint'] if laba_rugi >= 0 else COLORS['accent_mint_light'],
'borderRadius': '15px',
'border': f'2px solid {COLORS["success"] if laba_rugi >= 0 else COLORS["error"]}'
}),
html.Div([
html.Div([
html.P("Total Pendapatan", style={'fontWeight': 'bold'}),
html.P(format_rupiah(total_pendapatan), style={'fontSize': '1.2rem', 'color': COLORS['success']})
], style={'textAlign': 'center', 'flex': 1}),
html.Div([
html.P("â", style={'fontSize': '2rem', 'margin': '0 20px'})
], style={'display': 'flex', 'alignItems': 'center'}),
html.Div([
html.P("Total Beban", style={'fontWeight': 'bold'}),
html.P(format_rupiah(total_beban), style={'fontSize': '1.2rem', 'color': COLORS['error']})
], style={'textAlign': 'center', 'flex': 1}),
html.Div([
html.P("đ°", style={'fontSize': '2rem', 'margin': '0 20px'})
], style={'display': 'flex', 'alignItems': 'center'}),
html.Div([
html.P("Laba (Rugi) Bersih", style={'fontWeight': 'bold'}),
html.P(format_rupiah(abs(laba_rugi)),
style={'fontSize': '1.5rem', 'fontWeight': 'bold',
'color': COLORS['success'] if laba_rugi >= 0 else COLORS['error']})
], style={'textAlign': 'center', 'flex': 1})
], style={'display': 'flex', 'justifyContent': 'center', 'alignItems': 'center', 'marginTop': '20px'}),
html.Div([
html.P(f"Margin Keuntungan: {margin_keuntungan:.1f}%",
style={'textAlign': 'center', 'fontSize': '1.1rem', 'color': COLORS['gray_600']})
], style={'marginTop': '15px'})
])
])
return html.P("Klik 'Generate Laporan Laba Rugi' untuk melihat laporan", style={'color': '#6c757d'})
def neraca_lajur_layout():
return html.Div([
html.Div([
html.Div([
html.Div([
html.I(className="fas fa-table")
], className="card-icon"),
html.H1("Neraca Lajur", className="card-title"),
html.P("Worksheet - Laporan Keuangan Komprehensif", className="card-subtitle")
], className="card-header"),
html.Div([
html.Div([
html.Label("Periode Neraca Lajur:", className="form-label"),
dcc.DatePickerRange(
id='periode-neraca-lajur',
start_date=datetime.now().date().replace(day=1), # awal bulan
end_date=datetime.now().date(),
display_format='YYYY-MM-DD',
className="form-control"
)
], className="form-group", style={'marginBottom': '20px'}),
html.Button(
"đ Generate Neraca Lajur",
id='btn-generate-neraca-lajur',
className="btn btn-primary",
style={'marginBottom': '20px'}
),
html.Div(id='tabel-neraca-lajur', className="table-container")
], className="card-content")
], className="glass-card")
], className="main-container")
# Callback untuk generate neraca lajur
@app.callback(
Output('tabel-neraca-lajur', 'children'),
Input('btn-generate-neraca-lajur', 'n_clicks'),
[State('periode-neraca-lajur', 'start_date'),
State('periode-neraca-lajur', 'end_date')]
)
def generate_neraca_lajur(n_clicks, start_date, end_date):
if n_clicks and n_clicks > 0:
return create_neraca_lajur_table(start_date, end_date)
return html.P("Pilih periode dan klik 'Generate Neraca Lajur'", style={'color': '#6c757d'})
def create_neraca_lajur_table(start_date, end_date):
"""Membuat tabel neraca lajur dari data aktual"""
# Hitung saldo dari semua akun sebelum penyesuaian (hanya jurnal umum)
saldo_neraca_saldo = calculate_saldo_akun_periode(sibal_data.jurnal_umum, start_date, end_date)
# Hitung saldo penyesuaian (hanya jurnal penyesuaian)
saldo_penyesuaian = calculate_saldo_akun_periode(sibal_data.jurnal_penyesuaian, start_date, end_date)
# Hitung saldo setelah penyesuaian (gabungan jurnal umum + penyesuaian)
saldo_setelah_penyesuaian = calculate_saldo_akun_periode(
sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian, start_date, end_date
)
# Klasifikasikan akun untuk laba rugi dan posisi keuangan
akun_laba_rugi = ['pendapatan', 'pendapatan_tiket', 'hpp', 'beban_gaji', 'beban_listrik',
'beban_penyusutan_bangunan', 'beban_penyusutan_kendaraan',
'beban_penyusutan_peralatan', 'beban_lainnya']
akun_posisi_keuangan = ['kas', 'persediaan', 'perlengkapan', 'peralatan', 'tanah',
'bangunan_gazebo', 'akumulasi_penyusutan_bangunan', 'kendaraan',
'akumulasi_penyusutan_kendaraan', 'akumulasi_penyusutan_peralatan',
'utang', 'utang_gaji', 'modal']
# Header tabel
header = html.Tr([
html.Th("Kode Akun", rowSpan=2, style={'backgroundColor': '#343a40', 'color': 'white', 'padding': '10px', 'minWidth': '80px'}),
html.Th("Nama Akun", rowSpan=2, style={'backgroundColor': '#343a40', 'color': 'white', 'padding': '10px', 'minWidth': '200px'}),
html.Th("Neraca Saldo Sebelum Penyesuaian", colSpan=2, style={'backgroundColor': '#007bff', 'color': 'white', 'textAlign': 'center'}),
html.Th("Penyesuaian", colSpan=2, style={'backgroundColor': '#28a745', 'color': 'white', 'textAlign': 'center'}),
html.Th("Neraca Saldo Setelah Penyesuaian", colSpan=2, style={'backgroundColor': '#ffc107', 'color': 'white', 'textAlign': 'center'}),
html.Th("Laporan Laba Rugi", colSpan=2, style={'backgroundColor': '#dc3545', 'color': 'white', 'textAlign': 'center'}),
html.Th("Laporan Posisi Keuangan", colSpan=2, style={'backgroundColor': '#6f42c1', 'color': 'white', 'textAlign': 'center'})
])
sub_header = html.Tr([
html.Th("", style={'backgroundColor': '#343a40', 'color': 'white'}), # Kode Akun
html.Th("", style={'backgroundColor': '#343a40', 'color': 'white'}), # Nama Akun
html.Th("Debit", style={'backgroundColor': '#007bff', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Kredit", style={'backgroundColor': '#007bff', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Debit", style={'backgroundColor': '#28a745', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Kredit", style={'backgroundColor': '#28a745', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Debit", style={'backgroundColor': '#ffc107', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Kredit", style={'backgroundColor': '#ffc107', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Debit", style={'backgroundColor': '#dc3545', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Kredit", style={'backgroundColor': '#dc3545', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Debit", style={'backgroundColor': '#6f42c1', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'}),
html.Th("Kredit", style={'backgroundColor': '#6f42c1', 'color': 'white', 'textAlign': 'right', 'minWidth': '120px'})
])
rows = []
# Kumpulkan semua akun yang ada saldo
semua_akun = set(saldo_neraca_saldo.keys()).union(
set(saldo_penyesuaian.keys()),
set(saldo_setelah_penyesuaian.keys())
)
# Urutkan akun berdasarkan kode
akun_terurut = []
for akun in semua_akun:
kode = KODE_AKUN.get(akun, {}).get('kode', '')
akun_terurut.append((kode, akun))
akun_terurut.sort()
# Variabel untuk total
total_debit_neraca = 0
total_kredit_neraca = 0
total_debit_penyesuaian = 0
total_kredit_penyesuaian = 0
total_debit_setelah = 0
total_kredit_setelah = 0
total_debit_laba_rugi = 0
total_kredit_laba_rugi = 0
total_debit_posisi = 0
total_kredit_posisi = 0
# Buat baris untuk setiap akun
for kode, akun in akun_terurut:
nama_akun = KODE_AKUN.get(akun, {}).get('nama', akun)
# Saldo neraca saldo (sebelum penyesuaian)
saldo_ns = saldo_neraca_saldo.get(akun, 0)
debit_ns = saldo_ns if saldo_ns > 0 else 0
kredit_ns = abs(saldo_ns) if saldo_ns < 0 else 0
# Saldo penyesuaian
saldo_pen = saldo_penyesuaian.get(akun, 0)
debit_pen = saldo_pen if saldo_pen > 0 else 0
kredit_pen = abs(saldo_pen) if saldo_pen < 0 else 0
# Saldo setelah penyesuaian
saldo_setelah = saldo_setelah_penyesuaian.get(akun, 0)
debit_setelah = saldo_setelah if saldo_setelah > 0 else 0
kredit_setelah = abs(saldo_setelah) if saldo_setelah < 0 else 0
# Klasifikasi untuk laba rugi dan posisi keuangan
if akun in akun_laba_rugi:
debit_lr = debit_setelah
kredit_lr = kredit_setelah
debit_pk = 0
kredit_pk = 0
elif akun in akun_posisi_keuangan:
debit_lr = 0
kredit_lr = 0
debit_pk = debit_setelah
kredit_pk = kredit_setelah
else:
# Default: masukkan ke posisi keuangan
debit_lr = 0
kredit_lr = 0
debit_pk = debit_setelah
kredit_pk = kredit_setelah
# Tambahkan ke total
total_debit_neraca += debit_ns
total_kredit_neraca += kredit_ns
total_debit_penyesuaian += debit_pen
total_kredit_penyesuaian += kredit_pen
total_debit_setelah += debit_setelah
total_kredit_setelah += kredit_setelah
total_debit_laba_rugi += debit_lr
total_kredit_laba_rugi += kredit_lr
total_debit_posisi += debit_pk
total_kredit_posisi += kredit_pk
# Buat baris
rows.append(html.Tr([
html.Td(kode, style={'padding': '8px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold'}),
html.Td(nama_akun, style={'padding': '8px', 'border': '1px solid #dee2e6'}),
html.Td(format_currency(debit_ns), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e3f2fd'}),
html.Td(format_currency(kredit_ns), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e3f2fd'}),
html.Td(format_currency(debit_pen), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e8f5e8'}),
html.Td(format_currency(kredit_pen), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e8f5e8'}),
html.Td(format_currency(debit_setelah), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#fff3cd'}),
html.Td(format_currency(kredit_setelah), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#fff3cd'}),
html.Td(format_currency(debit_lr), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#f8d7da'}),
html.Td(format_currency(kredit_lr), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#f8d7da'}),
html.Td(format_currency(debit_pk), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e2e3f5'}),
html.Td(format_currency(kredit_pk), style={'padding': '8px', 'border': '1px solid #dee2e6', 'textAlign': 'right', 'backgroundColor': '#e2e3f5'})
]))
# Baris jumlah
rows.append(html.Tr([
html.Td("", colSpan=2, style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'center', 'backgroundColor': '#e9ecef'}, children=["Jumlah"]),
html.Td(format_currency(total_debit_neraca), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_neraca), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_debit_penyesuaian), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_penyesuaian), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_debit_setelah), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_setelah), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_debit_laba_rugi), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_laba_rugi), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_debit_posisi), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_posisi), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'})
]))
# Baris laba (rugi) bersih
laba_bersih = total_kredit_laba_rugi - total_debit_laba_rugi
rows.append(html.Tr([
html.Td("", colSpan=8, style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'center', 'backgroundColor': '#f8f9fa'}, children=["Laba (Rugi) Bersih"]),
html.Td(format_currency(laba_bersih), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#f8f9fa'}),
html.Td("", style={'padding': '10px', 'border': '1px solid #dee2e6', 'backgroundColor': '#f8f9fa'}),
html.Td(format_currency(laba_bersih), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#f8f9fa'}),
html.Td("", style={'padding': '10px', 'border': '1px solid #dee2e6', 'backgroundColor': '#f8f9fa'})
]))
# Baris jumlah akhir
rows.append(html.Tr([
html.Td("", colSpan=8, style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'center', 'backgroundColor': '#e9ecef'}, children=["Jumlah"]),
html.Td(format_currency(total_kredit_laba_rugi), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_laba_rugi), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_debit_posisi + laba_bersih), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'}),
html.Td(format_currency(total_kredit_posisi + laba_bersih), style={'padding': '10px', 'border': '1px solid #dee2e6', 'fontWeight': 'bold', 'textAlign': 'right', 'backgroundColor': '#e9ecef', 'borderTop': '2px solid #000'})
]))
# Cek balancing
is_balanced = (total_debit_neraca == total_kredit_neraca and
total_debit_penyesuaian == total_kredit_penyesuaian and
total_debit_setelah == total_kredit_setelah and
total_debit_laba_rugi + laba_bersih == total_kredit_laba_rugi and
total_debit_posisi + laba_bersih == total_kredit_posisi)
status_message = html.Div([
html.H4("â
NERACA LAJUR BALANCE" if is_balanced else "â NERACA LAJUR TIDAK BALANCE",
style={'color': '#28a745' if is_balanced else '#dc3545', 'textAlign': 'center', 'marginTop': '20px'})
])
return html.Div([
html.H4(f"NERACA LAJUR - Periode {start_date} s/d {end_date}", style={
'textAlign': 'center',
'color': '#007bff',
'marginBottom': '20px'
}),
html.Div([
html.Table(
[header, sub_header] + rows,
style={
'width': '100%',
'borderCollapse': 'collapse',
'border': '1px solid #dee2e6',
'fontSize': '11px',
'overflowX': 'auto'
}
)
], style={'overflowX': 'auto', 'width': '100%'}),
status_message
])
def calculate_saldo_akun_periode(jurnal_data, start_date, end_date):
"""Hitung saldo akun untuk periode tertentu dari data jurnal"""
saldo_akun = {}
# Filter jurnal berdasarkan tanggal
jurnal_periode = [j for j in jurnal_data if start_date <= j['tanggal'] <= end_date]
for jurnal in jurnal_periode:
# Debit
akun_debit = jurnal['akun_debit']
if akun_debit not in saldo_akun:
saldo_akun[akun_debit] = 0
saldo_akun[akun_debit] += jurnal['jumlah_debit']
# Kredit
akun_kredit = jurnal['akun_kredit']
if akun_kredit not in saldo_akun:
saldo_akun[akun_kredit] = 0
saldo_akun[akun_kredit] -= jurnal['jumlah_kredit']
return saldo_akun
def format_currency(amount):
"""Format currency dengan pemisah ribuan"""
if amount == 0:
return ""
return f"Rp{amount:,.0f}"
@app.callback(
Output('tabel-laporan-keuangan', 'children'),
Input('btn-generate-laporan-keuangan', 'n_clicks'),
State('tanggal-laporan-keuangan', 'date'),
prevent_initial_call=True
)
def generate_laporan_keuangan_lengkap(n_clicks, tanggal):
if n_clicks and n_clicks > 0:
# Reuse functions from previous callbacks to get data
saldo_akun = {}
# Process all journals
semua_jurnal = sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian
for jurnal in semua_jurnal:
kode_debit = jurnal['kode_akun_debit']
if kode_debit not in saldo_akun:
saldo_akun[kode_debit] = {
'debit': 0, 'kredit': 0, 'nama': jurnal['akun_debit'],
'tipe': 'aktiva' if kode_debit.startswith('1') else 'pasiva' if kode_debit.startswith('2') else 'modal' if kode_debit.startswith('3') else 'pendapatan' if kode_debit.startswith('4') else 'beban'
}
saldo_akun[kode_debit]['debit'] += jurnal['jumlah_debit']
kode_kredit = jurnal['kode_akun_kredit']
if kode_kredit not in saldo_akun:
saldo_akun[kode_kredit] = {
'debit': 0, 'kredit': 0, 'nama': jurnal['akun_kredit'],
'tipe': 'aktiva' if kode_kredit.startswith('1') else 'pasiva' if kode_kredit.startswith('2') else 'modal' if kode_kredit.startswith('3') else 'pendapatan' if kode_kredit.startswith('4') else 'beban'
}
saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit']
# Calculate financial ratios and summaries
aktiva_lancar = sum(data['debit'] - data['kredit'] for kode, data in saldo_akun.items()
if data['tipe'] == 'aktiva' and kode in ['1-110', '1-120', '1-130']) # Kas, Persediaan, Perlengkapan
aktiva_tetap = sum(data['debit'] - data['kredit'] for kode, data in saldo_akun.items()
if data['tipe'] == 'aktiva' and kode in ['1-210', '1-220', '1-230', '1-240']) # Aset tetap
kewajiban_lancar = sum(data['kredit'] - data['debit'] for kode, data in saldo_akun.items()
if data['tipe'] == 'pasiva' and kode in ['2-110', '2-120']) # Utang
modal = sum(data['kredit'] - data['debit'] for kode, data in saldo_akun.items()
if data['tipe'] == 'modal')
pendapatan = sum(data['kredit'] - data['debit'] for kode, data in saldo_akun.items()
if data['tipe'] == 'pendapatan')
beban = sum(data['debit'] - data['kredit'] for kode, data in saldo_akun.items()
if data['tipe'] == 'beban')
laba_bersih = pendapatan - beban
# Financial ratios
rasio_likuiditas = aktiva_lancar / kewajiban_lancar if kewajiban_lancar > 0 else float('inf')
margin_laba = (laba_bersih / pendapatan * 100) if pendapatan > 0 else 0
roa = (laba_bersih / (aktiva_lancar + aktiva_tetap) * 100) if (aktiva_lancar + aktiva_tetap) > 0 else 0
return html.Div([
html.H4(f"LAPORAN KEUANGAN LENGKAP",
style={'textAlign': 'center', 'marginBottom': '10px', 'color': COLORS['primary']}),
html.H5(f"Periode sampai dengan {tanggal}",
style={'textAlign': 'center', 'marginBottom': '30px', 'color': COLORS['gray_600']}),
# Laporan Laba Rugi Section
html.Div([
html.H5("1. LAPORAN LABA RUGI", style={'color': COLORS['primary'], 'borderBottom': f'2px solid {COLORS["primary"]}', 'paddingBottom': '10px'}),
html.Div([
html.Div([
html.P("Pendapatan Usaha", style={'fontWeight': 'bold'}),
html.P(format_rupiah(pendapatan), style={'fontSize': '1.1rem', 'color': COLORS['success']})
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '10px'}),
html.Div([
html.P("Beban Usaha", style={'fontWeight': 'bold'}),
html.P(format_rupiah(beban), style={'fontSize': '1.1rem', 'color': COLORS['error']})
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '10px'}),
html.Hr(),
html.Div([
html.P("LABA (RUGI) BERSIH", style={'fontWeight': 'bold', 'fontSize': '1.2rem'}),
html.P(format_rupiah(laba_bersih),
style={'fontSize': '1.3rem', 'fontWeight': 'bold',
'color': COLORS['success'] if laba_bersih >= 0 else COLORS['error']})
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginTop': '10px'})
], style={'backgroundColor': COLORS['gray_50'], 'padding': '20px', 'borderRadius': '10px', 'marginTop': '15px'})
], style={'marginBottom': '30px'}),
# Neraca Section
html.Div([
html.H5("2. NERACA", style={'color': COLORS['secondary'], 'borderBottom': f'2px solid {COLORS["secondary"]}', 'paddingBottom': '10px'}),
html.Div([
html.Div([
html.H6("AKTIVA", style={'color': COLORS['primary']}),
html.Div([
html.P("Aktiva Lancar", style={'fontWeight': 'bold'}),
html.P(format_rupiah(aktiva_lancar))
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '5px'}),
html.Div([
html.P("Aktiva Tetap", style={'fontWeight': 'bold'}),
html.P(format_rupiah(aktiva_tetap))
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '5px'}),
html.Hr(),
html.Div([
html.P("TOTAL AKTIVA", style={'fontWeight': 'bold'}),
html.P(format_rupiah(aktiva_lancar + aktiva_tetap), style={'fontWeight': 'bold'})
], style={'display': 'flex', 'justifyContent': 'space-between'})
], style={'flex': 1, 'padding': '15px', 'backgroundColor': COLORS['accent_mint'], 'borderRadius': '10px', 'marginRight': '10px'}),
html.Div([
html.H6("PASIVA & MODAL", style={'color': COLORS['secondary']}),
html.Div([
html.P("Kewajiban Lancar", style={'fontWeight': 'bold'}),
html.P(format_rupiah(kewajiban_lancar))
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '5px'}),
html.Div([
html.P("Modal", style={'fontWeight': 'bold'}),
html.P(format_rupiah(modal))
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '5px'}),
html.Div([
html.P("Laba Ditahan", style={'fontWeight': 'bold', 'fontStyle': 'italic'}),
html.P(format_rupiah(laba_bersih), style={'fontStyle': 'italic'})
], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '5px'}),
html.Hr(),
html.Div([
html.P("TOTAL PASIVA & MODAL", style={'fontWeight': 'bold'}),
html.P(format_rupiah(kewajiban_lancar + modal + laba_bersih), style={'fontWeight': 'bold'})
], style={'display': 'flex', 'justifyContent': 'space-between'})
], style={'flex': 1, 'padding': '15px', 'backgroundColor': COLORS['accent_mint_light'], 'borderRadius': '10px', 'marginLeft': '10px'})
], style={'display': 'flex', 'marginTop': '15px'})
], style={'marginBottom': '30px'}),
# Analisis Rasio Keuangan
html.Div([
html.H5("3. ANALISIS RASIO KEUANGAN", style={'color': COLORS['accent_teal'], 'borderBottom': f'2px solid {COLORS["accent_teal"]}', 'paddingBottom': '10px'}),
html.Div([
html.Div([
html.Div([
html.H6("Rasio Likuiditas", style={'color': COLORS['primary']}),
html.P(f"{rasio_likuiditas:.2f}", style={'fontSize': '1.5rem', 'fontWeight': 'bold', 'color': COLORS['success'] if rasio_likuiditas >= 1 else COLORS['warning']}),
html.Small("Aktiva Lancar / Kewajiban Lancar", style={'color': COLORS['gray_600']}),
html.Br(),
html.Small("â
Baik" if rasio_likuiditas >= 1 else "â ī¸ Perlu Perhatian",
style={'color': COLORS['success'] if rasio_likuiditas >= 1 else COLORS['warning']})
], style={'textAlign': 'center', 'padding': '15px', 'backgroundColor': COLORS['white'], 'borderRadius': '10px', 'boxShadow': '0 2px 10px rgba(0,0,0,0.1)'})
], style={'flex': 1, 'margin': '0 10px'}),
html.Div([
html.Div([
html.H6("Margin Laba Bersih", style={'color': COLORS['primary']}),
html.P(f"{margin_laba:.1f}%", style={'fontSize': '1.5rem', 'fontWeight': 'bold', 'color': COLORS['success'] if margin_laba >= 10 else COLORS['warning'] if margin_laba >= 5 else COLORS['error']}),
html.Small("(Laba Bersih / Pendapatan) Ã 100%", style={'color': COLORS['gray_600']}),
html.Br(),
html.Small("â
Excellent" if margin_laba >= 15 else "đ Baik" if margin_laba >= 10 else "â ī¸ Cukup" if margin_laba >= 5 else "â Perlu Perbaikan",
style={'color': COLORS['success'] if margin_laba >= 15 else COLORS['info'] if margin_laba >= 10 else COLORS['warning'] if margin_laba >= 5 else COLORS['error']})
], style={'textAlign': 'center', 'padding': '15px', 'backgroundColor': COLORS['white'], 'borderRadius': '10px', 'boxShadow': '0 2px 10px rgba(0,0,0,0.1)'})
], style={'flex': 1, 'margin': '0 10px'}),
html.Div([
html.Div([
html.H6("Return on Assets (ROA)", style={'color': COLORS['primary']}),
html.P(f"{roa:.1f}%", style={'fontSize': '1.5rem', 'fontWeight': 'bold', 'color': COLORS['success'] if roa >= 5 else COLORS['warning'] if roa >= 2 else COLORS['error']}),
html.Small("(Laba Bersih / Total Aset) Ã 100%", style={'color': COLORS['gray_600']}),
html.Br(),
html.Small("â
Excellent" if roa >= 8 else "đ Baik" if roa >= 5 else "â ī¸ Cukup" if roa >= 2 else "â Perlu Perbaikan",
style={'color': COLORS['success'] if roa >= 8 else COLORS['info'] if roa >= 5 else COLORS['warning'] if roa >= 2 else COLORS['error']})
], style={'textAlign': 'center', 'padding': '15px', 'backgroundColor': COLORS['white'], 'borderRadius': '10px', 'boxShadow': '0 2px 10px rgba(0,0,0,0.1)'})
], style={'flex': 1, 'margin': '0 10px'})
], style={'display': 'flex', 'marginTop': '15px'})
], style={'marginBottom': '30px'}),
# Ringkasan Eksekutif
html.Div([
html.H5("4. RINGKASAN EKSEKUTIF", style={'color': COLORS['accent_lavender'], 'borderBottom': f'2px solid {COLORS["accent_lavender"]}', 'paddingBottom': '10px'}),
html.Div([
html.P("đ **Tinjauan Kinerja:**", style={'fontWeight': 'bold'}),
html.Ul([
html.Li(f"Perusahaan {'mencapai laba' if laba_bersih >= 0 else 'mengalami rugi'} sebesar {format_rupiah(abs(laba_bersih))}"),
html.Li(f"Margin laba bersih sebesar {margin_laba:.1f}% {'di atas rata-rata industri' if margin_laba >= 15 else 'sesuai ekspektasi' if margin_laba >= 8 else 'perlu peningkatan'}"),
html.Li(f"Total aset perusahaan senilai {format_rupiah(aktiva_lancar + aktiva_tetap)}"),
]),
html.P("đĄ **Rekomendasi:**", style={'fontWeight': 'bold', 'marginTop': '15px'}),
html.Ul([
html.Li("Pertahankan pertumbuhan pendapatan dengan strategi pemasaran yang efektif"),
html.Li("Optimalkan pengelolaan persediaan untuk meningkatkan likuiditas"),
html.Li("Monitor beban operasional untuk efisiensi yang lebih baik"),
]) if laba_bersih >= 0 else html.Ul([
html.Li("Lakukan review terhadap struktur biaya operasional"),
html.Li("Tingkatkan efisiensi dalam pengelolaan persediaan"),
html.Li("Evaluasi strategi penetapan harga jual"),
])
], style={'backgroundColor': COLORS['gray_50'], 'padding': '20px', 'borderRadius': '10px', 'marginTop': '15px'})
])
])
return html.P("Klik 'Generate Laporan Keuangan Lengkap' untuk melihat laporan", style={'color': '#6c757d'})
if __name__ == '__main__':
print("đ Starting SIBAL Application...")
print("đ Debug mode: ON")
print("đ Server will be available at: http://127.0.0.1:8051")
print("âŗ Please wait for server to start...")
try:
app.run(debug=True, port=8051)
print("â
Server started successfully!")
except Exception as e:
print(f"â Error starting server: {e}")
print("đĄ Try changing port to 8052")