From 4ce0ad9765f9e4f3f630d60815447453eab77cc7 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 10 Feb 2026 18:13:18 +0700 Subject: [PATCH] feat: Set forecasting start year to acquisition year, archive historical data only when the acquisition year changes, and prevent duplicate history entries. --- .../__pycache__/service.cpython-311.pyc | Bin 41416 -> 42461 bytes src/equipment/service.py | 157 ++++++++++-------- .../insert_actual_data.cpython-311.pyc | Bin 53968 -> 53957 bytes src/modules/equipment/insert_actual_data.py | 2 +- 4 files changed, 87 insertions(+), 72 deletions(-) diff --git a/src/equipment/__pycache__/service.cpython-311.pyc b/src/equipment/__pycache__/service.cpython-311.pyc index 9559c6373c5bfa7f025fb4521588aaa5e2c1f75d..158ac2fc34a620c484ed9bdb28589eb0dac8e2df 100644 GIT binary patch delta 2966 zcmd5;Yfu~472dmIg@gb-5VCj$8IWOlh=*fH32#a;f?#YQi7|! zFc|EU)+7#2+?l4+$@JmbnNB*LOlUJT&Ny);B5Ir1#%?D4<0ci2os5&>^xj>F)6|`5 z|8-~eoo~-M_w2b3owFC-rmtV5!+#nU7DC}`PIs*9=ES*hn@N5nl%ft>KvP@}Q}v;+ zyD$X2Y^3m^DOsVmxT=Y_bRDrk6kA4jC&NlY28^qIHw+Zcs$ zl#7wEJPWrHtJJBkucK0ws{w2YQDi)7fcV7Hu!i*r6xE&fjl|R_O^lT$#!BbYNvx<0 zp5Ya|icjLrM=g9Jui>@)CO(zV5ZLrO8l?-vmIKJLq_?#X40buYTwVQpTHOvC*UC9M zmYCkIzAo32+SzXF#VY4u+rXfMbHi$;5oHR7EJW)g`g`pTr;E5hup4sr1!dwi;cU)E z_*G6A{5~f>WDYAX;h*y@;dV|4_L#|?{`Eq+bWT*`vR>Qh$xmDS5i z?YMDKm*>~jT-Mbs=<0mB9e&*opR(SstY21!k4GLEIBJ|QE-Q`W@jhj=Ul|=xQH=4f zj#8=@)sOlVTl|VG|0%Y5JVJV<)mv*@(Aa#+Hovk>*!Y-%X5Pk0?h*>}<7nmrru&4J zf}bGt2M8+4lD~r$+&ttLB{1(|ItI3)6X-qR)1rAAU4q}1K7(dKUzQL-Cd$2sVM*TJ z-`Cf_2M?+$OJFWz)lp$@+1()IgN{lIdLMpR`Kw|$5C8>Vjx&u@E^3B9^*s^xrOZL}o4PZUxZ z!uAokK_HC441r~MxiL<2lTbAQJ%L;BPGc^!g6Rz?+1e}YeJWW-Gc>%o^I0?xU+jF6 zxrc4{g_Ike7 zxKFfl&Q7>A7_%i+*B+;XbG6!SE*tk1u~7&Xcc(LQ95h;xxBLjD24l*RcXLNDID;^l z@Wtg~g4dpN*jx@yg`Ld8`Bo+bx#8WeNx7Dc(m`u?De}PC?mWC}SG#-G^S1UBL@J3# zA&Hw#AcMepINg(~3MZD6kN&pL$JL` z55+LwIzWvIp}j zj4ED+F?2TgUuzqpL~9rycAA=%jp$r)>!T^k9uD`0(t;7Mv#VdG;g&lZwtvse=y|=f z8Ey~7!JIn=Jnn30aBGkOR$8JkdfcUGgUC0)2kyjRjg#iJ+|#h?E{u%jWqe4hSjEX( z#3wbB4kyEpQxoAAF17H|u!Sx#;MqUH($Y$ilQe8DeNe+pq=uQ=J8gTObC~RSwc1?{ zyQ!m}GY!)jMW7vt2^k))uc)agZ!l#;=GX>k9nrv3lhM_t(%SEu9##!ag{FM0|GGMw zYO0>B!lQ8xFT-!$508C?E;HyR0LBjpen(}8NV6_>&w=pOAAWy}6PQC{5NF3RG= zMbPW9i2NmwS(I;ibfWZmVnz9}XGE0$O$^=GBcgBCSehtzNm7f`&B9A3=4?wLRwyhO5(KQ79WV?l6fJWu3* z{lU6UnqA2;2AU2@tMQvdiK5T7L+h(hFFwCM5ejDxXN&yXtFU^wO4R><ayFCM%E@h9)wEzm_!ZosRaN*3K^6Y{f3Tq!PJf2y2{h?s!&^5f8yF*9c!8XG1FSpjSwB z_)4%$Z9&Kd_UUvq04JvTynuqz#002X6jp<-Ya+b{_ZyuU@O>dolHQ%|YsBF8X@>op zVXtNwCQd|qDAqyamXOpz<5G^%t1(?MXI?hvEtvDXk8Sln-Rd0}@mlxe+h_Lp%^q+3 zx9`s>D&9)}T|^uzzJ`RtIIX(5At68&YX{Ls$!P&fmtkC?bSY7H2zd2LflXA5$9y?D zeIYvCn^EYkYxZ_?-eM=debFv|w9Bi1$bDB%rKAU_O(fVR5)1ECj9NB2{%^{r-@>#(Z6CmFT9QhpJNOR{%ALdBIm70y%c1UC6I*E=n z6R{&K1-13}_i|+WIV%jz#G&VeshQ34SSyX>S0Z8qGLby`#2VSrhzZ;{6Kf>;0QGO! iBMDP_sMU+y-s<^VNqWjQcsSzd>rBr+a z2a&2ZHpZ%HO>8DjY_(~XKCn&ORk2#C_0jZ~v01H}sx|hU9ZhXD>7P#Ke&@UAb?=>f z=iJNR@IBqU;b3&Mj>G4CVpGlH4IdaBqV{7w$8E5~(Yi|5XO35X5z`}#cK4m$Kw#T=+eLK%QK*{VlXtmY=C{Q7?~xM&l1XKm93O7$tDYOgshi`$-_6=}5PND|@q=7|a}*$TF2>BN-2`XVCG#aN}q!i&Cs@5q5lkRHRw~$ku$-al`;zioCk}dB{8Lk@^erUL$sG1QDbzH?8VLFb zq6xYPeuCw5l1-P08VTYEF2l|_qlKGDe*i;?TPc{E9>zanu=LeMtQ&e>ogv&s+dXB# zd_N1njkHt>!j1(N?LCUV4PPwC6CNSu6wShUZ1NwZ4I-E|QabT6g8KvyNUN2c#MNMA zefk=2txNJ(IURn7^pI?Sail?(n^yJ42^F>s?Ys;-6QeMR2o1X@&@!_*w= zkwR^47=kE@(rA=@TBpnH^1CDpy0*fd*V^^8TTR%0zob#xR>$zn3H#S%vl6(lrnRJ& zmt&;I{us1Bd7L_#?dF_%S&tYchapDGh7i{v8s65wi>cqkrap`dc^cuJ`gq7|Fu|Ag zgWy!d2#B6{mYfdP&8c8?7?fLb7GE5T zd8e3pb&0gm^~c(}a)7Ol^O_VvTz`1)9w zx_J;R+dLSIO;s=SvosHe>CIMXXtoYC^LQz;1aO5h7;gHLi2gY^S z*m79jA+Nn^91!H@uxNAF3RPY<#p7S!-@zYCRHceN)@OTx%Ibe~-SV?t7V~#Ph*so~yko%HSaaMhE_PV_9fE*K45o4Yhh9j?IGd0WFpue(9$4ZHjP>Dr zCShf7!peZ@1@B!QH!LH_rBSLhN`*kFO4)zrig_*{J=gd;x?>fS>`!H|!5cUA-cNepZ@cHf~Yw7-y=uT4ij;{first_year}, Cost {current_acq_cost}->{first_cost}. Archiving history.") + # We only archive transaction history if the acquisition year itself changed. + # This prevents redundant history entries for cost-only updates. + if change_year: + print(f"Acquisition year change detected for {assetnum}: {current_acq}->{first_year}. Archiving history.") acq_year_ref = f"{current_acq}_{current_target}" # --- ARCHIVE HISTORICAL DATA --- - # 1. Copy old equipment master data to history - history_ms_query = text(""" - INSERT INTO lcc_ms_equipment_historical_data ( - id, assetnum, acquisition_year, acquisition_cost, capital_cost_record_time, design_life, - forecasting_start_year, forecasting_target_year, manhours_rate, created_at, created_by, - updated_at, updated_by, min_eac_info, harga_saat_ini, minimum_eac_seq, minimum_eac_year, - minimum_eac, minimum_npv, minimum_pmt, minimum_pmt_aq_cost, minimum_is_actual, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, category_no, proportion, - acquisition_year_ref - ) - SELECT - uuid_generate_v4(), assetnum, acquisition_year, acquisition_cost, capital_cost_record_time, design_life, - forecasting_start_year, forecasting_target_year, manhours_rate, created_at, created_by, - updated_at, updated_by, min_eac_info, harga_saat_ini, minimum_eac_seq, minimum_eac_year, - minimum_eac, minimum_npv, minimum_pmt, minimum_pmt_aq_cost, minimum_is_actual, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, category_no, proportion, - :acq_year_ref - FROM lcc_ms_equipment_data - WHERE assetnum = :assetnum - """) - await db_session.execute(history_ms_query, {"acq_year_ref": acq_year_ref, "assetnum": assetnum}) - - # 2. Copy old transaction data to lcc_equipment_historical_tr_data - # Format: {acquisition_year}_{forecasting_target_year} + # Check for existing identical archive to prevent duplicates (after calculation failures/retries) + check_hist_query = text("SELECT 1 FROM lcc_ms_equipment_historical_data WHERE assetnum = :assetnum AND acquisition_year_ref = :acq_year_ref LIMIT 1") + hist_exists = (await db_session.execute(check_hist_query, {"assetnum": assetnum, "acq_year_ref": acq_year_ref})).fetchone() - history_tr_query = text(""" - INSERT INTO lcc_equipment_historical_tr_data ( - id, assetnum, tahun, seq, is_actual, - raw_cm_interval, raw_cm_material_cost, raw_cm_labor_time, raw_cm_labor_human, - raw_pm_interval, raw_pm_material_cost, raw_pm_labor_time, raw_pm_labor_human, - raw_oh_interval, raw_oh_material_cost, raw_oh_labor_time, raw_oh_labor_human, - raw_predictive_interval, raw_predictive_material_cost, raw_predictive_labor_time, raw_predictive_labor_human, - raw_project_task_material_cost, "raw_loss_output_MW", raw_loss_output_price, - raw_operational_cost, raw_maintenance_cost, - rc_cm_material_cost, rc_cm_labor_cost, - rc_pm_material_cost, rc_pm_labor_cost, - rc_oh_material_cost, rc_oh_labor_cost, - rc_predictive_labor_cost, - rc_project_material_cost, rc_lost_cost, rc_operation_cost, rc_maintenance_cost, - rc_total_cost, - eac_npv, eac_annual_mnt_cost, eac_annual_acq_cost, eac_disposal_cost, eac_eac, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, - created_by, created_at, acquisition_year_ref - ) - SELECT - uuid_generate_v4(), assetnum, tahun, seq, is_actual, - raw_cm_interval, raw_cm_material_cost, raw_cm_labor_time, raw_cm_labor_human, - raw_pm_interval, raw_pm_material_cost, raw_pm_labor_time, raw_pm_labor_human, - raw_oh_interval, raw_oh_material_cost, raw_oh_labor_time, raw_oh_labor_human, - raw_predictive_interval, raw_predictive_material_cost, raw_predictive_labor_time, raw_predictive_labor_human, - raw_project_task_material_cost, "raw_loss_output_MW", raw_loss_output_price, - raw_operational_cost, raw_maintenance_cost, - rc_cm_material_cost, rc_cm_labor_cost, - rc_pm_material_cost, rc_pm_labor_cost, - rc_oh_material_cost, rc_oh_labor_cost, - rc_predictive_labor_cost, - rc_project_material_cost, rc_lost_cost, rc_operation_cost, rc_maintenance_cost, - rc_total_cost, - eac_npv, eac_annual_mnt_cost, eac_annual_acq_cost, eac_disposal_cost, eac_eac, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, - created_by, NOW(), :acq_year_ref - FROM lcc_equipment_tr_data - WHERE assetnum = :assetnum - """) - await db_session.execute(history_tr_query, {"acq_year_ref": acq_year_ref, "assetnum": assetnum}) + if not hist_exists: + # 1. Copy old equipment master data to history + history_ms_query = text(""" + INSERT INTO lcc_ms_equipment_historical_data ( + id, assetnum, acquisition_year, acquisition_cost, capital_cost_record_time, design_life, + forecasting_start_year, forecasting_target_year, manhours_rate, created_at, created_by, + updated_at, updated_by, min_eac_info, harga_saat_ini, minimum_eac_seq, minimum_eac_year, + minimum_eac, minimum_npv, minimum_pmt, minimum_pmt_aq_cost, minimum_is_actual, + efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, category_no, proportion, + acquisition_year_ref + ) + SELECT + uuid_generate_v4(), assetnum, acquisition_year, acquisition_cost, capital_cost_record_time, design_life, + forecasting_start_year, forecasting_target_year, manhours_rate, created_at, created_by, + updated_at, updated_by, min_eac_info, harga_saat_ini, minimum_eac_seq, minimum_eac_year, + minimum_eac, minimum_npv, minimum_pmt, minimum_pmt_aq_cost, minimum_is_actual, + efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, category_no, proportion, + :acq_year_ref + FROM lcc_ms_equipment_data + WHERE assetnum = :assetnum + """) + await db_session.execute(history_ms_query, {"acq_year_ref": acq_year_ref, "assetnum": assetnum}) + + # 2. Copy old transaction data to lcc_equipment_historical_tr_data + history_tr_query = text(""" + INSERT INTO lcc_equipment_historical_tr_data ( + id, assetnum, tahun, seq, is_actual, + raw_cm_interval, raw_cm_material_cost, raw_cm_labor_time, raw_cm_labor_human, + raw_pm_interval, raw_pm_material_cost, raw_pm_labor_time, raw_pm_labor_human, + raw_oh_interval, raw_oh_material_cost, raw_oh_labor_time, raw_oh_labor_human, + raw_predictive_interval, raw_predictive_material_cost, raw_predictive_labor_time, raw_predictive_labor_human, + raw_project_task_material_cost, "raw_loss_output_MW", raw_loss_output_price, + raw_operational_cost, raw_maintenance_cost, + rc_cm_material_cost, rc_cm_labor_cost, + rc_pm_material_cost, rc_pm_labor_cost, + rc_oh_material_cost, rc_oh_labor_cost, + rc_predictive_labor_cost, + rc_project_material_cost, rc_lost_cost, rc_operation_cost, rc_maintenance_cost, + rc_total_cost, + eac_npv, eac_annual_mnt_cost, eac_annual_acq_cost, eac_disposal_cost, eac_eac, + efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, + created_by, created_at, acquisition_year_ref + ) + SELECT + uuid_generate_v4(), assetnum, tahun, seq, is_actual, + raw_cm_interval, raw_cm_material_cost, raw_cm_labor_time, raw_cm_labor_human, + raw_pm_interval, raw_pm_material_cost, raw_pm_labor_time, raw_pm_labor_human, + raw_oh_interval, raw_oh_material_cost, raw_oh_labor_time, raw_oh_labor_human, + raw_predictive_interval, raw_predictive_material_cost, raw_predictive_labor_time, raw_predictive_labor_human, + raw_project_task_material_cost, "raw_loss_output_MW", raw_loss_output_price, + raw_operational_cost, raw_maintenance_cost, + rc_cm_material_cost, rc_cm_labor_cost, + rc_pm_material_cost, rc_pm_labor_cost, + rc_oh_material_cost, rc_oh_labor_cost, + rc_predictive_labor_cost, + rc_project_material_cost, rc_lost_cost, rc_operation_cost, rc_maintenance_cost, + rc_total_cost, + eac_npv, eac_annual_mnt_cost, eac_annual_acq_cost, eac_disposal_cost, eac_eac, + efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, + created_by, NOW(), :acq_year_ref + FROM lcc_equipment_tr_data + WHERE assetnum = :assetnum + """) + await db_session.execute(history_tr_query, {"acq_year_ref": acq_year_ref, "assetnum": assetnum}) # 3. Delete old data del_query = text("DELETE FROM lcc_equipment_tr_data WHERE assetnum = :assetnum") await db_session.execute(del_query, {"assetnum": assetnum}) - # Update Equipment Master + # Update Equipment Master regardless of if archive was needed/skipped + if change_year or change_cost: if first_cost is not None and eq.acquisition_cost != first_cost: eq.acquisition_cost = first_cost if eq.acquisition_year != first_year: eq.acquisition_year = first_year + eq.forecasting_start_year = first_year # Align start with acquisition if is_valid_default and current_life: eq.forecasting_target_year = first_year + current_life 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 2a6f1651d19b83510616acc623f587fd9416b9a8..11b06a39bc62d0850ec148e6fcba929eb903acad 100644 GIT binary patch delta 595 zcmW-ZKWGzC9LL}9-n(4$nrmx$Nt)Bxv%$oiCekK0Sway(p@W0b#SF$I&EX?#3i(t7;2Pc={VACxT^8XIR%~gu;mp6R*e7}6&@8=Qyengi(=sG9z9fn>$ z`S=`8czqS}dtcXMhdt3H-XPQ|5oJ1@&g zX6v5Gy6YghYw~hX>ssM5)4DsTo-$P~r@=c>S$dbm!)>K^1J&L(ucXyJh)RU@!+;#& zm4wze732*-PS6&Xq#{4`ugswJk)vBP=`?k zIR?lEMULa4;riS$Zw6!;C81FwqZF?#($VHhEkMVh%4Hl-C)c7{RpU6adIIJ0vnj{eeYx(+^y&33{QT~lizv2x9HCn^?Y1j=0v_v7jK@E zujii7F^sIO<6jhhTbEA$0=@Q1ZJs~A(iEpby|VOD%2x99%msg%FW$dv{(IuKUVm5# z!kKmQrK4(}_dCWNtNLxiA>Mq@ekk*8d%F=QZ7)B_YVCmkU`J~wwP$kK(QwJ$%gEA6 z-Fg&NyXzpi8|8;?);0ZyjP(>$drXr{&wYPYmi~r#x~KHBsP^}GA)yUaQ3$AmtAHHf zg%}$|7357oj?o4dLy;c_TMH?1Hz~_-O&q1ja2qcTQ@j|Z!@MZ^l#v%Ck9@os);)xlCM|Tqd3d#j+_%IWo!dGNV%+ zFqcF*LZ%U3Ht940m^Vd5Co`Q_F4NfxV6KXaNoFRmcFvxbfn;&p>Lv4wL3kPWqNTCE=u{Xg{hs%-!O diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index c289efc..88f1764 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -993,7 +993,7 @@ async def query_data(target_assetnum: str = None): if acquisition_year: # Remove data before acquisition_year cursor.execute("DELETE FROM lcc_equipment_tr_data WHERE assetnum = %s AND tahun < %s", (assetnum, acquisition_year)) - forecasting_start_year = acquisition_year - 1 + forecasting_start_year = acquisition_year elif forecasting_start_year_db: # If no acquisition_year but forecasting_start_year defined in DB forecasting_start_year = forecasting_start_year_db