Kartu Perdana
This commit is contained in:
parent
226b35479f
commit
ad450980c3
Binary file not shown.
197
sibal.py
197
sibal.py
@ -2517,37 +2517,6 @@ def kartu_persediaan_layout():
|
|||||||
className="btn btn-primary",
|
className="btn btn-primary",
|
||||||
style={'marginBottom': '20px'}
|
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")
|
html.Div(id='tabel-kartu-persediaan', className="table-container")
|
||||||
], className="card-content")
|
], className="card-content")
|
||||||
], className="glass-card")
|
], className="glass-card")
|
||||||
@ -4340,6 +4309,100 @@ def hitung_ulang_semua_saldo():
|
|||||||
|
|
||||||
print("✅ SELESAI menghitung 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
|
# Callback untuk menampilkan jurnal umum dengan format yang rapi
|
||||||
@app.callback(
|
@app.callback(
|
||||||
[Output('tabel-jurnal-umum', 'children'),
|
[Output('tabel-jurnal-umum', 'children'),
|
||||||
@ -4728,8 +4791,8 @@ def update_kartu_persediaan(n_clicks):
|
|||||||
"""Update kartu persediaan dengan perhitungan real-time"""
|
"""Update kartu persediaan dengan perhitungan real-time"""
|
||||||
print(f"🔄 Memperbarui kartu persediaan...")
|
print(f"🔄 Memperbarui kartu persediaan...")
|
||||||
|
|
||||||
# HITUNG ULANG SALDO SEBELUM MENAMPILKAN
|
# Bangun ulang kartu persediaan dari transaksi sebelum menampilkan
|
||||||
hitung_ulang_semua_saldo()
|
regenerate_kartu_persediaan_from_transactions()
|
||||||
|
|
||||||
if sibal_data.kartu_persediaan:
|
if sibal_data.kartu_persediaan:
|
||||||
# Kelompokkan berdasarkan kode barang
|
# Kelompokkan berdasarkan kode barang
|
||||||
@ -4849,74 +4912,6 @@ def update_kartu_persediaan(n_clicks):
|
|||||||
style={'textAlign': 'center', 'color': COLORS['gray_500'], 'fontSize': '14px'})
|
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(
|
@app.callback(
|
||||||
Output('detail-buku-besar', 'children'),
|
Output('detail-buku-besar', 'children'),
|
||||||
Input('dropdown-akun-buku-besar', 'value')
|
Input('dropdown-akun-buku-besar', 'value')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user