feat: Add equipment remaining life count API, refine actual data insertion logging, and enhance prediction data fetching.

main
MrWaradana 5 days ago
parent bd6efc2a3a
commit 04bba31670

@ -12,6 +12,7 @@ from src.equipment.schema import (
EquipmentTop10, EquipmentTop10,
EquipmentTop10Pagination, EquipmentTop10Pagination,
EquipmentUpdate, EquipmentUpdate,
CountRemainingLifeResponse,
) )
from src.equipment.service import ( from src.equipment.service import (
get_master_by_assetnum, get_master_by_assetnum,
@ -20,6 +21,7 @@ from src.equipment.service import (
get_all, get_all,
create, create,
get_top_10_replacement_priorities, get_top_10_replacement_priorities,
get_count_remaining_life,
update, update,
delete, delete,
generate_all_transaction, generate_all_transaction,
@ -113,6 +115,16 @@ async def simulate_equipment(db_session: DbSession, assetnum: str):
return StreamingResponse(event_generator(), media_type='text/event-stream') return StreamingResponse(event_generator(), media_type='text/event-stream')
@router.get(
"/count-remaining-life",
response_model=StandardResponse[CountRemainingLifeResponse],
)
async def get_count_remaining_life(db_session: DbSession, common: CommonParameters):
count = await get_count_remaining_life(db_session=db_session, common=common)
return StandardResponse(
data=count,
message="Count remaining life retrieved successfully",
)
@router.get( @router.get(
"/top-10-replacement-priorities", "/top-10-replacement-priorities",

@ -145,3 +145,8 @@ class EquipmentDataMaster(EquipmentBase):
class EquipmentPagination(Pagination): class EquipmentPagination(Pagination):
items: List[EquipmentDataMaster] = [] items: List[EquipmentDataMaster] = []
class CountRemainingLifeResponse(DefaultBase):
safe: int
warning: int
critical: int

@ -411,6 +411,39 @@ async def get_top_10_economic_life(*, db_session: DbSession, common) -> list[Equ
result = await search_filter_sort_paginate(model=query, **common) result = await search_filter_sort_paginate(model=query, **common)
return result return result
async def get_count_remaining_life(*, db_session: DbSession, common) -> dict[str, int]:
"""Count remaining life based on the category"""
current_year = datetime.datetime.now().year
remaining_life = case(
(
(Equipment.minimum_eac_year - current_year) >= 0,
(Equipment.minimum_eac_year - current_year),
),
else_=0,
)
query = (
Select(
func.sum(case((remaining_life <= 1, 1), else_=0)).label("red"),
func.sum(case(((remaining_life > 1) & (remaining_life < 5), 1), else_=0)).label("orange"),
func.sum(case((remaining_life >= 5, 1), else_=0)).label("green"),
)
.select_from(Equipment)
.join(EquipmentMaster, Equipment.assetnum == EquipmentMaster.assetnum)
.filter(Equipment.minimum_eac_year != None)
.filter((Equipment.minimum_eac != None) & (Equipment.minimum_eac != 0))
)
result = await db_session.execute(query)
row = result.mappings().one()
return {
"safe": int(row["red"] or 0),
"warning": int(row["orange"] or 0),
"critical": int(row["green"] or 0),
}
async def get_top_10_replacement_priorities(*, db_session: DbSession, common) -> list[Equipment]: async def get_top_10_replacement_priorities(*, db_session: DbSession, common) -> list[Equipment]:
"""Returns top 10 replacement priorities.""" """Returns top 10 replacement priorities."""
query = ( query = (

@ -352,7 +352,7 @@ def main(assetnum=None):
print("Skipping None assetnum") print("Skipping None assetnum")
continue continue
print(f"Processing asset: {row_assetnum}") print(f"EAC Calculation asset: {row_assetnum}")
eac.hitung_eac_equipment(row_assetnum) eac.hitung_eac_equipment(row_assetnum)
processed.append(row_assetnum) processed.append(row_assetnum)

@ -126,35 +126,35 @@ class Prediksi:
return fv_values return fv_values
# Fungsi untuk menghapus data proyeksi pada tahun tertentu # Fungsi untuk menghapus data proyeksi pada tahun tertentu
def __delete_predictions_from_db(self, equipment_id): # def __delete_predictions_from_db(self, equipment_id):
try: # try:
connections = get_connection() # connections = get_connection()
connection = ( # connection = (
connections[0] if isinstance(connections, tuple) else connections # connections[0] if isinstance(connections, tuple) else connections
) # )
if connection is None: # if connection is None:
print("Database connection failed.") # print("Database connection failed.")
return None # return None
cursor = connection.cursor() # cursor = connection.cursor()
# Query untuk menghapus data berdasarkan tahun proyeksi # # Query untuk menghapus data berdasarkan tahun proyeksi
delete_query = """ # delete_query = """
DELETE FROM lcc_equipment_tr_data # DELETE FROM lcc_equipment_tr_data
WHERE assetnum = %s AND is_actual = 0; # WHERE assetnum = %s AND is_actual = 0;
""" # Asumsikan kolom is_actual digunakan untuk membedakan data proyeksi dan data aktual # """ # Asumsikan kolom is_actual digunakan untuk membedakan data proyeksi dan data aktual
# Eksekusi query delete # # Eksekusi query delete
cursor.execute(delete_query, (equipment_id,)) # cursor.execute(delete_query, (equipment_id,))
connection.commit() # connection.commit()
# print(f"Data proyeksi untuk tahun {equipment_id} berhasil dihapus.") # # print(f"Data proyeksi untuk tahun {equipment_id} berhasil dihapus.")
except Exception as e: # except Exception as e:
print(f"Error saat menghapus data proyeksi dari database: {e}") # print(f"Error saat menghapus data proyeksi dari database: {e}")
finally: # finally:
if connection: # if connection:
connection.close() # connection.close()
# Fungsi untuk menyimpan data proyeksi ke database # 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):
@ -707,6 +707,75 @@ class Prediksi:
# ====================================================================================================================================================== # ======================================================================================================================================================
async def _fetch_api_data(self, assetnum: str, year: int) -> dict:
url = self.RELIABILITY_APP_URL
endpoint = f"{url}/main/number-of-failures/{assetnum}/{int(year)}/{int(year)}"
async with httpx.AsyncClient() as client:
try:
current_token = getattr(self, "access_token", None)
response = await client.get(
endpoint,
timeout=30.0,
headers={"Authorization": f"Bearer {current_token}"} if current_token else {},
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
status = getattr(e.response, "status_code", None)
# If we get a 401 or 403, try to refresh the access token and retry once
if status in (401, 403):
print(f"Received {status} from reliability API, attempting to refresh/re-login...")
# Try refreshing token first
new_access = await self.refresh_access_token()
# If refresh failed (e.g. refresh token expired), try full sign-in
if not new_access:
print("Refresh failed, attempting full sign-in...")
await self.sign_in()
new_access = getattr(self, "access_token", None)
if new_access:
try:
response = await client.get(
endpoint,
timeout=30.0,
headers={"Authorization": f"Bearer {new_access}"},
)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e2:
print(f"HTTP error occurred after retry: {e2}")
return {}
print(f"HTTP error occurred: {e}")
return {}
except httpx.HTTPError as e:
print(f"HTTP error occurred: {e}")
return {}
def __get_man_hour_rate(self, staff_level: str = "junior"):
connection = None
try:
connections = get_connection()
connection = (
connections[0] if isinstance(connections, tuple) else connections
)
if connection is None:
return 0.0
cursor = connection.cursor()
# Takes from salary_per_hour_idr on specific staff_job_level
query = "SELECT salary_per_hour_idr FROM lcc_manpower_cost WHERE staff_job_level = %s LIMIT 1"
cursor.execute(query, (staff_level,))
result = cursor.fetchone()
if result:
return float(result[0])
return 0.0
except Exception as e:
print(f"Error getting man hour rate for {staff_level}: {e}")
return 0.0
finally:
if connection:
connection.close()
async def predict_equipment_data(self, assetnum, token): async def predict_equipment_data(self, assetnum, token):
try: try:
# Mengambil data dari database # Mengambil data dari database
@ -759,6 +828,7 @@ class Prediksi:
# Mendapatkan rate dan tahun maksimal # Mendapatkan rate dan tahun maksimal
rate, max_year = self.__get_rate_and_max_year(assetnum) rate, max_year = self.__get_rate_and_max_year(assetnum)
man_hour_rate = self.__get_man_hour_rate() # Defaults to 'junior'
pmt = 0 pmt = 0
# Prediksi untuk setiap kolom # Prediksi untuk setiap kolom
@ -788,13 +858,33 @@ class Prediksi:
recent_vals = df[column].dropna() recent_vals = df[column].dropna()
# Jika masih kosong, pakai default (interval minimal 1, lainnya 0) # Jika masih kosong, pakai default (interval minimal 1, lainnya 0)
if recent_vals.empty: if "labor" in col_lower:
preds_list = []
for yr in future_years:
failures_data = await self._fetch_api_data(assetnum, yr)
# Interval from number of failures
interval = 0.0
if isinstance(failures_data, dict):
val = failures_data.get("data")
if val is not None:
try:
interval = float(val)
except Exception:
interval = 0.0
# interval * labor_time(3) * labor_human(1) * man_hour_rate
cost = rc_labor_cost(interval, 3.0, 1.0, man_hour_rate)
preds_list.append(cost)
preds = np.array(preds_list, dtype=float)
elif recent_vals.empty:
avg = 0.0 avg = 0.0
preds = np.repeat(float(avg), n_future)
else: else:
avg = pd.to_numeric(recent_vals, errors="coerce").fillna(0).mean() avg = pd.to_numeric(recent_vals, errors="coerce").fillna(0).mean()
avg = 0.0 if pd.isna(avg) else float(avg) avg = 0.0 if pd.isna(avg) else float(avg)
preds = np.repeat(float(avg), n_future) preds = np.repeat(float(avg), n_future)
# print(preds) # print(preds)
else: else:
# Untuk kolom non-cm, gunakan nilai dari last actual year bila ada, # Untuk kolom non-cm, gunakan nilai dari last actual year bila ada,
@ -957,22 +1047,22 @@ async def main(RELIABILITY_APP_URL=RELIABILITY_APP_URL, assetnum=None, token=Non
prediksi = Prediksi(RELIABILITY_APP_URL) prediksi = Prediksi(RELIABILITY_APP_URL)
# If token not provided, sign in to obtain access_token/refresh_token # If token not provided, sign in to obtain access_token/refresh_token
# if token is None: if token is None:
# signin_res = await prediksi.sign_in() signin_res = await prediksi.sign_in()
# if not getattr(prediksi, "access_token", None): if not getattr(prediksi, "access_token", None):
# print("Failed to obtain access token; aborting.") print("Failed to obtain access token; aborting.")
# return return
# else: else:
# # Use provided token as access token # Use provided token as access token
# prediksi.access_token = token prediksi.access_token = token
# If an assetnum was provided, run only for that assetnum # If an assetnum was provided, run only for that assetnum
if assetnum: if assetnum:
print(f"Processing single assetnum: {assetnum}") print(f"Predicting single assetnum: {assetnum}")
try: try:
await prediksi.predict_equipment_data(assetnum, prediksi.access_token) await prediksi.predict_equipment_data(assetnum, prediksi.access_token)
except Exception as e: except Exception as e:
print(f"Error processing {assetnum}: {e}") print(f"Error Predicting {assetnum}: {e}")
print("Selesai.") print("Selesai.")
return return
@ -993,11 +1083,11 @@ async def main(RELIABILITY_APP_URL=RELIABILITY_APP_URL, assetnum=None, token=Non
if not current_asset or str(current_asset).strip() == "": if not current_asset or str(current_asset).strip() == "":
print(f"[{idx}/{len(results)}] Skipping empty assetnum") print(f"[{idx}/{len(results)}] Skipping empty assetnum")
continue continue
print(f"[{idx}/{len(results)}] Processing assetnum: {current_asset}") print(f"[{idx}/{len(results)}] Predicting assetnum: {current_asset}")
try: try:
await prediksi.predict_equipment_data(current_asset, prediksi.access_token) await prediksi.predict_equipment_data(current_asset, prediksi.access_token)
except Exception as e: except Exception as e:
print(f"Error processing {current_asset}: {e}") print(f"Error Predicting {current_asset}: {e}")
print("Selesai.") print("Selesai.")
except Exception as e: except Exception as e:

@ -954,6 +954,7 @@ async def query_data(target_assetnum: str = None):
total_assets = len(results) total_assets = len(results)
processed_assets = 0 processed_assets = 0
total_inserted = 0 total_inserted = 0
total_updated = 0
overall_start = datetime.now() overall_start = datetime.now()
print(f"Starting processing {total_assets} assets at {overall_start.isoformat()}") print(f"Starting processing {total_assets} assets at {overall_start.isoformat()}")
@ -970,6 +971,7 @@ async def query_data(target_assetnum: str = None):
processed_assets += 1 processed_assets += 1
years_processed = 0 years_processed = 0
inserted_this_asset = 0 inserted_this_asset = 0
updated_this_asset = 0
# CM # CM
data_cm = get_recursive_query(cursor_wo, assetnum, worktype="CM") data_cm = get_recursive_query(cursor_wo, assetnum, worktype="CM")
@ -1108,8 +1110,8 @@ async def query_data(target_assetnum: str = None):
year, year,
), ),
) )
inserted_this_asset += 1 updated_this_asset += 1
total_inserted += 1 total_updated += 1
seq = seq + 1 seq = seq + 1
# commit per asset to persist progress and free transaction # commit per asset to persist progress and free transaction
@ -1127,16 +1129,16 @@ async def query_data(target_assetnum: str = None):
# progress per asset # progress per asset
print( print(
f"[{idx}/{total_assets}] Asset {assetnum} processed. " f"[{idx}/{total_assets}] Asset {assetnum} processed. "
f"Inserted this asset: {inserted_this_asset}. " f"Inserted: {inserted_this_asset}, Updated: {updated_this_asset}. "
f"Asset time: {asset_elapsed.total_seconds():.2f}s. " f"Asset time: {asset_elapsed.total_seconds():.2f}s. "
f"Total inserted: {total_inserted}. " f"Total Ins: {total_inserted}, Upd: {total_updated}. "
f"Overall elapsed: {total_elapsed.total_seconds():.2f}s. " f"Overall elapsed: {total_elapsed.total_seconds():.2f}s. "
f"Progress: {pct_assets:.1f}%" f"Progress: {pct_assets:.1f}%"
) )
# periodic summary every 10 assets # periodic summary every 10 assets
if idx % 10 == 0 or idx == total_assets: if idx % 10 == 0 or idx == total_assets:
print(f"SUMMARY: {idx}/{total_assets} assets processed, {total_inserted} rows inserted, elapsed {total_elapsed.total_seconds():.1f}s") print(f"SUMMARY: {idx}/{total_assets} assets processed, {total_inserted} inserted, {total_updated} updated, elapsed {total_elapsed.total_seconds():.1f}s")
# Commit perubahan # Commit perubahan
try: try:
@ -1147,7 +1149,7 @@ async def query_data(target_assetnum: str = None):
except Exception: except Exception:
pass pass
print(f"Finished processing all assets. Total assets: {total_assets}, total inserted: {total_inserted}, total time: {(datetime.now()-overall_start).total_seconds():.2f}s") print(f"Finished processing all assets. Total assets: {total_assets}, Total Inserted: {total_inserted}, Total Updated: {total_updated}, Total time: {(datetime.now()-overall_start).total_seconds():.2f}s")
except Exception as e: except Exception as e:
print("Error saat menjalankan query:", e) print("Error saat menjalankan query:", e)

Loading…
Cancel
Save