From 04bba31670e061ea01b7fd832b13349a915e1063 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 6 Jan 2026 19:09:25 +0700 Subject: [PATCH] feat: Add equipment remaining life count API, refine actual data insertion logging, and enhance prediction data fetching. --- src/equipment/router.py | 12 ++ src/equipment/schema.py | 5 + src/equipment/service.py | 33 ++++ src/modules/equipment/Eac.py | 2 +- src/modules/equipment/Prediksi.py | 178 +++++++++++++----- .../equipment/__pycache__/Eac.cpython-311.pyc | Bin 15388 -> 15393 bytes .../__pycache__/Prediksi.cpython-311.pyc | Bin 43567 -> 48359 bytes .../insert_actual_data.cpython-311.pyc | Bin 51445 -> 51493 bytes src/modules/equipment/insert_actual_data.py | 14 +- 9 files changed, 193 insertions(+), 51 deletions(-) diff --git a/src/equipment/router.py b/src/equipment/router.py index 6695d09..5c06862 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -12,6 +12,7 @@ from src.equipment.schema import ( EquipmentTop10, EquipmentTop10Pagination, EquipmentUpdate, + CountRemainingLifeResponse, ) from src.equipment.service import ( get_master_by_assetnum, @@ -20,6 +21,7 @@ from src.equipment.service import ( get_all, create, get_top_10_replacement_priorities, + get_count_remaining_life, update, delete, 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') +@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( "/top-10-replacement-priorities", diff --git a/src/equipment/schema.py b/src/equipment/schema.py index 55682b1..d6a6a30 100644 --- a/src/equipment/schema.py +++ b/src/equipment/schema.py @@ -145,3 +145,8 @@ class EquipmentDataMaster(EquipmentBase): class EquipmentPagination(Pagination): items: List[EquipmentDataMaster] = [] + +class CountRemainingLifeResponse(DefaultBase): + safe: int + warning: int + critical: int diff --git a/src/equipment/service.py b/src/equipment/service.py index 05d9700..1bc7972 100644 --- a/src/equipment/service.py +++ b/src/equipment/service.py @@ -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) 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]: """Returns top 10 replacement priorities.""" query = ( diff --git a/src/modules/equipment/Eac.py b/src/modules/equipment/Eac.py index e91a793..5d72e61 100644 --- a/src/modules/equipment/Eac.py +++ b/src/modules/equipment/Eac.py @@ -352,7 +352,7 @@ def main(assetnum=None): print("Skipping None assetnum") continue - print(f"Processing asset: {row_assetnum}") + print(f"EAC Calculation asset: {row_assetnum}") eac.hitung_eac_equipment(row_assetnum) processed.append(row_assetnum) diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index 206cdab..c4fd0a7 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -126,35 +126,35 @@ class Prediksi: return fv_values # Fungsi untuk menghapus data proyeksi pada tahun tertentu - def __delete_predictions_from_db(self, equipment_id): - try: - connections = get_connection() - connection = ( - connections[0] if isinstance(connections, tuple) else connections - ) - if connection is None: - print("Database connection failed.") - return None - - cursor = connection.cursor() - - # Query untuk menghapus data berdasarkan tahun proyeksi - delete_query = """ - DELETE FROM lcc_equipment_tr_data - WHERE assetnum = %s AND is_actual = 0; - """ # Asumsikan kolom is_actual digunakan untuk membedakan data proyeksi dan data aktual - - # Eksekusi query delete - cursor.execute(delete_query, (equipment_id,)) - connection.commit() - # print(f"Data proyeksi untuk tahun {equipment_id} berhasil dihapus.") - - except Exception as e: - print(f"Error saat menghapus data proyeksi dari database: {e}") - - finally: - if connection: - connection.close() + # def __delete_predictions_from_db(self, equipment_id): + # try: + # connections = get_connection() + # connection = ( + # connections[0] if isinstance(connections, tuple) else connections + # ) + # if connection is None: + # print("Database connection failed.") + # return None + + # cursor = connection.cursor() + + # # Query untuk menghapus data berdasarkan tahun proyeksi + # delete_query = """ + # DELETE FROM lcc_equipment_tr_data + # WHERE assetnum = %s AND is_actual = 0; + # """ # Asumsikan kolom is_actual digunakan untuk membedakan data proyeksi dan data aktual + + # # Eksekusi query delete + # cursor.execute(delete_query, (equipment_id,)) + # connection.commit() + # # print(f"Data proyeksi untuk tahun {equipment_id} berhasil dihapus.") + + # except Exception as e: + # print(f"Error saat menghapus data proyeksi dari database: {e}") + + # finally: + # if connection: + # connection.close() # Fungsi untuk menyimpan data proyeksi ke database 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): try: # Mengambil data dari database @@ -759,6 +828,7 @@ class Prediksi: # Mendapatkan rate dan tahun maksimal rate, max_year = self.__get_rate_and_max_year(assetnum) + man_hour_rate = self.__get_man_hour_rate() # Defaults to 'junior' pmt = 0 # Prediksi untuk setiap kolom @@ -788,13 +858,33 @@ class Prediksi: recent_vals = df[column].dropna() # 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 + preds = np.repeat(float(avg), n_future) else: avg = pd.to_numeric(recent_vals, errors="coerce").fillna(0).mean() 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) else: # 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) # If token not provided, sign in to obtain access_token/refresh_token - # if token is None: - # signin_res = await prediksi.sign_in() - # if not getattr(prediksi, "access_token", None): - # print("Failed to obtain access token; aborting.") - # return - # else: - # # Use provided token as access token - # prediksi.access_token = token + if token is None: + signin_res = await prediksi.sign_in() + if not getattr(prediksi, "access_token", None): + print("Failed to obtain access token; aborting.") + return + else: + # Use provided token as access token + prediksi.access_token = token # If an assetnum was provided, run only for that assetnum if assetnum: - print(f"Processing single assetnum: {assetnum}") + print(f"Predicting single assetnum: {assetnum}") try: await prediksi.predict_equipment_data(assetnum, prediksi.access_token) except Exception as e: - print(f"Error processing {assetnum}: {e}") + print(f"Error Predicting {assetnum}: {e}") print("Selesai.") 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() == "": print(f"[{idx}/{len(results)}] Skipping empty assetnum") continue - print(f"[{idx}/{len(results)}] Processing assetnum: {current_asset}") + print(f"[{idx}/{len(results)}] Predicting assetnum: {current_asset}") try: await prediksi.predict_equipment_data(current_asset, prediksi.access_token) except Exception as e: - print(f"Error processing {current_asset}: {e}") + print(f"Error Predicting {current_asset}: {e}") print("Selesai.") except Exception as e: diff --git a/src/modules/equipment/__pycache__/Eac.cpython-311.pyc b/src/modules/equipment/__pycache__/Eac.cpython-311.pyc index 08b9fa69e2549a7194b95f8f4fcb114f24fa2fa8..88ac350a4bbe2d08fcaef8ce6651741472f81025 100644 GIT binary patch delta 68 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 NnVT!P$a`<@{eP?cnH2y4 delta 4646 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 diff --git a/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc b/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc index 1dee995c90eacecc3a27a82b9c30c6cde72989a0..29b657d9ad927d18124683ff591cb5f235d27722 100644 GIT binary patch delta 3137 zcma)8Yj6|S71q_umSj1`fQ;XYWMNDAfpCl64q%(uh7QI+8(y~Y&PKb!0$D4cT>&O0 zhCoP|P9Nk>lctm=Oq&v#CNT(R7#c{E2J$RPOFAUmmbTNRGnwfOnQ8N*PH211m9a^N zA871v&z}3;v*(`I?x%-sPd{q&9`}0P7W^&#&OoB!u(y<@57+J=AD+`UhglStI#+S4 zKE;FA0{r)?C2_Y>c);E_PpMIg4zRvbW!3>pUzuPQPcfXzY-E%xA!ROVd_rrFJC%|H zj=ly(Zj`tufV?((9tCzxdSVgg*|wFUv7^G#aFizQV#r7EU9HiSV>C=w{8 zo#K#=OggOstRpLK7{H5ZRpQXb2Seey4Mgi8T^`6gu|Bo}`J_{i;)$nz5acg}f^0l{ zCB*Wp$utIvc8O?-)Xc<)|EmG4O%{G_)8fh#tQSS`D2T%>RN}h*?XH^4s>*|b;RkSnx16+Fq^->?0pvFC)>(cGk?47NSWw82N|7dBwJF&7UKKb zOGQG={J3hwhP0FlIuV@+rZc7u4<`)F+l%er^@ym7#tHEC9rLkW#yYCl)$Eauh>!h- zU)s2wo#U=e=e>eMVLQQK0l%_o`TU2-q>3;VZIi)ttZ9krbN5&I-!=yLmd$N=prf00 z<`i}cxvziWO|hpxga?CuWt>V3@(3;xv=F?BU^-$+O;=4 zO7JRym}ZSAnGTRsTXBk{)LjO&lE$7)=!WSqGU=q6%k<6IR9N73VmM{2;FJPy;7!;M zZ{Y>r<|6}x&MPGTmj7X(eZgfC-$5`7Vi^E6Wk{o{47uX=$hwX*5Jwmo;N|UGmasy; zXG@^y5!!W#Kom-Vzp$m%DdtOn0%e68*;#|ha`pj#VAN#ivYU6WXXP&vU&M`i1O{QzTpwQJ=MAKu?A<1_P@-Z7+b}<`A1`&UP@Xnb9RXH#e3>K z|V&D`zrXMJ?nD=dzgQ?=N0xxe*6J7m!7{9Q)Ff)Q6G}-HuVT8TF?k~Aeebd z0?2tYUy|a9q$)`;PKz0WO9azn1V-uhV+3qTg@%r{VrNXtqzsXkT%sHaLmk#(gjlXl zeBpEhS$7&5jVDn$;fksJkvZYw75@A2MmE6N-mRYZNSkW`uJ7$&ZT!sMRfPf4x)_d* z3Y7S)=5kv*j->j10emg4+2``l`G6QE!8ruE$8NE>`SY9Gv%fs{RzCX^-}Q?Xy)#)8 z1NQ>4XA``Hkhq4?gAm>Rnczu+V+2nj;Pe$sw`jTd*{7PmLIkSqUU(4kge^Da{&G`2 zH5B)X*DxI9Mb8EJf1lle!++^>KdG$46N!BgrnA{_m*0O2x7HaU#+{J^p6h4;?s&&v zpC7$LHv+t6Vk2_DH!&sp}O~OtKLCn)jSq zh7;@Cr}lrjP;Xb6GsP>6i>V>XSd^id2)*>EMuI3o7r{pa{~!<(=PXH10va5-%6!vO zgic2wM8TYz9Y?cO)xwKUi1rzQkCrc!L_-Ar&CTpRU#Q2r(-rJf-gR23eufDDA^0i5 z&j_9-_&LFG{`%>vI;vUIeama)b6UGDwCR{3iGid;A-;c_i-SU)`%a|qH0a2F>&#@3rG~PEhFh`1T_R-iZXpWi0_=-co0#(O1c|j2Su&}fyM}p6C5L* xbZZ0r*!-gVlle=AH6@c&m%&<=Z?PcFq95YhE}ihLd)IQCzijZe%gjy({Xgq)EVTdt delta 3168 zcmb7GdvH|c72j`PWOuWW#$=O_*CyFb*pQ?#86gUWL=v$=9=0Ny0Cz5XZ;~aOdpGCa zB@fBVL}12gr}XQft=0M{WoSDESr~^pDz&1eRi+Q>(A$nqZ97xOR_jz}m=xQd^KDEZ z?H_h$_jk{C&iP*F@w` zd}iX$uLktFXY9*}Xq9!1;&OW!2H|Odp zRPx0*r_ZSrpjVNydSRARDa3KH$L6sql}gdv7S>au6yxHhN(o|6DMc((s+6Fb(`Q%8 zaItc=LMb1#)6t<=sX%+kZc}Z0!=#}PjzYX;LlOTfdpN+XoBCB#8W~VQm4=flj7i3D zJhjdKS9?|_I)k5JCBZ6wm+L`Rk$TnjcPGo_XS_9q<)}|D0BSS|Mr_~0B2b&U=51n) zRWxx8Lc|Ucl4}qQI$SUaV)jbfUXe{%GUb7!2KBr@^EP)K`BtZnWghZ0_()z{$nvz0 zM%9EF(={kYi{*_OG0iY#Evi~hB^EUypUZ(lR=_6$1-W8Vagl0TW-UPt|9)UciP$|r zyT|1{dJ?3lZkUp(n{wQMHXh6i`NB^PoXq%T_N|w)Z%uXO$xg4uf|hNb)j&J{QQ>`? zHqdHxh=>F3BzX&hWBy?+R(_ z_o55Xkzs&f8_i(i-Vh@Nog4;9>r8tH?I8asRLzQcX*iFq7vJ5unmwNyYxKDAz_U%m zZVy%aJnMyQ{z>!YqCGgeLgBhXw;lI(gWeCSVcZsk2L9ou$G3|PaiQ(H9&^Oiba%lD zG*}KzAF({DmK;(+HdS$HE2B>}qXW8zxueN$a&$-ap`n-w>-Zm9zT#^kz39C%KHgf% z%K6i+CG4%#xz_3cdxgKZ;|_L~*WLf3U-(d*k29#|SMR^0Y>uYVffv$aF<49vnI7i7 z5A=D6CFDki;rlQvH}%c~_c6Cv%Fj3S)_*Dv_lDUX{%Eg~OPwk%1h0~`j^F~%+8LNy z7K7KwLaYTpArKT;i<0F6S?fod(bQ2B)|18=j~S-rGLwmTIxb7mfY-^vEn|6W3j7oY zks$mGAMkTBvyP7cO{@rWXwbZ)Z@j`Z)KRafzv#H7P?X2(x z@)Y5sj>0?1m|fA?FY@;vZ1ydh;9q?G{xA4Mzb|z2Bl~OJR9JA1zqEfVYvymz9;O%x0L{L2&j(u5x8p2=63#ow4zZx_jUmmEYMsLs-9`6Efo5+ZUjLQdC$2NzHuDOyTa`aF~&Y;Zj)i=;WH%UCb3obLDi8VzZ_$>fhsx3Xq#O>cFVkv4;` zoe5(W?wKjaSEo7S3HmOR{Ecq09Y$O{hAtxCfl-c+}q62>M|gEp)ss3&M6_!GgO2?WPHPZBYwsLFJGUa;ZBNHv5guuF82AJsKY6?7pj zF^4pwr)%>AT4bIem*%}m5-|w;ga13V`GyMZI9PTaFpP? z{BNgAYRgcxyx2n0J~^IL4frS7-WS?L)RY7p86w1sFQ%0_Eux+*Z~7mlc-gh2M$a5s zRVZSs&>D3FL=l#ANRDZ+nMcl7*OM0iQH0Wuc6!)5J7JE5N^6B|jA_xBZn+b(rpVaA zeD?e{_Yhhkz<+bTX-b5?fW}P(6$HzY&`mn}uLF|=A_;Gibd8{b;@5&;Wk?c{q9nm) w8ae>~p5A!!`HYQ2NaAs|8M@E~zYd!*%fJ5G@vQn^*lzfyzV!M<=A_&G2eisGK>z>% diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index de5314f..1767020 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -954,6 +954,7 @@ async def query_data(target_assetnum: str = None): total_assets = len(results) processed_assets = 0 total_inserted = 0 + total_updated = 0 overall_start = datetime.now() 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 years_processed = 0 inserted_this_asset = 0 + updated_this_asset = 0 # CM data_cm = get_recursive_query(cursor_wo, assetnum, worktype="CM") @@ -1108,8 +1110,8 @@ async def query_data(target_assetnum: str = None): year, ), ) - inserted_this_asset += 1 - total_inserted += 1 + updated_this_asset += 1 + total_updated += 1 seq = seq + 1 # commit per asset to persist progress and free transaction @@ -1127,16 +1129,16 @@ async def query_data(target_assetnum: str = None): # progress per asset print( 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"Total inserted: {total_inserted}. " + f"Total Ins: {total_inserted}, Upd: {total_updated}. " f"Overall elapsed: {total_elapsed.total_seconds():.2f}s. " f"Progress: {pct_assets:.1f}%" ) # periodic summary every 10 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 try: @@ -1147,7 +1149,7 @@ async def query_data(target_assetnum: str = None): except Exception: 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: print("Error saat menjalankan query:", e)