From d10f9a2bde11f239ffad294623b32fcfb3b86801 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 2 Feb 2026 13:37:51 +0700 Subject: [PATCH] feat: Implement historical transaction records and refine actual data processing logic with acquisition year handling and targeted execution. --- src/equipment/model.py | 57 ++++++++++++++++++ src/equipment/router.py | 6 +- src/equipment/schema.py | 1 + src/equipment/service.py | 13 +++- src/modules/equipment/Prediksi.py | 57 +++--------------- .../__pycache__/Prediksi.cpython-311.pyc | Bin 53849 -> 53551 bytes .../insert_actual_data.cpython-311.pyc | Bin 51862 -> 53055 bytes src/modules/equipment/insert_actual_data.py | 18 ++++-- 8 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/equipment/model.py b/src/equipment/model.py index 454c16c..a0fa9f7 100644 --- a/src/equipment/model.py +++ b/src/equipment/model.py @@ -100,3 +100,60 @@ class EquipmentTransactionRecords(Base, DefaultMixin, IdentityMixin): eac_eac = Column(Float, nullable=False) efdh_equivalent_forced_derated_hours = Column(Float, nullable=False) foh_forced_outage_hours = Column(Float, nullable=False) + + +class EquipmentHistoricalTransactionRecords(Base, DefaultMixin, IdentityMixin): + __tablename__ = "lcc_equipment_historical_tr_data" + + equipment = relationship( + "Equipment", + backref="historical_maintenance_records", + lazy="raise", + primaryjoin="and_(EquipmentHistoricalTransactionRecords.assetnum == foreign(Equipment.assetnum))", + viewonly=True, + ) + + assetnum = Column(String, nullable=False) + tahun = Column(Integer, nullable=False) + seq = Column(Integer, nullable=False) + is_actual = Column(Integer, nullable=False) + raw_cm_interval = Column(Float, nullable=False) + raw_cm_material_cost = Column(Float, nullable=False) + raw_cm_labor_time = Column(Float, nullable=False) + raw_cm_labor_human = Column(Float, nullable=False) + raw_pm_interval = Column(Float, nullable=False) + raw_pm_material_cost = Column(Float, nullable=False) + raw_pm_labor_time = Column(Float, nullable=False) + raw_pm_labor_human = Column(Float, nullable=False) + raw_predictive_interval = Column(Float, nullable=False) + raw_predictive_material_cost = Column(Float, nullable=False) + raw_predictive_labor_time = Column(Float, nullable=False) + raw_predictive_labor_human = Column(Float, nullable=False) + raw_oh_interval = Column(Float, nullable=False) + raw_oh_material_cost = Column(Float, nullable=False) + raw_oh_labor_time = Column(Float, nullable=False) + raw_oh_labor_human = Column(Float, nullable=False) + raw_project_task_material_cost = Column(Float, nullable=False) + raw_loss_output_MW = Column(Float, nullable=False) + raw_loss_output_price = Column(Float, nullable=False) + raw_operational_cost = Column(Float, nullable=False) + raw_maintenance_cost = Column(Float, nullable=False) + rc_cm_material_cost = Column(Float, nullable=False) + rc_cm_labor_cost = Column(Float, nullable=False) + rc_pm_material_cost = Column(Float, nullable=False) + rc_pm_labor_cost = Column(Float, nullable=False) + rc_predictive_labor_cost = Column(Float, nullable=False) + rc_oh_material_cost = Column(Float, nullable=False) + rc_oh_labor_cost = Column(Float, nullable=False) + rc_project_material_cost = Column(Float, nullable=False) + rc_lost_cost = Column(Float, nullable=False) + rc_operation_cost = Column(Float, nullable=False) + rc_maintenance_cost = Column(Float, nullable=False) + rc_total_cost = Column(Float, nullable=False) + eac_npv = Column(Float, nullable=False) + eac_annual_mnt_cost = Column(Float, nullable=False) + eac_annual_acq_cost = Column(Float, nullable=False) + eac_disposal_cost = Column(Float, nullable=False) + eac_eac = Column(Float, nullable=False) + efdh_equivalent_forced_derated_hours = Column(Float, nullable=False) + foh_forced_outage_hours = Column(Float, nullable=False) diff --git a/src/equipment/router.py b/src/equipment/router.py index 655ca3f..23545e0 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -287,7 +287,8 @@ async def get_equipment(db_session: DbSession, collector_db_session: CollectorDb last_actual_year, maximo_data, joined_maximo_record, - min_eac_disposal_cost + min_eac_disposal_cost, + historical_records ) = await get_master_by_assetnum(db_session=db_session, collector_db_session=collector_db_session, assetnum=assetnum) # raise Exception(equipment[0]) if not chart_data: @@ -307,7 +308,8 @@ async def get_equipment(db_session: DbSession, collector_db_session: CollectorDb last_actual_year=last_actual_year, maximo_data=maximo_data, joined_maximo=joined_maximo_record, - min_eac_disposal_cost=min_eac_disposal_cost + min_eac_disposal_cost=min_eac_disposal_cost, + historical_data=historical_records ), message="Data retrieved successfully", ) diff --git a/src/equipment/schema.py b/src/equipment/schema.py index 0ede5a4..4d788b5 100644 --- a/src/equipment/schema.py +++ b/src/equipment/schema.py @@ -106,6 +106,7 @@ class EquipmentRead(DefaultBase): maximo_data: Optional[List[dict]] = Field(None, nullable=True) joined_maximo: Optional[List[dict]] = Field(None, nullable=True) min_eac_disposal_cost: Optional[float] = Field(None, nullable=True, le=MAX_PRICE) + historical_data: Optional[List[MasterBase]] = Field(None, nullable=True) class EquipmentTop10(EquipmentBase): id: UUID diff --git a/src/equipment/service.py b/src/equipment/service.py index 74fe65f..fe87502 100644 --- a/src/equipment/service.py +++ b/src/equipment/service.py @@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from asyncpg.exceptions import InterfaceError as AsyncpgInterfaceError from src.database.service import search_filter_sort_paginate -from src.equipment.model import Equipment, EquipmentTransactionRecords +from src.equipment.model import Equipment, EquipmentTransactionRecords, EquipmentHistoricalTransactionRecords from src.acquisition_cost.model import AcquisitionData from src.yeardata.model import Yeardata from ..equipment_master.model import EquipmentMaster @@ -299,6 +299,16 @@ async def get_master_by_assetnum( None, ) + # Historical data query + historical_query = ( + Select(EquipmentHistoricalTransactionRecords) + .join(EquipmentHistoricalTransactionRecords.equipment) + .filter(Equipment.assetnum == assetnum) + .order_by(EquipmentHistoricalTransactionRecords.tahun.asc()) + ) + historical_result = await db_session.execute(historical_query) + historical_records = historical_result.scalars().all() + return ( equipment_master_record, equipment_record, @@ -310,6 +320,7 @@ async def get_master_by_assetnum( maximo_record, joined_maximo_record, min_eac_disposal_cost, + historical_records, ) # return result.scalars().all() diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index 92b2a5b..b6ba30e 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -160,9 +160,9 @@ class Prediksi: cursor = connection.cursor() - # Query untuk mendapatkan nilai maksimum seq + # Query untuk mendapatkan nilai maksimum seq dari data actual get_max_seq_query = """ - SELECT COALESCE(MAX(seq), 0) FROM lcc_equipment_tr_data WHERE assetnum = %s + SELECT COALESCE(MAX(seq), 0) FROM lcc_equipment_tr_data WHERE assetnum = %s AND is_actual = 1 """ cursor.execute(get_max_seq_query, (equipment_id,)) max_seq = cursor.fetchone()[0] @@ -187,49 +187,6 @@ class Prediksi: %s, %s, 0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'Sys', NOW() ) """ - - # If a token was provided, store locally so fetch_api_data can use/refresh it - # if token: - # self.access_token = token - - # # Fetch data from external API (uses instance access_token and will try refresh on 403) - # async def fetch_api_data(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 403, try to refresh the access token and retry once - # if status == 403: - # print("Received 403 from reliability API, attempting to refresh access token...") - # new_access = await self.refresh_access_token() - # 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 refresh: {e2}") - # return {} - # print(f"HTTP error occurred: {e}") - # return {} - # except httpx.HTTPError as e: - # print(f"HTTP error occurred: {e}") - # return {} - # Menyiapkan data untuk batch insert atau update records_to_insert = [] @@ -241,6 +198,7 @@ class Prediksi: update_query = """ UPDATE lcc_equipment_tr_data SET + seq = %s, rc_cm_material_cost = %s, rc_cm_labor_cost = %s, rc_pm_material_cost = %s, @@ -254,7 +212,8 @@ class Prediksi: WHERE id = %s """ - for _, row in data.iterrows(): + for idx, row in data.iterrows(): + loop_seq = max_seq + idx + 1 # Check if data exists cursor.execute(check_existence_query, (equipment_id, int(row["year"]))) existing_record = cursor.fetchone() @@ -263,6 +222,7 @@ class Prediksi: # Update existing record record_id = existing_record[0] cursor.execute(update_query, ( + int(loop_seq), float(row.get("rc_cm_material_cost", 0)) if not pd.isna(row.get("rc_cm_material_cost", 0)) else 0.0, float(row.get("rc_cm_labor_cost", 0)) if not pd.isna(row.get("rc_cm_labor_cost", 0)) else 0.0, float(row.get("rc_pm_material_cost", 0)) if not pd.isna(row.get("rc_pm_material_cost", 0)) else 0.0, @@ -274,12 +234,11 @@ class Prediksi: record_id )) else: - max_seq = max_seq + 1 # Prepare for insert records_to_insert.append( ( str(uuid4()), # id - int(max_seq), # seq + int(loop_seq), # seq 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, @@ -831,8 +790,6 @@ class Prediksi: 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: diff --git a/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc b/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc index a3c4aa3919757b31fdc768ac28ee1e68812259eb..5a7408a7b6ad256bfe9d76edbd503c1ae268d422 100644 GIT binary patch delta 4257 zcmd^BdvH`&89(Pf_Lbf2E4z1}*-dsg56Gh-1C)eEQ!?^Q5zM(*8(W1?a(;tOU5_t$skLWkSpGRT5zdfvyaSkTek(z-6OxphN$PPobx&Uh5&yLCNi zkv?s0^d}giMQ>mU>OMOM%*A%TyKR$JRXm7Ag_S$pDLDscOMe9irzlRoLw;Xcbj3;enK%P>;DT^lK{$3g(PilDAi5)Jzr4mN0s%0|xT zwRnA_yw&Btji?x5Vl=5m;bzsX>Q%GQ1-x=5eO-STubz#r&oZqJ@x#1hwT~ZmiPSRe z6PYE%Gkv!A2^Vb2ku<6LBR!t3S!&Pm*s2ZA-6P?+*4$*bV58Qt?i~Z3r9HesG~p+; zxm$i`#64P3?;i~KsAlimZ{mk>3!H-Y@C9w>H)?sE&WzCWG5F=T9x{A2))(uwCuuUE zlTjlg2wrKE;Qe|V=)Y(2C5-oSgTDG=GM8qodz_go^NjxpKlX<6QedenB>40&Ss=p1 zJd-&hbYNB76>G<`xWl08lA4Zc`lP0(nt>q7yf+u*!Hs&GImx?5)kKYYsM-@PkqxT3 zFV<;D%|z*0So5N4Ar_fipUi0m?=~fz&?WhFl&>w6I+wo?X|NiRY$Vn#h_dkFzL;vG zPI6LCWJ5Ya)t(r)Q=aFEJv$85PHdEb+-{kZO~-PwI|nRm<#}{WCWj+p@gmr>KcC$M zE(dXRj^7rFuP>I_>vzcJqh?+;^w=vfO!6Xk_5BOa!*`m4=kyHru zFkFc!>^rKV0HWl%(y)9NCAb z`un-~s;tu~qU#DIGJW|{c7Kv$W|6WQ`7ukDf5K!pW9+?!Q~y{}xbB_5$f%;rMH^E3*{0Am!VAlv!lg_=eN^ z87s=PC7t`-&;MuiIQK9{QiCKo5Mp0tP<1$J>n=#lPi+HD|6uBS?)YK5H=|=x0;3-f zx%p~-yg1Xv1Cc#yesZZ9L`d={2BwB&q=rc-Bf!NZ#ELuhbQY#G*`2nk1rIpcg7Ktk zPLAuISX*g8vR@59u(raPwMD|c%d=T$Y9T4iSa!vP8ZOLSIJDM#d&2R&2UIbeNtdc( zT0Pl|(f3xI+GMbQ*Gq41D|EfAi{9rhIKiDK?aoKtu*-j< z!&2{K+SFIibGStN@W^A_^gA>%Ois3FcTq7N4L&;l2rhw+6CqKF(tg-=Vo=8xseB5}CxiG7Y(5#n zcj2XzwX2F0M%~H~6Kkm$wry-xS`w{a*c_))d8#d;+-BySjY^x`u`bck74KH`GyuiP zgo}yKu)#lI;t&%HnZQtS>Pu!Xi&R+GMYll*KRmSo7eUKhAK035Vae%vZi((!_Z1yN zAE&-z7YoJ;?fU88ipEi%lN`4ZC&8sNQTWXn5pJJ3)iX?ef2F9|i;mBCQTY~MKclgh zd%MQE$RrFJG1Ui6=0#rppqpd52Q%Hf&`29Y5$p15ZfF_<4ON?0aN^KB26`7`s=vz> zF}>8+C~%i~hPf=58=c~18^c_7V5U1iU(6acm`;7UHuQKDYQ)i0!(W{)GE0rfEb}~6zveZqMEtdgqq&y9Rx7SV@Z67%z>E`> zP wd~q$WV{N4qZoIt+7iq^u4<7DQ+Jcg(WL!$X79={Y#aLEUrMOEn*aa+ delta 4416 zcmdrPZE#f8_1^cfyKl4KyV>u}X0zE%LcR=;OaVnGpay{^P|BAW0xz)vy1>gK(B9X; zsMRWqWac9sBwQKn3%ww;dcBvo51qP=&+X7?pw?C;*} zJMY}{&euKno^x(a9_Bwh!dqT8n~jK$`x~}*ZOfdpIC;De%J@prE9)M|>G}LOc8|yy zZjWO6evCe!ePD*3r_15&L{St~9T8!7aa0r)H^qh~)p7KNXO|mPz09cwq6fRnD-f;| z!>L_P_~+gjbh(S1k}RmkN03@1i>gW1sb+$qwXNJX8}*4Rk37U7q*_#KR@&n<5iV)X zxq%R|%6gc&%L`_Y6YC-3sdfUf$-HlbMYRu0#>_<6Vb#bAM=?Uj=rcSLSVnLw=)qPP z@_4WTUh#>I#DOz^G?-iAxwh7A3!XiM*|xu2WiN8$J@c>lV- z`>(rVS6nsyu9{DR;a`X^>5u65&pSBpS}>f-B3Gx(&rDf%XjZ0l@`YgIrC?)*{@T{0 zZ`3W~`~i#4Kg8RL-Jc^01{oO2VsYqb_;m7e6FP^N8R=`F2(PfB0h4k0M1H{SSYFEy zREmsJD>BMNp5h-_eJd(?$cXOrfr($}@#^&Msc-T4?)3cWyFBIXXhJKpfgds19O>bNk&U znU{->n}$C^?eir;%Bg80#FEzv;W@F8lQ`Gfa=I0NZ+tX|&ncU;K4Qf|yP%7Rv(d^y zwU~yWeXfVcq!>=V?RTqg*`j)qv79Qp1MXxT8YcV1XNf!If(!SRJ5;ZR!J9OI{Xk$2 zYuC32@oN2*rF8=Pq%0v`O+>#G2)SY8lm)JKoKwK#=tD`*u ztx1e_4kah$<=6UiIB_K>$}5_Q9z>RN|0F_19FG9oe3cM!a-He3WU0lPw8L-n+5Refx^W z$`EVMm=)o~E3&&V`yNHg3%#_U$nN|}mxbXx%v17bzWd2<=N_psu+c5>C8Pv)yim@i zBD6`C*}5aiIk`AcpG#N~f)8J?`wFOI%M|#$mJ&6bs3;U3pRGp?XI(6~HIne?1*xSn zQX{0aU}1AfX<;(2jpn?RJ^4VWWp~)ovXR5omX+(y=1)!qU1**mOH?CwOsYs>Qjy5^ z*0EKlI!dAiuGg_#EwB|Wbe>+QeRCoS*Bw%rS<~fGn5ItT&Zz@KFM4~%rb!|;Mzt5C z@zuzdvGRq>)ku}+mQ+9upn}p6)RqRQ4+V8uD; zgIX>jf>IT%9+}GtCLHaz*?I--hXWC9y$h-_O=t9y7@Rxn^TmD|OBm=`qVFv@7e=bJ zl4)h#cmgeyXb~C{qR=RXzF~!7s)+HAR$1j=psb;5Nw@s`EQgn-ZATYy4}D1q%D)-7 zNx=Z$p4g_a3xx6?HZ^o^P&(GMcPEwaGTPS^ROuB&8=OMRAHDdTW42N5#h`$+v!DT+Ak+@O`7AqE~`072==pP1+wMFYZ`4KzL?ynJ#!u7IA!6>#$&31+?io$t-2f$X-?Tk(zD zwRpZD)m^Wuy;9ZMU)7pfxcoxZic3{1GSM+VQ|UF6J>#g%OqhPbbnhk8y&2QJ>D{mY zS>!W~&9LQHf9&~~y79zJ`%)2|^Uf-!uXpQaH+?_OosU_Y8-??eF~b{8a|QkTn4|dn zJZ8A?fJ7(#7VE+iu0O<>`b$g;YsLNvjH$l{GrTrV@ry41qI&LP9pkxJZ(1~6yf}^V zT)YQUeAUJ26?AebhNa$j1m_arP@pJtDr$6;A5X8DcG!9JfI}?!bJb2@s8gYQ{lDEPq?{wgwr5}8EHLqL9_IMqn-)qJ{ lNZ)$zK@s0fH~;Pfu3?)EQ+4CRp)CFL!mnE+(31JXe*t0m)nfnv 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 93630fac2ce7f98989af5831bde126b8eb7ac487..bc9c9625e27770b0b1c809b8c1b8e014fad94ea7 100644 GIT binary patch delta 4456 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