feat: Introduce linear projection for plant cost breakdown and decouple plant simulation from simulation_id.

main
MrWaradana 3 weeks ago
parent 8c01702c7a
commit 39a53c477a

@ -158,7 +158,7 @@ async def _trigger_masterdata_recalculation(
directory_path = os.path.abspath( directory_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../modules/plant") os.path.join(os.path.dirname(__file__), "../modules/plant")
) )
script_path = os.path.join(directory_path, "run2.py") script_path = os.path.join(directory_path, "run_plant_simulation.py")
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"python", "python",

@ -155,7 +155,7 @@ async def _trigger_masterdata_simulation_recalculation(
directory_path = os.path.abspath( directory_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../modules/plant") os.path.join(os.path.dirname(__file__), "../modules/plant")
) )
script_path = os.path.join(directory_path, "run2.py") script_path = os.path.join(directory_path, "run_plant_simulation.py")
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"python", "python",

@ -11,21 +11,15 @@ import math
import uuid import uuid
SIMULATION_ID = os.getenv("PLANT_SIMULATION_ID")
MASTER_TABLE = "lcc_ms_master_simulations"
PLANT_TABLE = "lcc_plant_tr_data_simulations"
def validate_number(n):
return n if n is not None else 0
def normalize_db_value(value): def normalize_db_value(value):
"""Convert numpy scalars to native Python types for psycopg2.""" """Convert numpy scalars to native Python types for psycopg2."""
if isinstance(value, np.generic): if isinstance(value, np.generic):
return value.item() return value.item()
return value return value
def validate_number(n):
return n if n is not None else 0
def cumulative_npv(values, rate, initial_cf0=0.0): def cumulative_npv(values, rate, initial_cf0=0.0):
""" """
@ -76,12 +70,69 @@ def hitung_pv(rate, nper, fv):
def hitung_irr(cashflows: list): def hitung_irr(cashflows: list):
return npf.irr(cashflows) return npf.irr(cashflows)
def getproyeksilinier(years, values, iterations=30, target_years=None):
"""
Jika target_years diberikan (list[int]), fungsi mengembalikan prediksi untuk tahun-tahun tersebut.
Jika target_years None, perilaku lama tetap: memproyeksikan dari max(years)+1 sepanjang (iterations - len(years)).
Catatan:
- Jika data historis < 2 titik, fallback pakai nilai terakhir (atau 0).
- Jika semua year sama (degenerate), fallback juga.
"""
if len(years) != len(values):
raise ValueError("Panjang years dan values harus sama")
# bersihkan pasangan (year, value) yang year-nya None
pairs = [(int(y), float(v)) for y, v in zip(years, values) if y is not None]
if not pairs:
# tidak ada data sama sekali
if target_years is None:
return {}
return {int(y): 0.0 for y in target_years}
years_clean = [p[0] for p in pairs]
values_clean = [p[1] for p in pairs]
# fallback kalau data tidak cukup untuk regresi
if len(set(years_clean)) < 2 or len(values_clean) < 2:
last_val = float(values_clean[-1]) if values_clean else 0.0
if target_years is None:
# perilaku lama
n_hist = len(years_clean)
if iterations <= n_hist:
return {}
start_year = max(years_clean) + 1
n_projection = iterations - n_hist
return {start_year + i: last_val for i in range(n_projection)}
return {int(y): last_val for y in target_years}
# regresi linier y = a*x + b
x = np.array(years_clean, dtype=float)
y = np.array(values_clean, dtype=float)
a, b = np.polyfit(x, y, 1)
def _predict(yr: int) -> float:
v = float(a * yr + b)
# optional: kalau tidak boleh negatif, clamp
return max(0.0, v)
# mode target_years: prediksi untuk tahun tertentu
if target_years is not None:
return {int(yr): _predict(int(yr)) for yr in target_years}
# mode lama: generate dari tahun setelah histori
n_hist = len(years_clean)
if iterations <= n_hist:
raise ValueError(
f"iterations ({iterations}) harus lebih besar dari jumlah data historis ({n_hist})"
)
start_year = max(years_clean) + 1
n_projection = iterations - n_hist
return {start_year + i: _predict(start_year + i) for i in range(n_projection)}
def main():
if not SIMULATION_ID:
print("Environment variable PLANT_SIMULATION_ID is required for simulations.")
sys.exit(1)
def main():
connections = get_connection() connections = get_connection()
conn = connections[0] if isinstance(connections, tuple) else connections conn = connections[0] if isinstance(connections, tuple) else connections
if conn is None: if conn is None:
@ -98,22 +149,18 @@ def main():
cur = conn.cursor() cur = conn.cursor()
# ### LOCKING: kunci tabel simulasi # ### LOCKING: kunci tabel lcc_plant_tr_data
# Mode SHARE ROW EXCLUSIVE: # Mode SHARE ROW EXCLUSIVE:
# - Menghalangi INSERT/UPDATE/DELETE di tabel ini # - Menghalangi INSERT/UPDATE/DELETE di tabel ini
# - Menghalangi lock SHARE ROW EXCLUSIVE lain → script ngantri satu per satu # - Menghalangi lock SHARE ROW EXCLUSIVE lain → script ngantri satu per satu
cur.execute(f"LOCK TABLE {PLANT_TABLE} IN SHARE ROW EXCLUSIVE MODE") cur.execute("LOCK TABLE lcc_plant_tr_data IN SHARE ROW EXCLUSIVE MODE")
# 0 Mendapatkan master parameter dari tabel lcc_ms_master # 0 Mendapatkan master parameter dari tabel lcc_ms_master
cur.execute( cur.execute("""
f"""
SELECT name, SELECT name,
value_num AS value value_num AS value
FROM {MASTER_TABLE} FROM lcc_ms_master
WHERE simulation_id = %s """)
""",
(SIMULATION_ID,),
)
param_rows = cur.fetchall() param_rows = cur.fetchall()
param_map = {name: val for (name, val) in param_rows} param_map = {name: val for (name, val) in param_rows}
@ -124,43 +171,29 @@ def main():
# 0-1 Generate New data Projection (is_actual=0) if not exist # 0-1 Generate New data Projection (is_actual=0) if not exist
# Hapus data projection lama (is_actual = 0) # Hapus data projection lama (is_actual = 0)
cur.execute( cur.execute("""
f"""
DELETE DELETE
FROM {PLANT_TABLE} FROM lcc_plant_tr_data
WHERE is_actual = 0 WHERE is_actual = 0
AND simulation_id = %s """)
""",
(SIMULATION_ID,),
)
# Hitung kebutuhan jumlah baris projection baru agar total (actual + projection) # Hitung kebutuhan jumlah baris projection baru agar total (actual + projection)
# sama dengan parameter umur_teknis # sama dengan parameter umur_teknis
cur.execute( cur.execute("""
f"""
SELECT COALESCE(COUNT(*), 0) SELECT COALESCE(COUNT(*), 0)
FROM {PLANT_TABLE} FROM lcc_plant_tr_data
WHERE is_actual = 1 WHERE is_actual = 1
AND simulation_id = %s """)
""",
(SIMULATION_ID,),
)
count_actual = cur.fetchone()[0] if cur.rowcount != -1 else 0 count_actual = cur.fetchone()[0] if cur.rowcount != -1 else 0
umur_teknis = int(get_param("umur_teknis")) umur_teknis = int(get_param("umur_teknis"))
proj_needed = max(0, umur_teknis - int(count_actual)) proj_needed = max(0, umur_teknis - int(count_actual))
# Ambil seq dan tahun terakhir sebagai titik awal penomoran berikutnya # Ambil seq dan tahun terakhir sebagai titik awal penomoran berikutnya
cur.execute( cur.execute("SELECT COALESCE(MAX(seq), 0) FROM lcc_plant_tr_data")
f"SELECT COALESCE(MAX(seq), 0) FROM {PLANT_TABLE} WHERE simulation_id = %s",
(SIMULATION_ID,),
)
last_seq = int(cur.fetchone()[0]) last_seq = int(cur.fetchone()[0])
cur.execute( cur.execute("SELECT COALESCE(MAX(tahun), 0) FROM lcc_plant_tr_data")
f"SELECT COALESCE(MAX(tahun), 0) FROM {PLANT_TABLE} WHERE simulation_id = %s",
(SIMULATION_ID,),
)
last_year = int(cur.fetchone()[0]) last_year = int(cur.fetchone()[0])
# Jika belum ada tahun sama sekali, gunakan tahun_cod-1 sebagai dasar # Jika belum ada tahun sama sekali, gunakan tahun_cod-1 sebagai dasar
@ -181,29 +214,62 @@ def main():
next_year += 1 next_year += 1
insert_sql = ( insert_sql = (
f"INSERT INTO {PLANT_TABLE} (id, seq, tahun, is_actual, simulation_id, created_at, created_by) " "INSERT INTO lcc_plant_tr_data (id, seq, tahun, is_actual, created_at, created_by) "
"VALUES (%s, %s, %s, 0, %s, CURRENT_TIMESTAMP, 'SYS')" "VALUES (%s, %s, %s, 0, CURRENT_TIMESTAMP, 'SYS')"
) )
sim_values = [(*val, SIMULATION_ID) for val in values] cur.executemany(insert_sql, values)
cur.executemany(insert_sql, sim_values)
# 1. Ambil data awal # 1. Ambil data awal
select_sql = f""" select_sql = """
SELECT * SELECT *
FROM {PLANT_TABLE} FROM lcc_plant_tr_data
WHERE simulation_id = %s
ORDER BY seq \ ORDER BY seq \
""" """
cur.execute(select_sql, (SIMULATION_ID,)) cur.execute(select_sql)
col_names = [desc[0] for desc in cur.description] col_names = [desc[0] for desc in cur.description]
rows = cur.fetchall() rows = cur.fetchall()
# ============================================================
# PROYEKSI LINIER untuk COST BD berdasarkan histori (is_actual=1)
# ============================================================
hist_years_om, hist_vals_om = [], []
hist_years_pm, hist_vals_pm = [], []
hist_years_bd, hist_vals_bd = [], []
projection_years = []
for r in rows:
d = dict(zip(col_names, r))
yr = d.get("tahun")
if yr is None:
continue
yr = int(yr)
if d.get("is_actual") == 1:
# ambil histori (boleh 0 kalau null)
hist_years_om.append(yr)
hist_vals_om.append(validate_number(d.get("cost_bd_om")))
hist_years_pm.append(yr)
hist_vals_pm.append(validate_number(d.get("cost_bd_pm_nonmi")))
hist_years_bd.append(yr)
hist_vals_bd.append(validate_number(d.get("cost_bd_bd")))
else:
# tahun-tahun projection yang ingin diprediksi
projection_years.append(yr)
# buat mapping prediksi per tahun untuk masing-masing komponen cost BD
proj_cost_bd_om = getproyeksilinier(hist_years_om, hist_vals_om, target_years=projection_years)
proj_cost_bd_pm = getproyeksilinier(hist_years_pm, hist_vals_pm, target_years=projection_years)
proj_cost_bd_bd = getproyeksilinier(hist_years_bd, hist_vals_bd, target_years=projection_years)
print(f"Jumlah baris yang akan di-update: {len(rows)}") print(f"Jumlah baris yang akan di-update: {len(rows)}")
# 2. Siapkan data untuk bulk UPDATE # 2. Siapkan data untuk bulk UPDATE
update_sql = f""" update_sql = """
UPDATE {PLANT_TABLE} UPDATE lcc_plant_tr_data
SET net_capacity_factor = %s, SET net_capacity_factor = %s,
eaf = %s, eaf = %s,
production_bruto = %s, production_bruto = %s,
@ -269,9 +335,9 @@ def main():
chart_capex_component_a = %s, chart_capex_component_a = %s,
chart_capex_biaya_investasi_tambahan = %s, chart_capex_biaya_investasi_tambahan = %s,
chart_capex_acquisition_cost = %s, chart_capex_acquisition_cost = %s,
chart_capex_annualized = %s chart_capex_annualized = %s,
WHERE seq = %s cost_disposal_cost = %s
AND simulation_id = %s \ WHERE seq = %s \
""" """
# Ambil parameter dari tabel (fungsi get_param sudah kamu buat sebelumnya) # Ambil parameter dari tabel (fungsi get_param sudah kamu buat sebelumnya)
@ -333,32 +399,25 @@ def main():
net_capacity_factor_v = 0 net_capacity_factor_v = 0
eaf_v = 0 eaf_v = 0
# Prefetch CF/EAF master data once to avoid repeated queries per row # Prefetch master data CF dan EAF sekali saja di luar loop
cur.execute( cur.execute("""
"""
SELECT year as tahun, cf, eaf SELECT year as tahun, cf, eaf
FROM lcc_ms_year_data FROM lcc_ms_year_data
""" order by year asc
) """)
year_rows = cur.fetchall() year_rows = cur.fetchall()
year_data_map = { year_data_map = {int(t): (validate_number(cf), validate_number(eaf)) for (t, cf, eaf) in year_rows if
int(t): (validate_number(cf), validate_number(eaf)) t is not None}
for (t, cf, eaf) in year_rows
if t is not None
}
for row in rows: for row in rows:
# row adalah tuple sesuai urutan select_sql # row adalah tuple sesuai urutan select_sql
data = dict(zip(col_names, row)) data = dict(zip(col_names, row))
seq = data["seq"] # primary key / unique key untuk WHERE seq = data["seq"] # primary key / unique key untuk WHERE
yr = int(data["tahun"]) if data.get("tahun") is not None else None
# Ambil net_capacity_factor dan eaf dari year-data cache berdasarkan tahun # Ambil net_capacity_factor dan eaf dari cache berdasarkan tahun
cf_eaf = ( cf_eaf = year_data_map.get(int(data["tahun"])) if data.get("tahun") is not None else None
year_data_map.get(int(data["tahun"]))
if data.get("tahun") is not None
else None
)
if cf_eaf: if cf_eaf:
net_capacity_factor_v, eaf_v = cf_eaf net_capacity_factor_v, eaf_v = cf_eaf
else: else:
@ -366,8 +425,6 @@ def main():
eaf_v = 0 eaf_v = 0
if data["is_actual"] == 1: if data["is_actual"] == 1:
net_capacity_factor = validate_number(data["net_capacity_factor"])
eaf = validate_number(data["eaf"])
production_bruto = validate_number(data["production_bruto"]) production_bruto = validate_number(data["production_bruto"])
production_netto = validate_number(data["production_netto"]) production_netto = validate_number(data["production_netto"])
energy_sales = production_netto energy_sales = production_netto
@ -381,8 +438,6 @@ def main():
cost_bd_pm_nonmi = validate_number(data["cost_bd_pm_nonmi"]) cost_bd_pm_nonmi = validate_number(data["cost_bd_pm_nonmi"])
cost_bd_bd = validate_number(data["cost_bd_bd"]) cost_bd_bd = validate_number(data["cost_bd_bd"])
else: else:
net_capacity_factor = net_capacity_factor # last value
eaf = eaf # last value
production_netto = net_capacity_factor * 8760 * daya_mampu_netto / 100 production_netto = net_capacity_factor * 8760 * daya_mampu_netto / 100
production_bruto = production_netto / (100 - (auxiliary + susut_trafo)) * 100 production_bruto = production_netto / (100 - (auxiliary + susut_trafo)) * 100
energy_sales = production_netto energy_sales = production_netto
@ -392,9 +447,15 @@ def main():
revenue_c = price_c * production_netto * 1000 / 1000000 revenue_c = price_c * production_netto * 1000 / 1000000
revenue_d = price_d * production_netto * 1000 / 1000000 revenue_d = price_d * production_netto * 1000 / 1000000
cost_c_fuel = fuel_consumption * harga_bahan_bakar / 1000000 cost_c_fuel = fuel_consumption * harga_bahan_bakar / 1000000
cost_bd_om = cost_bd_om # last value # default fallback tetap pakai last value kalau tahun kosong / prediksi tidak ada
cost_bd_pm_nonmi = cost_bd_pm_nonmi # last value if yr is not None:
cost_bd_bd = cost_bd_bd # last value cost_bd_om = proj_cost_bd_om.get(yr, cost_bd_om)
cost_bd_pm_nonmi = proj_cost_bd_pm.get(yr, cost_bd_pm_nonmi)
cost_bd_bd = proj_cost_bd_bd.get(yr, cost_bd_bd)
else:
cost_bd_om = cost_bd_om
cost_bd_pm_nonmi = cost_bd_pm_nonmi
cost_bd_bd = cost_bd_bd
net_capacity_factor = net_capacity_factor_v net_capacity_factor = net_capacity_factor_v
eaf = eaf_v eaf = eaf_v
@ -440,6 +501,7 @@ def main():
# + cost_a_pinjaman # + cost_a_pinjaman
# + cost_a_depreciation # + cost_a_depreciation
) )
else: else:
cost_a_replacement = 0 cost_a_replacement = 0
cost_a_pm = 0 cost_a_pm = 0
@ -455,11 +517,15 @@ def main():
chart_capex_component_a = cost_a_acquisition chart_capex_component_a = cost_a_acquisition
chart_capex_annualized = cost_a_annualized chart_capex_annualized = cost_a_annualized
cost_disposal_cost = -npf.pmt(discount_rate, seq, 0, 0.05 * total_project_cost)
else: else:
chart_capex_component_a = total_project_cost chart_capex_component_a = total_project_cost
chart_capex_annualized = 0 chart_capex_annualized = 0
cost_a_pv = 0 cost_a_pv = 0
cost_a_annualized = 0 cost_a_annualized = 0
cost_disposal_cost = 0
chart_capex_biaya_investasi_tambahan = 0 chart_capex_biaya_investasi_tambahan = 0
chart_capex_acquisition_cost = 0 chart_capex_acquisition_cost = 0
@ -544,6 +610,8 @@ def main():
calc4_free_cash_flow_on_equity_array.append(calc4_free_cash_flow_on_equity) calc4_free_cash_flow_on_equity_array.append(calc4_free_cash_flow_on_equity)
calc4_discounted_fcf_on_equity = hitung_pv(wacc_on_equity, seq, calc4_free_cash_flow_on_equity) calc4_discounted_fcf_on_equity = hitung_pv(wacc_on_equity, seq, calc4_free_cash_flow_on_equity)
row_params = ( row_params = (
net_capacity_factor, net_capacity_factor,
eaf, eaf,
@ -611,8 +679,8 @@ def main():
chart_capex_biaya_investasi_tambahan, chart_capex_biaya_investasi_tambahan,
chart_capex_acquisition_cost, chart_capex_acquisition_cost,
chart_capex_annualized, chart_capex_annualized,
seq, cost_disposal_cost,
SIMULATION_ID, seq # <-- penting: ini untuk WHERE
) )
params.append(tuple(normalize_db_value(v) for v in row_params)) params.append(tuple(normalize_db_value(v) for v in row_params))
@ -639,11 +707,10 @@ def main():
ROA_TO_L = sum(calc2_earning_after_tax_array_sampai_sekarang) / sum( ROA_TO_L = sum(calc2_earning_after_tax_array_sampai_sekarang) / sum(
total_residual_value_array_sampai_sekarang) * 100 # dalam % total_residual_value_array_sampai_sekarang) * 100 # dalam %
update_kpi_sql = f""" update_kpi_sql = """
UPDATE {MASTER_TABLE} UPDATE lcc_ms_master
SET value_num = %s SET value_num = %s
WHERE name = %s WHERE name = %s \
AND simulation_id = %s \
""" """
kpi_params_raw = [ kpi_params_raw = [
@ -656,11 +723,7 @@ def main():
] ]
kpi_params = [ kpi_params = [
( (None if (value is None or isinstance(value, float) and math.isnan(value)) else value, key)
None if (value is None or isinstance(value, float) and math.isnan(value)) else value,
key,
SIMULATION_ID,
)
for value, key in kpi_params_raw for value, key in kpi_params_raw
] ]

@ -9,11 +9,14 @@ from src.models import StandardResponse
from .schema import ( from .schema import (
PlantFSTransactionDataCreate, PlantFSTransactionDataCreate,
PlantFSTransactionDataImport,
PlantFSTransactionDataPagination, PlantFSTransactionDataPagination,
PlantFSTransactionDataRead, PlantFSTransactionDataRead,
PlantFSTransactionDataUpdate, PlantFSTransactionDataUpdate,
) )
from .service import create, delete, get, get_all, update from .service import create, delete, get, get_all, update, update_fs_charts_from_matrix
from typing import List
router = APIRouter() router = APIRouter()
@ -119,3 +122,26 @@ async def delete_fs_transaction(
return StandardResponse(data=record, message="Data deleted successfully") return StandardResponse(data=record, message="Data deleted successfully")
@router.post(
"/import/charts",
response_model=StandardResponse[List[PlantFSTransactionDataRead]],
)
async def import_fs_charts(
db_session: DbSession,
payload: PlantFSTransactionDataImport,
current_user: CurrentUser,
):
updated, missing = await update_fs_charts_from_matrix(
db_session=db_session,
payload=payload,
updated_by=getattr(current_user, "user_id", None) if current_user else None,
)
msg = "Data imported successfully."
if missing:
msg += f" Note: Years {missing} were not found."
return StandardResponse(data=updated, message=msg)

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import Any, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field from pydantic import Field
@ -84,3 +84,8 @@ class PlantFSTransactionDataRead(PlantFSTransactionDataBase):
class PlantFSTransactionDataPagination(Pagination): class PlantFSTransactionDataPagination(Pagination):
items: List[PlantFSTransactionDataRead] = [] items: List[PlantFSTransactionDataRead] = []
class PlantFSTransactionDataImport(DefaultBase):
data: List[List[Any]]
seq: Optional[int] = None

@ -1,5 +1,5 @@
import logging import logging
from typing import Optional from typing import Any, Dict, List, Optional
from sqlalchemy import Delete, Select, String, cast from sqlalchemy import Delete, Select, String, cast
@ -9,6 +9,7 @@ from src.database.service import search_filter_sort_paginate
from src.plant_fs_transaction_data.model import PlantFSTransactionData from src.plant_fs_transaction_data.model import PlantFSTransactionData
from src.plant_fs_transaction_data.schema import ( from src.plant_fs_transaction_data.schema import (
PlantFSTransactionDataCreate, PlantFSTransactionDataCreate,
PlantFSTransactionDataImport,
PlantFSTransactionDataUpdate, PlantFSTransactionDataUpdate,
) )
@ -116,3 +117,130 @@ async def delete(*, db_session: DbSession, fs_transaction_id: str) -> None:
) )
await db_session.execute(query) await db_session.execute(query)
await db_session.commit() await db_session.commit()
def _safe_float(x: object) -> float:
"""Safely convert `x` to float, returning 0.0 for None or invalid values."""
try:
if x is None:
return 0.0
return float(x)
except Exception:
return 0.0
_FS_LABEL_FIELD_MAP: Dict[str, str] = {
"Total Revenue": "fs_chart_total_revenue",
"Revenue A": "fs_chart_revenue_a",
"Revenue B": "fs_chart_revenue_b",
"Revenue C": "fs_chart_revenue_c",
"Revenue D": "fs_chart_revenue_d",
"Revenue Annualized": "fs_chart_revenue_annualized",
"Fuel Cost (Component C)": "fs_chart_fuel_cost_component_c",
"Fuel Cost": "fs_chart_fuel_cost",
"Fuel Cost Annualized": "fs_chart_fuel_cost_annualized",
"O and M Cost (Component B and D)": "fs_chart_oem_component_bd",
"O and M Cost": "fs_chart_oem_bd_cost",
"Periodic Maintenance Cost (NonMI)": "fs_chart_oem_periodic_maintenance_cost",
"O and M Cost Annualized": "fs_chart_oem_annualized",
"Capex (Component A)": "fs_chart_capex_component_a",
"Biaya Investasi Tambahan": "fs_chart_capex_biaya_investasi_tambahan",
"Acquisition Cost": "fs_chart_capex_acquisition_cost",
"Capex Annualized": "fs_chart_capex_annualized",
}
def _extract_years(header_row: List[Any]) -> List[int]:
years: List[int] = []
for cell in header_row[2:]:
if cell is None:
continue
try:
years.append(int(float(cell)))
except Exception:
continue
return years
def _resolve_label(row: List[Any]) -> Optional[str]:
for candidate in row[:2]:
if isinstance(candidate, str):
label = candidate.strip()
if label:
return label
return None
def _build_fs_year_value_map(matrix: List[List[Any]]) -> Dict[int, Dict[str, float]]:
if not matrix:
return {}
header = matrix[0]
years = _extract_years(header)
if not years:
return {}
year_map: Dict[int, Dict[str, float]] = {year: {} for year in years}
for row in matrix[1:]:
label = _resolve_label(row)
if not label:
continue
field_name = _FS_LABEL_FIELD_MAP.get(label)
if not field_name:
continue
for idx, year in enumerate(years):
col_idx = idx + 2
if col_idx >= len(row):
continue
value = row[col_idx]
if value is None:
continue
try:
year_map[year][field_name] = _safe_float(value)
except Exception:
continue
return year_map
async def update_fs_charts_from_matrix(
*,
db_session: DbSession,
payload: PlantFSTransactionDataImport,
updated_by: Optional[str] = None,
):
"""Update fs_* chart columns based on a transposed matrix payload."""
year_value_map = _build_fs_year_value_map(payload.data)
if not year_value_map:
return [], []
updated_records: List[PlantFSTransactionData] = []
missing_years: List[int] = []
for year, field_values in year_value_map.items():
if not field_values:
continue
query = Select(PlantFSTransactionData).where(PlantFSTransactionData.tahun == year)
if payload.seq is not None:
query = query.where(PlantFSTransactionData.seq == payload.seq)
result = await db_session.execute(query)
records = result.scalars().all()
if not records:
missing_years.append(year)
continue
for record in records:
for field_name, value in field_values.items():
setattr(record, field_name, value)
if updated_by:
record.updated_by = updated_by
updated_records.append(record)
await db_session.commit()
return updated_records, missing_years

@ -218,7 +218,7 @@ async def create(
) )
# Construct path to the script # Construct path to the script
script_path = os.path.join(directory_path, "run2.py") script_path = os.path.join(directory_path, "run_plant_simulation.py")
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
@ -261,7 +261,7 @@ async def update(
) )
# Construct path to the script # Construct path to the script
script_path = os.path.join(directory_path, "run2.py") script_path = os.path.join(directory_path, "run_plant_simulation.py")
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(

@ -229,7 +229,7 @@ async def create(
) )
# Construct path to the script # Construct path to the script
script_path = os.path.join(directory_path, "run2.py") script_path = os.path.join(directory_path, "run_plant_simulation.py")
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
@ -272,7 +272,7 @@ async def update(
) )
# Construct path to the script # Construct path to the script
script_path = os.path.join(directory_path, "run2.py") script_path = os.path.join(directory_path, "run_plant_simulation.py")
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(

Loading…
Cancel
Save