diff --git a/__pycache__/sibal.cpython-314.pyc b/__pycache__/sibal.cpython-314.pyc index 56d1b85..793c847 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 8c0542b..ad1b026 100644 --- a/sibal.py +++ b/sibal.py @@ -2517,37 +2517,6 @@ 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") @@ -4340,6 +4309,100 @@ def hitung_ulang_semua_saldo(): print("✅ SELESAI menghitung ulang semua saldo") + +def regenerate_kartu_persediaan_from_transactions(): + """Rebuild in-memory `kartu_persediaan` from transaksi_pemasukan and transaksi_pengeluaran. + This does NOT persist results to the database; it only reconstructs the view used by the UI. + """ + print("🔁 Membangun ulang kartu persediaan dari transaksi...") + + # Reset in-memory kartu persediaan for active user + uid = str(sibal_data.active_user_id) if sibal_data.active_user_id else None + # We'll rebuild _all_kartu_persediaan entries only for the active user + # Remove existing entries for this user + sibal_data._all_kartu_persediaan = [item for item in sibal_data._all_kartu_persediaan if str(item.get('user_id')) != str(uid)] + + # Collect relevant transactions + pemasukan_list = [t for t in sibal_data.transaksi_pemasukan if t.get('tipe') == 'pemasukan' and t.get('jenis') == 'penjualan_ikan'] + pengeluaran_list = [t for t in sibal_data.transaksi_pengeluaran if t.get('tipe') == 'pengeluaran' and t.get('jenis') == 'pembelian_persediaan'] + + # Build events list per kode_barang + events = [] + for p in pengeluaran_list: + kode = p.get('kode_barang') or 'IK001' + tanggal = p.get('tanggal') + qty = int(p.get('quantity', p.get('qty', 0)) or 0) + hpp = float(p.get('hpp', p.get('harga_beli', 0)) or 0) + events.append({'tanggal': tanggal, 'kode_barang': kode, 'masuk_qty': qty, 'masuk_harga': hpp, 'keterangan': p.get('keterangan', 'Pembelian')}) + + for s in pemasukan_list: + # For penjualan_ikan we treat as keluar + kode = 'IK001' + tanggal = s.get('tanggal') + qty = int(s.get('quantity', 0) or 0) + events.append({'tanggal': tanggal, 'kode_barang': kode, 'keluar_qty': qty, 'keluar_harga': None, 'keterangan': s.get('keterangan', 'Penjualan')}) + + # Sort events by tanggal (string 'YYYY-MM-DD' expected); stable sort preserves insertion order + events.sort(key=lambda x: x.get('tanggal') or '') + + # Process per kode_barang, compute running saldo + saldo_map = {} + for ev in events: + kode = ev['kode_barang'] + if kode not in saldo_map: + saldo_map[kode] = {'saldo_qty': 0, 'saldo_total': 0.0} + + current = saldo_map[kode] + masuk_qty = int(ev.get('masuk_qty', 0) or 0) + masuk_harga = float(ev.get('masuk_harga', 0) or 0) + keluar_qty = int(ev.get('keluar_qty', 0) or 0) + + masuk_total = masuk_qty * masuk_harga + keluar_total = 0.0 + + # If keluar, compute HPP using current saldo average + if keluar_qty > 0: + if current['saldo_qty'] > 0: + unit_hpp = current['saldo_total'] / current['saldo_qty'] + else: + unit_hpp = 0.0 + keluar_total = keluar_qty * unit_hpp + + # Update saldo + new_saldo_qty = current['saldo_qty'] + masuk_qty - keluar_qty + new_saldo_total = current['saldo_total'] + masuk_total - keluar_total + new_saldo_harga = (new_saldo_total / new_saldo_qty) if new_saldo_qty > 0 else 0.0 + + # Lookup nama barang + barang = next((b for b in sibal_data.master_persediaan if b['kode'] == kode), None) + nama_barang = barang['nama'] if barang else kode + + entry = { + 'tanggal': ev.get('tanggal'), + 'kode_barang': kode, + 'nama_barang': nama_barang, + 'masuk_qty': masuk_qty, + 'masuk_harga': masuk_harga, + 'masuk_total': masuk_total, + 'keluar_qty': keluar_qty, + 'keluar_harga': unit_hpp if keluar_qty > 0 else 0.0, + 'keluar_total': keluar_total, + 'saldo_qty': new_saldo_qty, + 'saldo_harga': new_saldo_harga, + 'saldo_total': new_saldo_total, + 'keterangan': ev.get('keterangan', '') + } + entry['user_id'] = sibal_data.active_user_id + + # Append to in-memory kartu persediaan + sibal_data._all_kartu_persediaan.append(entry) + + # Update map + saldo_map[kode]['saldo_qty'] = new_saldo_qty + saldo_map[kode]['saldo_total'] = new_saldo_total + + print(f"✅ Selesai membangun kartu persediaan ({len(events)} events processed)") + # Callback untuk menampilkan jurnal umum dengan format yang rapi @app.callback( [Output('tabel-jurnal-umum', 'children'), @@ -4728,8 +4791,8 @@ 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() + # Bangun ulang kartu persediaan dari transaksi sebelum menampilkan + regenerate_kartu_persediaan_from_transactions() if sibal_data.kartu_persediaan: # Kelompokkan berdasarkan kode barang @@ -4848,74 +4911,6 @@ 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'),