diff --git a/.env b/.env index 3155e20..b36dc95 100644 --- a/.env +++ b/.env @@ -7,8 +7,8 @@ # Optional: override 'From' email SMTP_HOST=smtp.gmail.com SMTP_PORT=587 -SMTP_USER=maulanaryan2004@gmail.com -SMTP_PASS=kfebdpeoaxfabzpm +SMTP_USER=appsibal@gmail.com +SMTP_PASS=yogsorikxfqymakh SMTP_SENDER_NAME=SIBAL OTP_DEBUG=false diff --git a/__pycache__/sibal.cpython-314.pyc b/__pycache__/sibal.cpython-314.pyc index f548f75..56d1b85 100644 Binary files a/__pycache__/sibal.cpython-314.pyc and b/__pycache__/sibal.cpython-314.pyc differ diff --git a/sibal.py b/sibal.py index 419bc4b..8c0542b 100644 --- a/sibal.py +++ b/sibal.py @@ -11,6 +11,7 @@ import glob from pathlib import Path import os, smtplib, ssl from email.message import EmailMessage +import re BASE_DIR = Path(__file__).resolve().parent ENV_PATH = BASE_DIR / ".env" @@ -76,6 +77,76 @@ supabase_url = "https://shltrwcweexbdcuogscs.supabase.co" supabase_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNobHRyd2N3ZWV4YmRjdW9nc2NzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQwNjk4MjUsImV4cCI6MjA3OTY0NTgyNX0.tVwzjiqORJ0dAZhW_f0PneVqJyummvmPOi-WFC5Wd1I" supabase_client = create_client(supabase_url, supabase_key) + +def delete_old_transactions(table_name: str, older_than_days: int | None = None) -> dict: + """Delete historical rows from a Supabase table. + + - If `older_than_days` is an int, delete rows where `tanggal` < (today - days). + - If `older_than_days` is None, delete ALL rows in the table (best-effort). + + Returns a dict with result information. + """ + try: + if older_than_days is None: + # Best-effort: fetch ids then delete by ids to avoid delete-without-filter issues + resp = supabase_client.table(table_name).select('id').execute() + ids = [r.get('id') for r in (resp.data or []) if r.get('id') is not None] + if not ids: + return {'deleted': 0, 'ids_deleted': []} + del_resp = supabase_client.table(table_name).delete().in_('id', ids).execute() + return {'deleted': len(ids), 'ids_deleted': ids, 'raw': del_resp} + else: + cutoff = (datetime.now().date() - timedelta(days=older_than_days)).isoformat() + # Try deleting by tanggal < cutoff + del_resp = supabase_client.table(table_name).delete().lt('tanggal', cutoff).execute() + # supabase returns deleted rows in del_resp.data when succesful + deleted = len(del_resp.data) if getattr(del_resp, 'data', None) else 0 + return {'deleted': deleted, 'cutoff': cutoff, 'raw': del_resp} + except Exception as e: + print(f"[CLEANUP] Error deleting from {table_name}: {e}") + return {'error': str(e)} + + +@server.route('/admin/cleanup-transactions') +def admin_cleanup_transactions(): + """HTTP helper to cleanup transactions. + + Query params: + - table: 'pemasukan'|'pengeluaran'|'both' (default: both) + - days: integer number of days to keep (delete older than days), or 'all' to delete everything + + Example: /admin/cleanup-transactions?table=both&days=365 + """ + try: + table_param = request.args.get('table', 'both') + days_param = request.args.get('days', None) + + if days_param is None: + return ("Usage: provide ?table={pemasukan|pengeluaran|both}&days={N|all}", 400) + + if days_param == 'all': + days = None + else: + try: + days = int(days_param) + except Exception: + return ("Invalid days parameter", 400) + + results = {} + targets = [] + if table_param in ('pemasukan', 'both'): + targets.append('transaksi_pemasukan') + if table_param in ('pengeluaran', 'both'): + targets.append('transaksi_pengeluaran') + + for t in targets: + results[t] = delete_old_transactions(t, days) + + return results + except Exception as e: + return {'error': str(e)} + + # ===== SIMPLE AUTH SYSTEM ===== import requests import secrets @@ -1403,17 +1474,34 @@ class SIBALData: 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}") + # Make a shallow copy so we can mutate for retry attempts + payload = dict(data) + attempts = 0 + while attempts < 3: + attempts += 1 + try: + response = self.client.table(table_name).insert(payload).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: + err = str(e) + print(f"⚠️ Insert attempt {attempts} failed for {table_name}: {err}") + # Handle PostgREST schema cache error where a column is missing + # message example: "Could not find the 'user_id' column of 'transaksi_pemasukan' in the schema cache" + m = re.search(r"Could not find the '([a-zA-Z0-9_]+)' column", err) + if m: + missing_col = m.group(1) + if missing_col in payload: + print(f"ℹ️ Removing unknown column '{missing_col}' and retrying insert to {table_name}") + payload.pop(missing_col, None) + continue + # If not a schema-missing issue or column not in payload, stop retrying + print(f"❌ Error inserting to {table_name}: {err}") 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""" @@ -2037,10 +2125,22 @@ def create_laporan_sub_nav(): html.I(className="fas fa-sync-alt"), " Jurnal Penyesuaian" ], href="/jurnal-penyesuaian", className="sub-nav-link"), + dcc.Link([ + html.I(className="fas fa-balance-scale"), + " Neraca Saldo Setelah Penyesuaian" + ], href="/neraca-saldo-penyesuaian", className="sub-nav-link"), dcc.Link([ html.I(className="fas fa-chart-line"), - " Neraca Setelah Penyesuaian" + " Laporan Posisi Keuangan" ], href="/neraca-setelah-penyesuaian", className="sub-nav-link"), + dcc.Link([ + html.I(className="fas fa-clipboard-list"), + " Jurnal Penutup" + ], href="/jurnal-penutup", className="sub-nav-link"), + dcc.Link([ + html.I(className="fas fa-file-invoice"), + " Neraca Setelah Penutupan" + ], href="/neraca-setelah-penutupan", className="sub-nav-link"), dcc.Link([ html.I(className="fas fa-money-bill-wave"), " Laporan Laba Rugi" @@ -2188,16 +2288,6 @@ def transaksi_layout(): ) ], 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"), @@ -2219,6 +2309,7 @@ def transaksi_layout(): className="form-input" ) ], className="form-group"), + html.Div([ html.Label("Keterangan Transaksi", className="form-label"), dcc.Input( @@ -2308,16 +2399,6 @@ def transaksi_layout(): ) ], 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( @@ -2436,6 +2517,37 @@ def kartu_persediaan_layout(): className="btn btn-primary", style={'marginBottom': '20px'} ), + # Single-row input form (masuk / keluar) - user requested + html.Div([ + html.Div([ + html.Div([ + html.Label('Tanggal', className='form-label'), + dcc.DatePickerSingle(id='tanggal-persediaan', date=datetime.now().date(), display_format='YYYY-MM-DD', className='form-control') + ], className='form-group', style={'width': '18%', 'display': 'inline-block', 'marginRight': '10px'}), + html.Div([ + html.Label('Barang', className='form-label'), + dcc.Dropdown(id='dropdown-kode-barang', options=[{'label': f"{b['kode']} - {b['nama']}", 'value': b['kode']} for b in sibal_data.master_persediaan], placeholder='Pilih Barang', className='form-control') + ], className='form-group', style={'width': '30%', 'display': 'inline-block', 'marginRight': '10px'}), + html.Div([ + html.Label('Masuk (Qty)', className='form-label'), + dcc.Input(id='input-masuk-qty', type='number', value=0, min=0, className='form-control') + ], className='form-group', style={'width': '12%', 'display': 'inline-block', 'marginRight': '10px'}), + html.Div([ + html.Label('Masuk (Harga)', className='form-label'), + dcc.Input(id='input-masuk-harga', type='number', value=0, min=0, className='form-control') + ], className='form-group', style={'width': '15%', 'display': 'inline-block', 'marginRight': '10px'}), + html.Div([ + html.Label('Keluar (Qty)', className='form-label'), + dcc.Input(id='input-keluar-qty', type='number', value=0, min=0, className='form-control') + ], className='form-group', style={'width': '12%', 'display': 'inline-block', 'marginRight': '10px'}), + html.Div([ + html.Label('Keterangan', className='form-label'), + dcc.Input(id='input-keterangan-persediaan', type='text', value='', className='form-control') + ], className='form-group', style={'width': '100%', 'display': 'block', 'marginTop': '10px'}), + html.Button('💾 Simpan Transaksi', id='btn-simpan-kartu-persediaan', className='btn btn-success', style={'marginTop': '10px'}) + ], className='form-row') + ], style={'marginBottom': '18px'}), + html.Div(id='tabel-kartu-persediaan', className="table-container") ], className="card-content") ], className="glass-card") @@ -2559,7 +2671,7 @@ def neraca_setelah_penyesuaian_layout(): html.Div([ html.I(className="fas fa-chart-line") ], className="card-icon"), - html.H1("Neraca Setelah Penyesuaian", className="card-title") + html.H1("Laporan posisi keuangan", className="card-title") ], className="card-header"), html.Div([ html.Div([ @@ -2572,7 +2684,7 @@ def neraca_setelah_penyesuaian_layout(): ) ], className="form-group"), html.Button( - "📊 Generate Neraca Setelah Penyesuaian", + "📊 Generate Laporan Posisi Keuangan", id='btn-generate-neraca-penyesuaian', className="btn btn-primary" ), @@ -2581,6 +2693,60 @@ def neraca_setelah_penyesuaian_layout(): ], className="glass-card") ], className="main-container") +def neraca_saldo_penyesuaian_layout(): + return html.Div([ + html.Div([ + html.Div([ + html.Div([html.I(className='fas fa-balance-scale')], className='card-icon'), + html.H1('Neraca Saldo 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-saldo-penyesuaian', date=datetime.now().date(), display_format='YYYY-MM-DD', className='form-control')], className='form-group'), + html.Div([html.Button('📊 Generate Neraca Saldo Setelah Penyesuaian', id='btn-generate-neraca-saldo-penyesuaian', className='btn btn-primary'), html.Button('💾 Simpan', id='btn-save-neraca-saldo-penyesuaian', className='btn btn-secondary', style={'marginLeft':'10px'})], style={'marginBottom':'12px'}), + html.Div(id='tabel-neraca-saldo-penyesuaian', className='table-container'), + html.Div(id='save-neraca-saldo-penyesuaian-status', style={'marginTop':'12px'}) + ], className='card-content') + ], className='glass-card') + ], className='main-container') + +def jurnal_penutup_layout(): + return html.Div([ + html.Div([ + html.Div([ + html.Div([html.I(className="fas fa-clipboard-list")], className="card-icon"), + html.H1("Jurnal Penutup", className="card-title") + ], className="card-header"), + html.Div([ + html.Div([ + html.Label("Tanggal Jurnal Penutup", className="form-label"), + dcc.DatePickerSingle( + id='tanggal-jurnal-penutup', + date=datetime.now().date(), + display_format='YYYY-MM-DD', + className='form-control' + ) + ], className='form-group'), + html.Button("📌 Generate Jurnal Penutup", id='btn-generate-jurnal-penutup', className='btn btn-primary'), + html.Div(id='tabel-jurnal-penutup', className='table-container') + ], className='card-content') + ], className='glass-card') + ], className='main-container') + +def neraca_setelah_penutupan_layout(): + return html.Div([ + html.Div([ + html.Div([html.Div([html.I(className='fas fa-file-invoice')], className='card-icon'), html.H1('Neraca Setelah Penutupan', className='card-title')], className='card-header'), + html.Div([ + html.Div([html.Label('Pilih Tanggal Neraca Setelah Penutupan:', className='form-label'), + dcc.DatePickerSingle(id='tanggal-neraca-penutupan', date=datetime.now().date(), display_format='YYYY-MM-DD', className='form-control')], className='form-group'), + html.Div([html.Button('📊 Generate Neraca Setelah Penutupan', id='btn-generate-neraca-penutupan', className='btn btn-primary'), html.Button('💾 Simpan ke Supabase', id='btn-save-neraca-penutupan', className='btn btn-secondary', style={'marginLeft':'10px'})], style={'marginBottom':'12px'}), + html.Div(id='tabel-neraca-penutupan', className='table-container'), + html.Div(id='save-neraca-penutupan-status', style={'marginTop':'12px'}) + ], className='card-content') + ], className='glass-card') + ], className='main-container') + def laporan_laba_rugi_layout(): return html.Div([ html.Div([ @@ -3228,7 +3394,7 @@ def display_page(pathname): laporan_pages = [ '/laporan-analisis', '/jurnal-penyesuaian', '/neraca-setelah-penyesuaian', - '/laporan-laba-rugi', '/laporan-keuangan' + '/laporan-laba-rugi', '/laporan-keuangan', '/jurnal-penutup', '/neraca-setelah-penutupan', '/neraca-saldo-penyesuaian' ] if pathname in akuntansi_pages: @@ -3265,6 +3431,12 @@ def display_page(pathname): content = jurnal_penyesuaian_layout() elif pathname == '/neraca-setelah-penyesuaian': content = neraca_setelah_penyesuaian_layout() + elif pathname == '/jurnal-penutup': + content = jurnal_penutup_layout() + elif pathname == '/neraca-setelah-penutupan': + content = neraca_setelah_penutupan_layout() + elif pathname == '/neraca-saldo-penyesuaian': + content = neraca_saldo_penyesuaian_layout() elif pathname == '/laporan-laba-rugi': content = laporan_laba_rugi_layout() elif pathname == '/laporan-keuangan': @@ -3816,10 +3988,24 @@ 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 = [] - + # Each checklist returns a list of selected values. We encode option values as + # 'id:' when item has been saved to DB, or 'idx:' for in-memory items. for i, checked in enumerate(checklists): - if checked and 'selected' in checked and i < len(transaksi_hari_ini): - indices_to_delete.append(i) + if checked: + val = checked[0] + if isinstance(val, str) and val.startswith('id:'): + tid = val.split(':', 1)[1] + for idx_t, t in enumerate(transaksi_hari_ini): + if str(t.get('id')) == str(tid): + indices_to_delete.append(idx_t) + break + elif isinstance(val, str) and val.startswith('idx:'): + try: + idx_val = int(val.split(':', 1)[1]) + if idx_val < len(transaksi_hari_ini): + indices_to_delete.append(idx_val) + except Exception: + pass if indices_to_delete: # Hapus dari belakang untuk menghindari index error @@ -3827,9 +4013,16 @@ def hapus_pemasukan(n_clicks, tanggal, checklists): for i in sorted(indices_to_delete, reverse=True): if i < len(transaksi_hari_ini): transaksi_to_delete = transaksi_hari_ini[i] + # If saved to DB, attempt DB delete + try: + if 'id' in transaksi_to_delete and transaksi_to_delete.get('id') and supabase_client: + supabase_client.table('transaksi_pemasukan').delete().eq('id', transaksi_to_delete.get('id')).execute() + except Exception as e: + print(f"[DELETE] Failed to delete pemasukan id {transaksi_to_delete.get('id')}: {e}") + 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] @@ -3846,10 +4039,23 @@ 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 = [] - + # Parse checklist selected values encoded as 'id:' or 'idx:' for i, checked in enumerate(checklists): - if checked and 'selected' in checked and i < len(transaksi_hari_ini): - indices_to_delete.append(i) + if checked: + val = checked[0] + if isinstance(val, str) and val.startswith('id:'): + tid = val.split(':', 1)[1] + for idx_t, t in enumerate(transaksi_hari_ini): + if str(t.get('id')) == str(tid): + indices_to_delete.append(idx_t) + break + elif isinstance(val, str) and val.startswith('idx:'): + try: + idx_val = int(val.split(':', 1)[1]) + if idx_val < len(transaksi_hari_ini): + indices_to_delete.append(idx_val) + except Exception: + pass if indices_to_delete: # Hapus dari belakang untuk menghindari index error @@ -3857,9 +4063,16 @@ def hapus_pengeluaran(n_clicks, tanggal, checklists): for i in sorted(indices_to_delete, reverse=True): if i < len(transaksi_hari_ini): transaksi_to_delete = transaksi_hari_ini[i] + # If saved to DB, attempt DB delete + try: + if 'id' in transaksi_to_delete and transaksi_to_delete.get('id') and supabase_client: + supabase_client.table('transaksi_pengeluaran').delete().eq('id', transaksi_to_delete.get('id')).execute() + except Exception as e: + print(f"[DELETE] Failed to delete pengeluaran id {transaksi_to_delete.get('id')}: {e}") + 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] @@ -3887,10 +4100,16 @@ def update_daftar_transaksi(tanggal, n_pemasukan, n_pengeluaran, n_hapus_pemasuk quantity = trans.get('quantity', 0) jumlah = trans.get('jumlah', 0) + # encode option value to indicate DB id or index + if trans.get('id'): + opt_value = f"id:{trans.get('id')}" + else: + opt_value = f"idx:{i}" + items_pemasukan.append(html.Div([ dcc.Checklist( id={'type': 'pemasukan-check', 'index': i}, - options=[{'label': '', 'value': 'selected'}], + options=[{'label': '', 'value': opt_value}], value=[], style={'display': 'inline-block', 'marginRight': '10px'} ), @@ -3917,10 +4136,16 @@ def update_daftar_transaksi(tanggal, n_pemasukan, n_pengeluaran, n_hapus_pemasuk jenis_label = trans['jenis'].replace('_', ' ').title() metode = trans.get('metode_bayar', 'tunai') + # encode option value to indicate DB id or index + if trans.get('id'): + opt_value = f"id:{trans.get('id')}" + else: + opt_value = f"idx:{i}" + items_pengeluaran.append(html.Div([ dcc.Checklist( id={'type': 'pengeluaran-check', 'index': i}, - options=[{'label': '', 'value': 'selected'}], + options=[{'label': '', 'value': opt_value}], value=[], style={'display': 'inline-block', 'marginRight': '10px'} ), @@ -4623,6 +4848,74 @@ def update_kartu_persediaan(n_clicks): html.P("💡 Buat transaksi pembelian persediaan terlebih dahulu", style={'textAlign': 'center', 'color': COLORS['gray_500'], 'fontSize': '14px'}) ]) + + +@app.callback( + Output('tabel-kartu-persediaan', 'children'), + Input('btn-simpan-kartu-persediaan', 'n_clicks'), + [State('dropdown-kode-barang', 'value'), + State('tanggal-persediaan', 'date'), + State('input-masuk-qty', 'value'), + State('input-masuk-harga', 'value'), + State('input-keluar-qty', 'value'), + State('input-keterangan-persediaan', 'value')], + prevent_initial_call=True, + allow_duplicate=True +) +def save_kartu_persediaan_row(n_clicks, kode_barang, tanggal, masuk_qty, masuk_harga, keluar_qty, keterangan): + """Simpan satu baris transaksi kartu persediaan (masuk atau keluar).""" + ctx = dash.callback_context + if not ctx.triggered: + return dash.no_update + + # Normalize inputs + try: + masuk_qty = int(masuk_qty) if masuk_qty else 0 + except Exception: + masuk_qty = 0 + try: + masuk_harga = float(masuk_harga) if masuk_harga else 0 + except Exception: + masuk_harga = 0 + try: + keluar_qty = int(keluar_qty) if keluar_qty else 0 + except Exception: + keluar_qty = 0 + + if not kode_barang: + return html.Div([html.P('⚠️ Pilih barang terlebih dahulu', style={'color': '#d9534f'})]) + + if masuk_qty <= 0 and keluar_qty <= 0: + return html.Div([html.P('⚠️ Masukkan qty masuk atau keluar (minimal salah satu > 0)', style={'color': '#d9534f'})]) + + # Use existing helpers to update in-memory kartu persediaan + result_msg = None + if masuk_qty > 0: + success = update_persediaan_pembelian(kode_barang, masuk_qty, masuk_harga, tanggal, keterangan) + if success: + result_msg = html.Div([html.P('✅ Transaksi pembelian disimpan', style={'color': '#28a745'})]) + else: + result_msg = html.Div([html.P('❌ Gagal menyimpan pembelian', style={'color': '#d9534f'})]) + + if keluar_qty > 0: + success, hpp = update_persediaan_penjualan(kode_barang, keluar_qty, tanggal, keterangan) + if success: + result_msg = html.Div([html.P('✅ Transaksi penjualan disimpan', style={'color': '#28a745'})]) + else: + result_msg = html.Div([html.P('❌ Gagal menyimpan penjualan (cek stok)', style={'color': '#d9534f'})]) + + # Persist data to Supabase + try: + sibal_data.save_all_data() + except Exception as e: + print(f"❌ Error saat menyimpan ke Supabase: {e}") + + # Return refreshed table by calling the update function directly + try: + return update_kartu_persediaan(None) + except Exception as e: + print(f"❌ Error saat merefresh tampilan kartu persediaan: {e}") + return result_msg if result_msg is not None else html.Div([html.P('Operasi selesai')]) @app.callback( Output('detail-buku-besar', 'children'), @@ -5366,7 +5659,7 @@ def generate_neraca_setelah_penyesuaian(n_clicks, tanggal): # Buat tabel neraca return html.Div([ - html.H4(f"Neraca Setelah Penyesuaian per {tanggal}", + html.H4(f"Laporan Posisi Keuangan per {tanggal}", style={'textAlign': 'center', 'marginBottom': '30px', 'color': COLORS['primary']}), html.Div([ @@ -5420,7 +5713,7 @@ def generate_neraca_setelah_penyesuaian(n_clicks, tanggal): ]) ]) - return html.P("Klik 'Generate Neraca Setelah Penyesuaian' untuk melihat neraca", style={'color': '#6c757d'}) + return html.P("Klik 'Generate Laporan Posisi Keuangan' untuk melihat neraca", style={'color': '#6c757d'}) @app.callback( Output('tabel-laba-rugi', 'children'), @@ -5854,6 +6147,534 @@ def calculate_saldo_akun_periode(jurnal_data, start_date, end_date): return saldo_akun + +@app.callback( + Output('tabel-jurnal-penutup', 'children'), + Input('btn-generate-jurnal-penutup', 'n_clicks'), + State('tanggal-jurnal-penutup', 'date'), + prevent_initial_call=True +) +def generate_jurnal_penutup(n_clicks, tanggal): + if not (n_clicks and n_clicks > 0): + return html.P("Klik 'Generate Jurnal Penutup' untuk membuat jurnal penutup", style={'color': '#6c757d'}) + + # Hitung saldo akun dari jurnal umum + jurnal penyesuaian + saldo_akun = {} + 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']} + 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']} + saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit'] + + # Identifikasi akun nominal (pendapatan & beban) + jurnal_penutup_entries = [] + total_pendapatan = 0 + total_beban = 0 + + for kode, data in sorted(saldo_akun.items()): + tipe = ('pendapatan' if kode.startswith('4') else 'beban' if kode.startswith('5') or kode.startswith('6') else None) + saldo = (data['kredit'] - data['debit']) if tipe == 'pendapatan' else (data['debit'] - data['kredit']) if tipe == 'beban' else 0 + if tipe == 'pendapatan' and saldo != 0: + # Tutup pendapatan: Debit akun pendapatan, Kredit Ikhtisar Laba Rugi + jurnal_penutup_entries.append({ + 'tanggal': tanggal, + 'keterangan': f"Penutupan akun pendapatan {data['nama']}", + 'akun_debit': data['nama'], + 'debit': saldo, + 'akun_kredit': 'Ikhtisar Laba Rugi', + 'kredit': saldo + }) + total_pendapatan += saldo + elif tipe == 'beban' and saldo != 0: + # Tutup beban: Debit Ikhtisar Laba Rugi, Kredit akun beban + jurnal_penutup_entries.append({ + 'tanggal': tanggal, + 'keterangan': f"Penutupan akun beban {data['nama']}", + 'akun_debit': 'Ikhtisar Laba Rugi', + 'debit': saldo, + 'akun_kredit': data['nama'], + 'kredit': saldo + }) + total_beban += saldo + + laba_rugi = total_pendapatan - total_beban + # Tutup Ikhtisar Laba Rugi ke Modal + if laba_rugi != 0: + if laba_rugi > 0: + # Laba: Debit Ikhtisar, Kredit Modal + jurnal_penutup_entries.append({ + 'tanggal': tanggal, + 'keterangan': 'Penutupan Ikhtisar Laba Rugi (Laba)', + 'akun_debit': 'Ikhtisar Laba Rugi', + 'debit': laba_rugi, + 'akun_kredit': KODE_AKUN['modal']['nama'], + 'kredit': laba_rugi + }) + else: + # Rugi: Debit Modal, Kredit Ikhtisar + jurnal_penutup_entries.append({ + 'tanggal': tanggal, + 'keterangan': 'Penutupan Ikhtisar Laba Rugi (Rugi)', + 'akun_debit': KODE_AKUN['modal']['nama'], + 'debit': abs(laba_rugi), + 'akun_kredit': 'Ikhtisar Laba Rugi', + 'kredit': abs(laba_rugi) + }) + + # Render tabel jurnal penutup + if jurnal_penutup_entries: + 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 j in jurnal_penutup_entries: + rows.append(html.Tr([html.Td(j['tanggal']), html.Td(j['keterangan']), html.Td(j['akun_debit']), html.Td(format_rupiah(j['debit'])), html.Td(j['akun_kredit']), html.Td(format_rupiah(j['kredit']))])) + + # Also persist jurnal penutup entries to Supabase table 'jurnal_penutup' (delete existing for same tanggal+user) + insert_count = 0 + insert_errors = [] + try: + if sibal_data and getattr(sibal_data, 'active_user_id', None) is not None: + try: + supabase_client.table('jurnal_penutup').delete().eq('tanggal', str(tanggal)).eq('user_id', sibal_data.active_user_id).execute() + except Exception: + # ignore delete errors but continue to attempt inserts + pass + + # Build name -> code map from existing data to supply kode_akun_* fields + code_map = {} + try: + all_journals = sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian + for jj in all_journals: + name_debit = jj.get('akun_debit') + code_debit = jj.get('kode_akun_debit') + if name_debit and code_debit: + code_map.setdefault(name_debit, code_debit) + name_kredit = jj.get('akun_kredit') + code_kredit = jj.get('kode_akun_kredit') + if name_kredit and code_kredit: + code_map.setdefault(name_kredit, code_kredit) + except Exception: + pass + + for j in jurnal_penutup_entries: + akun_debet_name = j.get('akun_debit','') + akun_kredit_name = j.get('akun_kredit','') + kode_debet = code_map.get(akun_debet_name) + kode_kredit = code_map.get(akun_kredit_name) + # try known KODE_AKUN mapping for modal or ikhtisar if available + try: + if not kode_debet and akun_debet_name and 'modal' in globals() or 'KODE_AKUN' in globals(): + kode_debet = KODE_AKUN.get(akun_debet_name, {}).get('kode') if isinstance(KODE_AKUN, dict) else None + except Exception: + pass + + try: + if not kode_kredit and akun_kredit_name and 'KODE_AKUN' in globals(): + kode_kredit = KODE_AKUN.get(akun_kredit_name, {}).get('kode') if isinstance(KODE_AKUN, dict) else None + except Exception: + pass + + # fallback to using the account name as code (to satisfy NOT NULL constraints) + if not kode_debet: + kode_debet = str(akun_debet_name) + if not kode_kredit: + kode_kredit = str(akun_kredit_name) + + payload = { + 'tanggal': str(j.get('tanggal')), + 'keterangan': j.get('keterangan',''), + 'akun_debit': akun_debet_name, + 'kode_akun_debit': kode_debet, + 'jumlah_debit': float(j.get('debit',0) or 0), + 'akun_kredit': akun_kredit_name, + 'kode_akun_kredit': kode_kredit, + 'jumlah_kredit': float(j.get('kredit',0) or 0), + 'user_id': sibal_data.active_user_id + } + try: + resp = supabase_client.table('jurnal_penutup').insert(payload).execute() + if getattr(resp, 'status_code', 0) in (200,201) or getattr(resp, 'data', None): + insert_count += 1 + else: + insert_errors.append(str(payload.get('akun_debit'))) + except Exception as e: + insert_errors.append(f"{payload.get('akun_debit')}:{e}") + except Exception: + insert_errors.append('unknown') + + status_el = None + if insert_count > 0: + status_msg = f"✅ Tersimpan {insert_count} baris jurnal penutup ke tabel 'jurnal_penutup'" + if insert_errors: + status_msg += f" — gagal: {', '.join(insert_errors)}" + status_el = html.Div(status_msg, style={'color': COLORS['success'], 'marginTop': '10px'}) + elif insert_errors: + status_el = html.Div(f"⚠️ Gagal menyimpan jurnal penutup: {', '.join(insert_errors)}", style={'color': COLORS['warning'], 'marginTop': '10px'}) + + content = [html.Table([header] + rows, className='modern-table')] + if status_el: + content.append(status_el) + return html.Div(content) + + return html.P('Tidak ada akun nominal yang perlu ditutup', style={'color': '#6c757d'}) + + +@app.callback( + Output('tabel-neraca-penutupan', 'children'), + Input('btn-generate-neraca-penutupan', 'n_clicks'), + State('tanggal-neraca-penutupan', 'date'), + prevent_initial_call=True +) +def generate_neraca_setelah_penutupan(n_clicks, tanggal): + if not (n_clicks and n_clicks > 0): + return html.P("Klik 'Generate Neraca Setelah Penutupan' untuk melihat neraca", style={'color': '#6c757d'}) + # Ambil saldo dari jurnal umum + penyesuaian + saldo_akun = {} + semua_jurnal = sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian + for jurnal in semua_jurnal: + kode_debit = jurnal.get('kode_akun_debit') + kode_kredit = jurnal.get('kode_akun_kredit') + + if kode_debit: + if kode_debit not in saldo_akun: + saldo_akun[kode_debit] = {'debit': 0.0, 'kredit': 0.0, 'nama': jurnal.get('akun_debit','')} + saldo_akun[kode_debit]['debit'] += float(jurnal.get('jumlah_debit', 0) or 0) + + if kode_kredit: + if kode_kredit not in saldo_akun: + saldo_akun[kode_kredit] = {'debit': 0.0, 'kredit': 0.0, 'nama': jurnal.get('akun_kredit','')} + saldo_akun[kode_kredit]['kredit'] += float(jurnal.get('jumlah_kredit', 0) or 0) + + # Hitung laba rugi dari akun pendapatan/beban + pendapatan = {k: v for k, v in saldo_akun.items() if str(k).startswith('4')} + beban = {k: v for k, v in saldo_akun.items() if str(k).startswith('5') or str(k).startswith('6')} + total_pendapatan = sum((v['kredit'] - v['debit']) for v in pendapatan.values()) + total_beban = sum((v['debit'] - v['kredit']) for v in beban.values()) + laba_rugi = total_pendapatan - total_beban + + # Siapkan baris neraca setelah penutupan dalam format Debit/Kredit + rows = [] + total_debit = 0.0 + total_kredit = 0.0 + + # Masukkan semua akun non-nominal (exclude pendapatan & beban) + for kode, data in sorted(saldo_akun.items()): + kode_str = str(kode) + tipe = 'aktiva' if kode_str.startswith('1') else 'pasiva' if kode_str.startswith('2') else 'modal' if kode_str.startswith('3') else 'pendapatan' if kode_str.startswith('4') else 'beban' if kode_str.startswith('5') or kode_str.startswith('6') else 'lainnya' + if tipe in ('pendapatan', 'beban'): + continue + + # aktiva shown as debit = debit - kredit ; pasiva/modal shown as kredit = kredit - debit + debit_amt = 0.0 + kredit_amt = 0.0 + if tipe == 'aktiva': + saldo = data['debit'] - data['kredit'] + if abs(saldo) < 0.01: + continue + debit_amt = round(saldo,2) if saldo > 0 else 0.0 + kredit_amt = round(abs(saldo),2) if saldo < 0 else 0.0 + else: + saldo = data['kredit'] - data['debit'] + if abs(saldo) < 0.01: + continue + kredit_amt = round(saldo,2) if saldo > 0 else 0.0 + debit_amt = round(abs(saldo),2) if saldo < 0 else 0.0 + + rows.append(html.Tr([html.Td(kode), html.Td(data.get('nama','')), html.Td(format_rupiah(debit_amt) if debit_amt else ''), html.Td(format_rupiah(kredit_amt) if kredit_amt else '')])) + total_debit += debit_amt + total_kredit += kredit_amt + + # Tambahkan laba/rugi ke modal (jika ada) + if abs(laba_rugi) >= 0.01: + # find modal kode if available + modal_kode = KODE_AKUN.get('modal', {}).get('kode') + modal_nama = KODE_AKUN.get('modal', {}).get('nama') + if modal_kode: + # laba (positif) increases modal kredit, rugi increases debit + if laba_rugi > 0: + rows.append(html.Tr([html.Td(modal_kode), html.Td(modal_nama or 'Modal'), html.Td(''), html.Td(format_rupiah(round(laba_rugi,2)))])) + total_kredit += round(laba_rugi,2) + else: + rows.append(html.Tr([html.Td(modal_kode), html.Td(modal_nama or 'Modal'), html.Td(format_rupiah(round(abs(laba_rugi),2))), html.Td('')])) + total_debit += round(abs(laba_rugi),2) + + header = html.Tr([html.Th('Kode'), html.Th('Nama Akun'), html.Th('Debit'), html.Th('Kredit')]) + if rows: + rows.append(html.Tr([html.Td('', 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'})])) + balans = abs(total_debit - total_kredit) < 0.01 + status = html.P('✅ Neraca Setelah Penutupan Seimbang' if balans else '❌ Neraca Setelah Penutupan Tidak Seimbang', style={'textAlign': 'center', 'fontWeight': 'bold', 'color': COLORS['success'] if balans else COLORS['error']}) + return html.Div([html.H4(f'Neraca Setelah Penutupan per {tanggal}', style={'textAlign': 'center', 'marginBottom': '20px', 'color': COLORS['primary']}), html.Table([header] + rows, className='modern-table', style={'width': '100%'}), status]) + + return html.P('Tidak ada akun non-nominal untuk ditampilkan setelah penutupan', style={'color': '#6c757d'}) + + +@app.callback( + Output('save-neraca-penutupan-status', 'children'), + Input('btn-save-neraca-penutupan', 'n_clicks'), + State('tanggal-neraca-penutupan', 'date'), + prevent_initial_call=True +) +def save_neraca_setelah_penutupan(n_clicks, tanggal): + if not (n_clicks and n_clicks > 0): + return '' + + # Recompute balances (same logic as generate_neraca_setelah_penutupan) + saldo_akun = {} + 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.0, 'kredit': 0.0, 'nama': jurnal['akun_debit']} + saldo_akun[kode_debit]['debit'] += float(jurnal.get('jumlah_debit', 0) or 0) + + kode_kredit = jurnal['kode_akun_kredit'] + if kode_kredit not in saldo_akun: + saldo_akun[kode_kredit] = {'debit': 0.0, 'kredit': 0.0, 'nama': jurnal['akun_kredit']} + saldo_akun[kode_kredit]['kredit'] += float(jurnal.get('jumlah_kredit', 0) or 0) + + # Compute laba/rugi + pendapatan = {k: v for k, v in saldo_akun.items() if str(k).startswith('4')} + beban = {k: v for k, v in saldo_akun.items() if str(k).startswith('5') or str(k).startswith('6')} + total_pendapatan = sum((v['kredit'] - v['debit']) for v in pendapatan.values()) + total_beban = sum((v['debit'] - v['kredit']) for v in beban.values()) + laba_rugi = total_pendapatan - total_beban + + # Build adjusted balances (exclude nominal accounts) + rows_to_save = [] + total_aktiva = 0.0 + total_pasiva_modal = 0.0 + + for kode, data in sorted(saldo_akun.items()): + kode_str = str(kode) + tipe = 'aktiva' if kode_str.startswith('1') else 'pasiva' if kode_str.startswith('2') else 'modal' if kode_str.startswith('3') else 'pendapatan' if kode_str.startswith('4') else 'beban' if kode_str.startswith('5') or kode_str.startswith('6') else 'lainnya' + if tipe in ('pendapatan', 'beban'): + continue + + if tipe == 'aktiva': + saldo = data['debit'] - data['kredit'] + if abs(saldo) < 0.01: + continue + # aktiva -> saldo_debit + rows_to_save.append({'kode_akun': kode, 'nama_akun': data.get('nama',''), 'saldo_debit': round(saldo,2), 'saldo_kredit': 0.0, 'jenis_akun': tipe}) + total_aktiva += saldo + else: + # pasiva or modal -> saldo_kredit + saldo = data['kredit'] - data['debit'] + if abs(saldo) < 0.01: + continue + rows_to_save.append({'kode_akun': kode, 'nama_akun': data.get('nama',''), 'saldo_debit': 0.0, 'saldo_kredit': round(saldo,2), 'jenis_akun': tipe}) + total_pasiva_modal += saldo + + # Add laba_rugi to modal account + if abs(laba_rugi) >= 0.01: + modal_kode = KODE_AKUN.get('modal', {}).get('kode') + modal_nama = KODE_AKUN.get('modal', {}).get('nama') + if modal_kode: + # find existing modal row + found = next((r for r in rows_to_save if r['kode_akun'] == modal_kode), None) + if found: + if laba_rugi > 0: + found['saldo_kredit'] = round(found.get('saldo_kredit',0) + laba_rugi,2) + else: + found['saldo_debit'] = round(found.get('saldo_debit',0) + abs(laba_rugi),2) + else: + if laba_rugi > 0: + rows_to_save.append({'kode_akun': modal_kode, 'nama_akun': modal_nama or '', 'saldo_debit': 0.0, 'saldo_kredit': round(laba_rugi,2), 'jenis_akun': 'modal'}) + else: + rows_to_save.append({'kode_akun': modal_kode, 'nama_akun': modal_nama or '', 'saldo_debit': round(abs(laba_rugi),2), 'saldo_kredit': 0.0, 'jenis_akun': 'modal'}) + + # Prepare payloads in same format as neraca_setelah_penyesuaian + payloads = [] + for r in rows_to_save: + payloads.append({ + 'tanggal': str(tanggal) if tanggal is not None else None, + 'kode_akun': str(r['kode_akun']), + 'nama_akun': r.get('nama_akun',''), + 'saldo_debit': float(r.get('saldo_debit',0) or 0), + 'saldo_kredit': float(r.get('saldo_kredit',0) or 0), + 'jenis_akun': r.get('jenis_akun',''), + 'keterangan': f'Neraca setelah penutupan per {tanggal}', + 'user_id': sibal_data.active_user_id + }) + + if not payloads: + return html.Div([html.Span('Tidak ada baris disimpan (saldo kosong atau error).', style={'color': COLORS['warning']})]) + + # Optional: delete existing rows for same tanggal + user to avoid duplicates + try: + if sibal_data and getattr(sibal_data, 'active_user_id', None) is not None: + del_resp = supabase_client.table('neraca_setelah_penutup').delete().eq('tanggal', str(tanggal)).eq('user_id', sibal_data.active_user_id).execute() + else: + del_resp = None + except Exception as e: + # proceed but capture warning + del_resp = {'error': str(e)} + + # Insert payloads in batch (use supabase client directly to get feedback) + inserted = 0 + errors = [] + for p in payloads: + try: + resp = supabase_client.table('neraca_setelah_penutup').insert(p).execute() + # supabase-python returns object with .status_code or .data + if getattr(resp, 'status_code', 0) in (200, 201) or getattr(resp, 'data', None): + inserted += 1 + else: + errors.append(str(p.get('kode_akun'))) + except Exception as e: + errors.append(f"{p.get('kode_akun')}:{e}") + + if inserted == 0: + err_msg = 'Tidak ada baris disimpan (saldo kosong atau error).' + if errors: + err_msg += ' Error: ' + '; '.join(errors) + return html.Div([html.Span(err_msg, style={'color': COLORS['warning']})]) + + msg = f"✅ Disimpan {inserted} baris ke tabel 'neraca_setelah_penutup'" + if errors: + msg += f" — gagal menyimpan kode: {', '.join(errors)}" + + return html.Div([html.Span(msg, style={'color': COLORS['success']} )]) + + +@app.callback( + Output('tabel-neraca-saldo-penyesuaian', 'children'), + Input('btn-generate-neraca-saldo-penyesuaian', 'n_clicks'), + State('tanggal-neraca-saldo-penyesuaian', 'date'), + prevent_initial_call=True +) +def generate_neraca_saldo_penyesuaian(n_clicks, tanggal): + if not (n_clicks and n_clicks > 0): + return html.P("Klik 'Generate Neraca Saldo Setelah Penyesuaian' untuk melihat neraca saldo", style={'color': '#6c757d'}) + + # Kumpulkan saldo dari jurnal umum + penyesuaian + saldo_akun = {} + 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']} + 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']} + saldo_akun[kode_kredit]['kredit'] += jurnal['jumlah_kredit'] + + # Buat baris neraca saldo: tampilkan debit atau kredit sesuai saldo + rows = [] + total_debit = 0 + total_kredit = 0 + + for kode, data in sorted(saldo_akun.items()): + saldo = data['debit'] - data['kredit'] + if saldo > 0: + debit_amt = saldo + kredit_amt = 0 + elif saldo < 0: + debit_amt = 0 + kredit_amt = abs(saldo) + else: + debit_amt = 0 + kredit_amt = 0 + + if debit_amt != 0 or kredit_amt != 0: + rows.append(html.Tr([html.Td(kode), html.Td(data['nama']), html.Td(format_rupiah(debit_amt) if debit_amt else ''), html.Td(format_rupiah(kredit_amt) if kredit_amt else '')])) + total_debit += debit_amt + total_kredit += kredit_amt + + header = html.Tr([html.Th('Kode'), html.Th('Nama Akun'), html.Th('Debit'), html.Th('Kredit')]) + # Tambahkan baris total + if rows: + rows.append(html.Tr([html.Td('', 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'})])) + balans = abs(total_debit - total_kredit) < 0.01 + status = html.P('✅ Neraca Saldo Seimbang' if balans else '❌ Neraca Saldo Tidak Seimbang', style={'textAlign': 'center', 'fontWeight': 'bold', 'color': COLORS['success'] if balans else COLORS['error']}) + return html.Div([html.H4(f'Neraca Saldo Setelah Penyesuaian per {tanggal}', style={'textAlign': 'center', 'marginBottom': '20px', 'color': COLORS['primary']}), html.Table([header] + rows, className='modern-table', style={'width': '100%'}), status]) + + return html.P('Tidak ada saldo akun untuk ditampilkan', style={'color': '#6c757d'}) + + +@app.callback( + Output('save-neraca-saldo-penyesuaian-status', 'children'), + Input('btn-save-neraca-saldo-penyesuaian', 'n_clicks'), + State('tanggal-neraca-saldo-penyesuaian', 'date'), + prevent_initial_call=True +) +def save_neraca_saldo_penyesuaian(n_clicks, tanggal): + if not (n_clicks and n_clicks > 0): + return '' + + # Recompute saldo per akun (gabungkan jurnal umum + penyesuaian) + saldo_akun = {} + semua_jurnal = sibal_data.jurnal_umum + sibal_data.jurnal_penyesuaian + for jurnal in semua_jurnal: + kode_debit = jurnal.get('kode_akun_debit') + kode_kredit = jurnal.get('kode_akun_kredit') + + if kode_debit: + if kode_debit not in saldo_akun: + saldo_akun[kode_debit] = {'kode': kode_debit, 'nama': jurnal.get('akun_debit'), 'debit': 0.0, 'kredit': 0.0} + saldo_akun[kode_debit]['debit'] += float(jurnal.get('jumlah_debit', 0) or 0) + + if kode_kredit: + if kode_kredit not in saldo_akun: + saldo_akun[kode_kredit] = {'kode': kode_kredit, 'nama': jurnal.get('akun_kredit'), 'debit': 0.0, 'kredit': 0.0} + saldo_akun[kode_kredit]['kredit'] += float(jurnal.get('jumlah_kredit', 0) or 0) + + # Prepare and insert rows into Supabase table 'neraca_setelah_penyesuaian' + inserted = 0 + errors = [] + for kode, data in sorted(saldo_akun.items()): + debit_amt = data['debit'] - 0 + kredit_amt = data['kredit'] - 0 + + # skip zero balances + if abs(debit_amt) < 0.01 and abs(kredit_amt) < 0.01: + continue + + # Compute final balance: if debit > credit => saldo_debit = debit-credit; else saldo_kredit = credit-debit + if debit_amt >= kredit_amt: + saldo_debit = round(debit_amt - kredit_amt, 2) + saldo_kredit = 0.0 + else: + saldo_debit = 0.0 + saldo_kredit = round(kredit_amt - debit_amt, 2) + + # Determine jenis akun from kode (best-effort) + kode_str = str(kode) + jenis = 'aktiva' if kode_str.startswith('1') else 'pasiva' if kode_str.startswith('2') else 'modal' if kode_str.startswith('3') else 'pendapatan' if kode_str.startswith('4') else 'beban' if kode_str.startswith('5') or kode_str.startswith('6') else 'lainnya' + + payload = { + 'tanggal': tanggal, + 'kode_akun': kode, + 'nama_akun': data.get('nama') or '', + 'saldo_debit': float(saldo_debit), + 'saldo_kredit': float(saldo_kredit), + 'jenis_akun': jenis, + 'keterangan': f'Neraca saldo setelah penyesuaian per {tanggal}', + 'user_id': sibal_data.active_user_id + } + + saved = sibal_data._insert_table_data('neraca_setelah_penyesuaian', payload) + if saved: + inserted += 1 + else: + errors.append(str(kode)) + + if inserted == 0: + return html.Div([html.Span('Tidak ada baris disimpan (saldo kosong atau error).', style={'color': COLORS['warning']})]) + + msg = f"✅ Disimpan {inserted} baris ke tabel 'neraca_setelah_penyesuaian'" + if errors: + msg += f" — gagal menyimpan kode: {', '.join(errors)}" + + return html.Div([html.Span(msg, style={'color': COLORS['success']} )]) + def format_currency(amount): """Format currency dengan pemisah ribuan""" if amount == 0: diff --git a/text b/text index d1edaa4..7970d3b 100644 --- a/text +++ b/text @@ -1,4 +1,4 @@ -cd 'C:\Users\ilham\Downloads\Projek-SIBAL-main\Projek-SIBAL-main' +cd 'C:\Users\ilham\Downloads\Projek-SIBA\Projek-SIBAL-main' python -m venv .venv Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force .\.venv\Scripts\Activate.ps1 diff --git a/text copy b/text copy new file mode 100644 index 0000000..d1edaa4 --- /dev/null +++ b/text copy @@ -0,0 +1,8 @@ +cd 'C:\Users\ilham\Downloads\Projek-SIBAL-main\Projek-SIBAL-main' +python -m venv .venv +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +pip install authlib +pip freeze > requirements.txt +python sibal.py \ No newline at end of file