From 4ff7e29d316e2be52bd4bd6ef8c79a58960b6107 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 30 Jan 2026 16:22:02 +0700 Subject: [PATCH] feat: Dynamically update equipment acquisition year from production data and link prediction records to this reference. --- src/equipment/model.py | 1 + src/equipment/router.py | 64 +++++++++++++ src/modules/equipment/Prediksi.py | 101 +++++++++++++++++++- src/modules/equipment/insert_actual_data.py | 4 +- 4 files changed, 163 insertions(+), 7 deletions(-) diff --git a/src/equipment/model.py b/src/equipment/model.py index 454c16c..8152bde 100644 --- a/src/equipment/model.py +++ b/src/equipment/model.py @@ -60,6 +60,7 @@ class EquipmentTransactionRecords(Base, DefaultMixin, IdentityMixin): tahun = Column(Integer, nullable=False) seq = Column(Integer, nullable=False) is_actual = Column(Integer, nullable=False) + acquisition_year_ref = Column(Integer, nullable=True) raw_cm_interval = Column(Float, nullable=False) raw_cm_material_cost = Column(Float, nullable=False) raw_cm_labor_time = Column(Float, nullable=False) diff --git a/src/equipment/router.py b/src/equipment/router.py index 23c7aa0..655ca3f 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -38,6 +38,7 @@ from src.database.service import CommonParameters, search_filter_sort_paginate from src.database.core import DbSession, CollectorDbSession from src.auth.service import CurrentUser, Token from src.models import CommonParams, StandardResponse +from src.modules.config import get_production_connection router = APIRouter() @@ -118,6 +119,66 @@ async def simulate_equipment(db_session: DbSession, assetnum: str): return StreamingResponse(event_generator(), media_type='text/event-stream') +async def update_initial_simulation_data(db_session: DbSession, assetnum: str): + """ + Update acquisition_year from wo_maximo and set default forecasting_target_year + before running simulation. + """ + conn = get_production_connection() + first_year = None + if conn: + try: + cursor = conn.cursor() + # Query the first year from wo_maximo + query = """ + select DATE_PART('year', a.reportdate) AS year + from wo_maximo a + where a.asset_replacecost > 0 + and a.asset_assetnum = %s + order by a.reportdate asc + limit 1; + """ + cursor.execute(query, (assetnum,)) + result = cursor.fetchone() + if result: + first_year = int(result[0]) + cursor.close() + conn.close() + except Exception as e: + print(f"Error fetching acquisition year for {assetnum}: {e}") + if conn: + try: + conn.close() + except: + pass + + if first_year: + # Fetch equipment to update + eq = await get_by_assetnum(db_session=db_session, assetnum=assetnum) + if eq: + # Check if forecasting_target_year matches the "default" logic (acquisition + design_life) + # using the OLD acquisition year. + current_acq = eq.acquisition_year + current_life = eq.design_life + current_target = eq.forecasting_target_year + + # If current_target is logically "default", we update it. + # If user changed it to something else, we might want to preserve it? + # User request: "must be default acquisition_year+design_life if not changes" + # We interpret "if not changes" as: if it currently holds the default value (based on old acq year). + is_valid_default = current_target == (current_acq + current_life) + + # Apply updates + if eq.acquisition_year != first_year: + eq.acquisition_year = first_year + + if is_valid_default: + eq.forecasting_target_year = first_year + current_life + + await db_session.commit() + # await db_session.refresh(eq) + + @router.get("/simulate-all") async def simulate_all_equipment(db_session: DbSession): """Run simulation (prediksi + EAC) for ALL equipment. @@ -142,6 +203,9 @@ async def simulate_all_equipment(db_session: DbSession): yield f"data: {json.dumps({'status':'working', 'step':f'Processing {idx}/{total}', 'assetnum': assetnum})}\\n\\n" try: + # Update acquisition year and target year + await update_initial_simulation_data(db_session=db_session, assetnum=assetnum) + # Prediksi await prediksi_main(assetnum=assetnum) # EAC diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index a7724d9..e897268 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -151,7 +151,7 @@ class Prediksi: # connection.close() # Fungsi untuk menyimpan data proyeksi ke database - async def __insert_predictions_to_db(self, data, equipment_id, token): + async def __insert_predictions_to_db(self, data, equipment_id, token, acquisition_year_ref=None): try: connection, connection_wo_db = get_connection() if connection is None: @@ -173,6 +173,7 @@ class Prediksi: id, seq, is_actual, + acquisition_year_ref, tahun, assetnum, rc_cm_material_cost, rc_cm_labor_cost, @@ -184,7 +185,7 @@ class Prediksi: rc_predictive_labor_cost, created_by, created_at ) VALUES ( - %s, %s, 0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'Sys', NOW() + %s, %s, 0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'Sys', NOW() ) """ @@ -235,7 +236,7 @@ class Prediksi: check_existence_query = """ SELECT id FROM lcc_equipment_tr_data - WHERE assetnum = %s AND tahun = %s AND is_actual = 0 + WHERE assetnum = %s AND tahun = %s AND is_actual = 0 AND (acquisition_year_ref = %s OR (acquisition_year_ref IS NULL AND %s IS NULL)) """ update_query = """ @@ -256,7 +257,7 @@ class Prediksi: for _, row in data.iterrows(): # Check if data exists - cursor.execute(check_existence_query, (equipment_id, int(row["year"]))) + cursor.execute(check_existence_query, (equipment_id, int(row["year"]), acquisition_year_ref, acquisition_year_ref)) existing_record = cursor.fetchone() if existing_record: @@ -280,6 +281,7 @@ class Prediksi: ( str(uuid4()), # id int(max_seq), # seq + int(acquisition_year_ref) if acquisition_year_ref is not None else None, # acquisition_year_ref int(row["year"]), equipment_id, float(row.get("rc_cm_material_cost", 0)) if not pd.isna(row.get("rc_cm_material_cost", 0)) else 0.0, @@ -743,8 +745,97 @@ class Prediksi: if connection: connection.close() + async def __update_equipment_acquisition_year(self, assetnum: str): + """ + Update acquisition_year from wo_maximo and set default forecasting_target_year + using raw SQL connections. + """ + try: + # 1. Fetch first acquisition year from wo_maximo (production db) + prod_conn = get_production_connection() + if not prod_conn: + print("Failed to connect to production DB for acquisition year update.") + return None + + first_year = None + try: + p_cursor = prod_conn.cursor() + query = """ + select DATE_PART('year', a.reportdate) AS year + from wo_maximo a + where a.asset_replacecost > 0 + and a.asset_assetnum = %s + order by a.reportdate asc + limit 1; + """ + p_cursor.execute(query, (assetnum,)) + res = p_cursor.fetchone() + if res and res[0] is not None: + first_year = int(res[0]) + p_cursor.close() + finally: + prod_conn.close() + + if not first_year: + # No data, skip update + return None + + # 2. Update local DB + conn, _ = get_connection() + if not conn: + return None + + updated_acq = None + try: + cursor = conn.cursor(cursor_factory=DictCursor) + + # Fetch current values + cursor.execute("SELECT acquisition_year, design_life, forecasting_target_year FROM lcc_ms_equipment_data WHERE assetnum = %s", (assetnum,)) + current = cursor.fetchone() + + if current: + curr_acq = int(current["acquisition_year"]) + curr_life = int(current["design_life"]) + curr_target = int(current["forecasting_target_year"]) + + # Logic: if current_target matches the "old default" (old_acq + life), then update it too. + is_valid_default = (curr_target == (curr_acq + curr_life)) + + if curr_acq != first_year: + new_target = curr_target + if is_valid_default: + new_target = first_year + curr_life + + update_q = """ + UPDATE lcc_ms_equipment_data + SET acquisition_year = %s, forecasting_target_year = %s + WHERE assetnum = %s + """ + cursor.execute(update_q, (first_year, new_target, assetnum)) + conn.commit() + print(f"Updated acquisition_year for {assetnum}: {curr_acq} -> {first_year}") + updated_acq = first_year + else: + updated_acq = curr_acq + else: + # Logic if equipment not found? Unlikely here. + pass + + cursor.close() + finally: + conn.close() + + return updated_acq + + except Exception as e: + print(f"Error updating acquisition year for {assetnum}: {e}") + return None + async def predict_equipment_data(self, assetnum, token): try: + # Update acquisition year first + acquisition_year_ref = await self.__update_equipment_acquisition_year(assetnum) + # Mengambil data dari database df = self.__fetch_data_from_db(assetnum) if df is None: @@ -999,7 +1090,7 @@ class Prediksi: # Insert hasil prediksi ke database try: await self.__insert_predictions_to_db( - predictions_df, assetnum, token + predictions_df, assetnum, token, acquisition_year_ref ) except Exception as e: print(f"Error saat insert data ke database: {e}") diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index f7ad447..0b3ecc5 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -966,8 +966,8 @@ async def query_data(target_assetnum: str = None): if not assetnum or str(assetnum).strip() == "": print(f"[{idx}/{total_assets}] Skipping empty assetnum") continue - # forecasting_start_year = row["forecasting_start_year"] - 1 - forecasting_start_year = 2014 + forecasting_start_year = row["forecasting_start_year"] - 1 if row["forecasting_start_year"] else 2014 + # forecasting_start_year = 2014 asset_start = datetime.now() processed_assets += 1