From 3fbf941cfdbbb8b50561b28d33e79cf3b8186adb Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 2 Feb 2026 11:23:13 +0700 Subject: [PATCH] update for new acquisition year replacement calculation minimum eac --- src/modules/equipment/Eac.py | 110 +++++++++++++----- .../equipment/__pycache__/Eac.cpython-311.pyc | Bin 15442 -> 16823 bytes 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/modules/equipment/Eac.py b/src/modules/equipment/Eac.py index 9eff21d..eb0a249 100644 --- a/src/modules/equipment/Eac.py +++ b/src/modules/equipment/Eac.py @@ -18,9 +18,12 @@ class Eac: try: # Mendapatkan koneksi dari config.py connections = get_connection() - connection = ( - connections[0] if isinstance(connections, tuple) else connections - ) + if isinstance(connections, tuple): + connection, connection_wo_db = connections + else: + connection = connections + connection_wo_db = None + if connection is None: print("Database connection failed.") return None @@ -28,42 +31,67 @@ class Eac: # Membuat cursor menggunakan DictCursor cursor = connection.cursor(cursor_factory=DictCursor) - # Query untuk mendapatkan data2 dasar + # Query untuk mendapatkan data2 dasar dari LCCA DB query_inflation_rate = """ select (SELECT value_num / 100 FROM lcc_ms_master WHERE name = 'inflation_rate') as inflation_rate , (SELECT value_num / 100 FROM lcc_ms_master WHERE name = 'discount_rate') as discount_rate - , (select COALESCE(rc_total_cost,0) from lcc_equipment_tr_data ltd where assetnum = %s and seq = 0) as rc_total_cost_0 + , (SELECT acquisition_cost FROM lcc_ms_equipment_data WHERE assetnum = %s) as rc_total_cost_0 , (SELECT value_num / 100 FROM lcc_ms_master WHERE name = 'history_inflation_rate') as history_inflation_rate , (SELECT value_num / 100 FROM lcc_ms_master WHERE name = 'history_future_inflation_rate_annual') as history_future_inflation_rate_annual + , (SELECT acquisition_year FROM lcc_ms_equipment_data WHERE assetnum = %s) as acquisition_year_fallback ; """ - cursor.execute(query_inflation_rate, (equipment_id,)) + cursor.execute(query_inflation_rate, (equipment_id, equipment_id)) inflation_rate_result = cursor.fetchone() if not inflation_rate_result: print("Inflation rate tidak ditemukan.") return None - + + # Fetch acquisition year from Maximo/Production DB if available + acquisition_year_maximo = None + if connection_wo_db: + try: + cursor_wo = connection_wo_db.cursor() + query_maximo = """ + SELECT CAST(DATE_PART('year', max(reportdate)) AS INTEGER) + FROM wo_maximo wm + WHERE wm.asset_assetnum = %s AND wm.asset_replacecost > 0 + GROUP BY CAST(DATE_PART('year', reportdate) AS INTEGER) + ORDER BY CAST(DATE_PART('year', reportdate) AS INTEGER) ASC + LIMIT 1 + """ + cursor_wo.execute(query_maximo, (equipment_id,)) + res_wo = cursor_wo.fetchone() + if res_wo and res_wo[0]: + acquisition_year_maximo = res_wo[0] + cursor_wo.close() + except Exception as e: + print(f"Error fetching from maximo DB: {e}") + + # Use Maximo year if available, else fallback + acquisition_year = acquisition_year_maximo if acquisition_year_maximo else inflation_rate_result["acquisition_year_fallback"] inflation_rate = inflation_rate_result["inflation_rate"] history_inflation_rate = inflation_rate_result["history_inflation_rate"] history_future_inflation_rate = inflation_rate_result["history_future_inflation_rate_annual"] disc_rate = inflation_rate_result["discount_rate"] rc_total_cost_0 = inflation_rate_result["rc_total_cost_0"] + last_seq = 0 last_npv = 0 # Query untuk mendapatkan data dengan seq dan is_actual query_data_actual = """ SELECT assetnum, tahun, seq, is_actual, rc_total_cost FROM lcc_equipment_tr_data - WHERE is_actual = 1 AND seq != 0 + WHERE is_actual = 1 AND seq != 0 AND tahun >= %s AND assetnum = %s ORDER BY seq; """ - cursor.execute(query_data_actual, (equipment_id,)) + cursor.execute(query_data_actual, (acquisition_year, equipment_id)) data_actual = cursor.fetchall() - + # Variabel untuk menyimpan hasil NPV dan PMT per baris npv_results = [] cumulative_values = [] # Menyimpan nilai kumulatif hingga baris ke-n @@ -77,24 +105,36 @@ class Eac: value / ((1 + history_inflation_rate) ** (i + 1)) for i, value in enumerate(cumulative_values) ) + # Recalculate seq based on new acquisition year + current_seq = row["tahun"] - acquisition_year + + if current_seq <= 0: + current_seq = 0 # Avoid negative or zero periods for PMT if year <= acquisition_year + # Menghitung PMT biaya maintenance # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] - # dimana PV = final_value, r = history_future_inflation_rate, n = row["seq"] - pmt_mnt_cost = -npf.pmt(history_future_inflation_rate, row["seq"], final_value) + # dimana PV = final_value, r = history_future_inflation_rate, n = current_seq + if current_seq > 0: + pmt_mnt_cost = -npf.pmt(history_future_inflation_rate, current_seq, final_value) + else: + pmt_mnt_cost = 0.0 # Menghitung PMT biaya disposal # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] - # dimana PV = 0.05 * rc_total_cost_0, r = disc_rate, n = row["seq"] - # rc_total_cost_0 adalah biaya akuisisi awal (seq = 0) - eac_disposal_cost = -npf.pmt(disc_rate, row["seq"], 0, 0.05 * rc_total_cost_0) if row["seq"] > 0 else 0.0 + # dimana PV = 0.05 * rc_total_cost_0, r = disc_rate, n = current_seq + # rc_total_cost_0 adalah biaya akuisisi awal + eac_disposal_cost = -npf.pmt(disc_rate, current_seq, 0, 0.05 * rc_total_cost_0) if current_seq > 0 else 0.0 # Menghitung PMT biaya akuisisi # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] - # dimana PV = rc_total_cost_0, r = disc_rate, n = row["seq"] - # rc_total_cost_0 adalah biaya akuisisi awal (seq = 0) + # dimana PV = rc_total_cost_0, r = disc_rate, n = current_seq + # rc_total_cost_0 adalah biaya akuisisi awal # disc_rate adalah discount rate dari database - # row["seq"] adalah periode ke-n - pmt_aq_cost = -npf.pmt(disc_rate, row["seq"], rc_total_cost_0) + # current_seq adalah periode ke-n + if current_seq > 0: + pmt_aq_cost = -npf.pmt(disc_rate, current_seq, rc_total_cost_0) + else: + pmt_aq_cost = 0.0 eac = pmt_mnt_cost + pmt_aq_cost npv_results.append( @@ -129,21 +169,24 @@ class Eac: ), ) - last_seq = row["seq"] + last_seq = row["seq"] # Keep logical linking if needed, but calculations rely on current_seq last_npv = float(final_value) # Commit perubahan connection.commit() # Query untuk mendapatkan data dengan seq dan is_actual = 0 + # Filter by acquisition_year_ref to match current acquisition_year query_data_proyeksi = """ SELECT assetnum, tahun, seq, is_actual, rc_total_cost FROM lcc_equipment_tr_data WHERE assetnum = %s AND is_actual = 0 + AND (acquisition_year_ref = %s OR acquisition_year_ref IS NULL) ORDER BY seq; """ - cursor.execute(query_data_proyeksi, (equipment_id,)) + cursor.execute(query_data_proyeksi, (equipment_id, acquisition_year)) data_proyeksi = cursor.fetchall() + cumulative_values = [] # Menghitung NPV dan PMT secara bertahap untuk data proyeksi @@ -170,37 +213,43 @@ class Eac: # Total NPV pada titik proyeksi ini = NPV aktual terakhir + biaya proyeksi yang didiskontokan final_value = float(last_npv) + float(discounted_proj) + # Recalculate seq based on new acquisition year + current_seq = row["tahun"] - acquisition_year + if current_seq <= 0: + current_seq = 1 # Fallback, though for projection it should be > 0 + # Gunakan seq penuh (jumlah periode dari akuisisi) untuk menghitung pembayaran tahunan pemeliharaan. # Menggunakan hanya selisih dari seq aktual terakhir # (sisa_periode) mengamortisasi seluruh nilai sekarang selama # sejumlah periode yang sangat kecil untuk proyeksi pertama dan menghasilkan lonjakan. - # Menggunakan row["seq"] menjaga periode amortisasi konsisten dengan perhitungan lain + # Menggunakan current_seq menjaga periode amortisasi konsisten dengan perhitungan lain # dan mencegah lonjakan setelah tahun berjalan. # amortisasi adalah proses pencatatan biaya aset selama masa manfaatnya. - periods = int(row["seq"]) if int(row.get("seq", 0)) > 0 else 1 + periods = int(current_seq) # Menghitung PMT # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] - # dimana PV = final_value, r = disc_rate, n = row["seq"] + # dimana PV = final_value, r = disc_rate, n = current_seq # periods adalah jumlah periode # final_value adalah PV pada titik proyeksi periods pmt_mnt_cost = -float(npf.pmt(disc_rate, periods, final_value)) - eac_disposal_cost_proyeksi = -npf.pmt(disc_rate, row["seq"], 0, 0.05 * rc_total_cost_0) if row["seq"] > 0 else 0.0 + eac_disposal_cost_proyeksi = -npf.pmt(disc_rate, current_seq, 0, 0.05 * rc_total_cost_0) if current_seq > 0 else 0.0 # menghitung PMT biaya akuisisi # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] - # dimana PV = rc_total_cost_0, r = disc_rate, n = row["seq"] - # rc_total_cost_0 adalah biaya akuisisi awal (seq = 0) + # dimana PV = rc_total_cost_0, r = disc_rate, n = current_seq + # rc_total_cost_0 adalah biaya akuisisi awal # disc_rate adalah discount rate dari database - # row["seq"] adalah periode ke-n - pmt_aq_cost = -float(npf.pmt(disc_rate, row["seq"], rc_total_cost_0)) + # current_seq adalah periode ke-n + pmt_aq_cost = -float(npf.pmt(disc_rate, current_seq, rc_total_cost_0)) eac = float(pmt_mnt_cost) + float(pmt_aq_cost) + npv_results.append( { - "seq": row["seq"], + "seq": current_seq, "year": row["tahun"], "npv": final_value, "pmt": pmt_mnt_cost, @@ -279,6 +328,7 @@ class Eac: } else: positives = [r for r in rslt if float(r.get("eac", 0)) > 0] + if positives: lowest_eac_record = min(positives, key=lambda x: float(x["eac"])) else: diff --git a/src/modules/equipment/__pycache__/Eac.cpython-311.pyc b/src/modules/equipment/__pycache__/Eac.cpython-311.pyc index ebe1ad9677b5a1451a3c075961c794559ddee29f..35cc311eda92c433e8a8b3c2371301c7351bed79 100644 GIT binary patch delta 4707 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