From f0142044cd3fe3b826323b2ab56365cd2a34c420 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 20 Feb 2026 10:22:52 +0700 Subject: [PATCH] update replace cost equipment logic --- docs/updated_acquisition_algorithm.md | 59 ++++++ src/database/service.py | 2 +- .../__pycache__/service.cpython-311.pyc | Bin 42461 -> 38426 bytes src/equipment/service.py | 194 +++++++----------- .../insert_actual_data.cpython-311.pyc | Bin 53957 -> 54461 bytes .../equipment/__pycache__/run.cpython-311.pyc | Bin 4437 -> 5349 bytes src/modules/equipment/insert_actual_data.py | 152 ++++---------- src/modules/equipment/run.py | 32 +-- 8 files changed, 189 insertions(+), 250 deletions(-) create mode 100644 docs/updated_acquisition_algorithm.md diff --git a/docs/updated_acquisition_algorithm.md b/docs/updated_acquisition_algorithm.md new file mode 100644 index 0000000..72006b1 --- /dev/null +++ b/docs/updated_acquisition_algorithm.md @@ -0,0 +1,59 @@ +# Updated Equipment Acquisition & Simulation Algorithm + +This document outlines the refactored logic for equipment acquisition cost calculation and simulation forecasting, implemented in February 2026. + +## 1. Timeline Definitions + +The simulation follows a strict temporal alignment to ensure consistency across the fleet: + +| Parameter | Value | Description | +| :--- | :--- | :--- | +| **Base Year** | `2015` | The target year for all "Value of Money" (Net Present Value) calculations. | +| **Forecasting Start** | `2015` | The year from which future predictions and Economic Life reports begin. | +| **Calculation Start** | `2014` | The technical sequence start ($seq = 0$) used to establish an initial state. | + +--- + +## 2. Capital Cost Adjustment (Value of Money) + +To account for the time value of money, both the **Initial Acquisition Cost** and the **Replacement Cost** are normalized to the **2015 Base Year** using the project's inflation rate. + +### 2.1 Adjustment Formula + +The value of any cost $V$ at a specific $Year$ is adjusted to its equivalent value in $2015$ using the following formula: + +$$V_{2015} = \frac{V_{Year}}{(1 + r)^{(Year - 2015)}}$$ + +Where: +- $V_{2015}$ = Adjusted value in 2015 terms. +- $V_{Year}$ = Raw cost recorded in the database or Maximo. +- $r$ = Inflation rate (from `lcc_ms_master`, defaults to $0.05$ if undefined). +- $Year$ = The year the cost was recorded ($Y_{acq}$ or $Y_{replace}$). + +### 2.2 Total Acquisition Cost + +The total capital cost $C_{total}$ stored in the master data is the sum of the adjusted initial cost and the adjusted first detected replacement cost: + +$$C_{total} = \frac{C_{initial}}{(1+r)^{(Y_{acq} - 2015)}} + \frac{C_{replace}}{(1+r)^{(Y_{replace} - 2015)}}$$ + +--- + +## 3. Maintenance Cost Suppression Logic + +A specific business rule is applied to prevent "double counting" or distorted maintenance records during major equipment replacement years: + +### 3.1 Replacement Year Rule +In the **first year** where a `replace_cost > 0` is detected in Maximo ($Y_{replace}$): +- All **Material Costs** are set to $0.0$. +- All **Labor Costs** (and labor hours) are set to $0.0$. + +### 3.2 Logic Rationale +The replacement cost is treated as a capital expenditure (CAPEX) that restarts the equipment's life cycle. Standard maintenance (OPEX) for that specific year is ignored because the replacement action supersedes regular repair tasks. + +--- + +## 4. Implementation Reference + +The logic is primarily contained in: +- `src/equipment/service.py`: `check_and_update_acquisition_data()` (Cost adjustments). +- `src/modules/equipment/insert_actual_data.py`: `query_data()` (Timeline and cost suppression). diff --git a/src/database/service.py b/src/database/service.py index b797248..baa61e7 100644 --- a/src/database/service.py +++ b/src/database/service.py @@ -134,7 +134,7 @@ async def search_filter_sort_paginate( # Get total count count_query = Select(func.count()).select_from(query.subquery()) total = await db_session.scalar(count_query) - if all: + if all or items_per_page == -1: result = await db_session.execute(query) items = _extract_result_items(result) return { diff --git a/src/equipment/__pycache__/service.cpython-311.pyc b/src/equipment/__pycache__/service.cpython-311.pyc index 158ac2fc34a620c484ed9bdb28589eb0dac8e2df..190e00cf52f799942706641d6dbb87a69a55c7c0 100644 GIT binary patch delta 2522 zcmZ`(OKcm*8Qvi!nUCeu;#-zRkz|PyO^cT7x=LbIPV`DFTbAv->O@&{R}vM9v}c!g z%`D3nxe#a%a@R@JI7SWBO_bIM6fg%JdT@Z&HPFkhz`}%3p~|V~jYd+yFi@obaP@9F znEht{fB)wJh^bPw`N43L#hV=G-tszT)3gXX{Y37|nczP)w;@{lMaYK<~lx&CTNcHJd^C?e!jr z^XPi#5O2!aWh=4CEV0TqVwP>PRc5nb`;GyC$=P{J&S7Sx5S8@7TR(xce}G3HIj3^t z^#F1z-{GL)j$QepwI%M7T|ak7)=d+JkzIS79H~ePBiXktS{c%mGfA#(4bRHx7Q^q8 zO}CiaHM{*0qvWrLo~_NfWy>t>uR6$4kepjmRBGAWLj}xE%X>EEyVhX-B6OMCpR1Sa zfrA~cyggeBF7*^4g*)c~mg;3I?|8>ZLlV2}=4)gJWr}LEa9CLrJkT=O)?7w>ZO$tj z2|QaqPF=s{XXNv{9lf$g_R^3!9~9%JiRh4hdeWR9wEJ}(!)%7l1!TWykOL$m`$=>c zf6fj5cEkccU9u0*EqiV;d49j_1)*Mc0D5lQ_L3|^A(PvM1W5a?1YVlpwqdUq?*Ol@ zLS;K);eJ^_TfJHEj@@XY7Ea|(z4DbDv-)yo*?f)(`?LR2{v!vKhFs^5E?nk0Nf7ZI zCrSdrT>MHVAtt0mIyD(jixTcgq(I`5xC;~Ea*~S+b3#fAV=l#GQIJIC%6q;cET!?0 z?%2_&1;dySqzp-k_&mu7I5D*unh5bU;qf(2#F=VRK7xn1q$uFDG$W8}iQ<>JS5SUO z77V3vcq&aW3Bac?6gChZx+3EM(;xksj?enDqvr-s4Zd(5U*(b+VKSAO!-sLKyF1#A zhbGR9;bc5MIVVnT^!Mt>;KU$KadQHG9=9h_Q%SDcD*+PPvyoT1JV_-|(|!1bbSBB; zR9eDQf)t;@yG`|?9Q4=V1W5d2AKc4^fo<||l{~#q-PjIxStsro;Af+FR426$XFWh{ zRW+fr+eqmJy&21R0*dOxPf<7<8Av9kX6(r62AeKXTD3@NiA(AZ z*7yvUnih0{-4H~HBa#?YzE1g-r9z$Zw_kNwPeC)kfxnEHttpz6!zlmnI>y|}YL`7v`6)wWgY7+;{|4Hv9sU*P6X3v4+MDg}B9_Vo}}P5!dg zTVas-G+KC0Ul&j7Yq{yrQrCS}J928JX-s7sAWpEcV6Ws2b><1Q7C4|bCbYn8DKJ}j zsodQ4sJUmQxo7Dmt+~I{+^>fA&PT!FmEf=z94!S$3on-)LDg|^t@+@cceUo8QghF| z{mGLxho7dNKs#}bb*b+0`+fkJ56`Ok>PBPFme{lE|I8;aDn=TFZcL0n1PxnlzdTW4 zYRw~P;nK%*`fja#v>a+%HvG|EF&aATj~Re(DO4E4GyfknHs4qnDCB_+XA{(E8$o6K z%w5}3jfQ)d9a_`yJPTTu*t!yms-bAPscQ)>oz}WfKD4T@yrGS}p&kCQ z)^t(l_dvVaaA>im>gu5+#L+_MV$Di0q6Q;n+^OQ&Vm!ZeW%-2mgVBds_2S#wnYXoL zoYr(n58yqhb{y9{#|xHqICEd;;_Qm=c)_~v50?C~rSmKP9~NkJtgpg2&20;j>H=p0 zl-OcxPEW;vJXy4K9CE{|<1#7rPwY_Sk^0)D9Wp8(JO+$!*_X%O+$GLj-6`shkBvr` z`&$N}QUw#!9-B9moGt5)5UphpK{2ZL%>CB@U~10sl5<>jj+Z0d7pP%)eSZstat)$I zR$t4pqsy!odvX3`sXn^yZg}KwS#h^$?zWP7mE-t)Wdj>v7d-2}FpNzdx`kRRXtkzsg zr;~(!sf4~Kz4W(pl)^E9r>5uT(tIWg+Hx#sl_@c{4+b zJH(sqVUc#FQcYW>-HqByTdMXz8P;>FKX=xGq94KgMI^m> z$K|NfDmF>pHce2Yco+((($efm%4Pt^J}QuA^RA35}=-K~yaSLhGBPo8Yjn&jJ-v@Pr@5pFhx zn>DXSa*dWSMG{9uyd@%jQ^fxw5nqYOaV%1OoNLlZfy4%;z(tSj&>GR4R(bD8!1JL) z)mo+Iqzj2C7kt#NRRB*>QsA$_aurh4faNk#!E6Awbl~n7nPS)#RgeX90Bjl+kr0Laz;Y$%KpZ%Qt^-K+TrOp_YzIRXo>h3x z6ihk>N0Uuq>ayvMATH(|3Zp5kC3$mfa_l>^)EVl^jhQJb#-><0F4H{Nkcji5LRiYqMwWR+RO8fZ)P=(L<>@!5BI{{vJ&pK?$;7T4{!flijl+=O4)$cf z9)t?SD(1m4h=xuMHw}$X-yFL!ON~#@&VXfKV^|-SX_*&r?riJe>o7*<2uCrjBr73` zWfFn{?%_W$mz5AlD?*&dDVK#sk&X!wK4fawx7Yozhj3td7Savjq!k7&@`|lI&WKS- zMKXxA%ShG~kThqoO=D&eR#RZY(8I~5rh!motpq`;+hEV_^m{cLI z#!U{HUpb1v0k^czfR;SpA&;0?J^V?;!KyqP{Lsvb8G=KkAk#3>RVHTX{2m|SqUOw4 zWny@0Kp0r?j^nYn9Bq1zgA>hDBvoM+`8c%v1irD-F;yD3!#8tjTbrGS=5Jl9|p`IZX4LM~EDJ{V!2h~?zE;YXkGsGlWrlI~M zRE3_J>#{;$j7%VbwX1pv3rGoEOQ0SdNmiV}!4esf<*njqmY-h4;%t}tWFjqB%$=pc zTPABPUC7W;-ZJT6=|boad0WATXTo$yzHXAOAU$g*1VXWkJ@6G=KSY)Hy8)OLDZf|R zI}P5e%#8(qFroxh zawU234Y*Uyj^%GuP}mB$sr&)s`8v#V%WSYw|IItmW;Z^_;Cn1;#$QeV!4`ewUeI#} z|Nj+JzK=mrm+oCnu_{9NV4knE`yM!_z;Oi5${ZhI)R?kgXI&D_`%2*lS7r0iwC`b~ zJiBaO`sfvvM{E0a_yS1tKL9B2d+ggVz9L%W0tP(uTBusMDO*&zMgLT6(Ub3n>aK#l zzj6?S{Inwd@ZBLYbx_^-giy$1!OwhMTT45y8otS0-(yZL*@o5}LAq{`_cg2Ec3XY+ zO`mdZs zcV8+8spK6`2%}-NEs$9C4{pB|qyO$PwPbmM4U_@5GlF7){-|Qz@dY=W%OjuQg;0 zzGhs%Z7d<IPRmrp}MpK#gRTa?}$`PlqvQtN9Ee(JHmZ_nSibz=M0PKz;oZP!1M@lP0@G7WNuvr*>?-d{GmtS?~rf?Erj@a3Ib zd*K-)Jd^Do-h4gN)O+A-&iGDk_3!zH4d1Yho$t+_zJSSt+2g%1F`8${tx2oTp1ZUY z+C4XY-;rrKdw{L#-ShYE`hyvN(D0OL+3r5W(|J(WZdA6*r}d%tUP=G(y#PsG)qk-u zaqJq*{L%D3HvUpEJb)K%7{Gqzf+WQdUK9w^(J)@b=P(!n@b8+7afwr7{3u#XfBHeT g=Im_;8Thz1N8tX^O{+J)J8ylIfp;tQ&wn=bA8Ud{WdHyG diff --git a/src/equipment/service.py b/src/equipment/service.py index e3080c9..0651595 100644 --- a/src/equipment/service.py +++ b/src/equipment/service.py @@ -670,9 +670,9 @@ async def delete(*, db_session: DbSession, equipment_id: str): async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str) -> bool: """ - Check if acquisition year/cost in Maximo differs from local DB. - If changed, archive history, delete transaction data, update master, and return True. - Otherwise return False. + Check if acquisition cost in Maximo differs from local DB. + Updates master acquisition_cost (initial + replacement) and sets forecasting_start_year to 2015. + Returns True if master record was updated, False otherwise. """ conn = get_production_connection() first_year = None @@ -680,7 +680,7 @@ async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str if conn: try: cursor = conn.cursor() - # Query the oldest year from wo_maximo to detect the original acquisition + # Query the oldest year from wo_maximo to detect the original replacement cost query = """ select DATE_PART('year', a.reportdate) AS year, a.asset_replacecost AS cost from wo_maximo a @@ -697,7 +697,7 @@ async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str cursor.close() conn.close() except Exception as e: - print(f"Error fetching acquisition year for {assetnum}: {e}") + print(f"Error fetching replacement data for {assetnum}: {e}") if conn: try: conn.close() @@ -706,123 +706,75 @@ async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str updates_performed = False - 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 - current_acq_cost = eq.acquisition_cost - - # If current_target is logically "default", we update it. - # If user changed it to something else, we might want to preserve it - # if it currently holds the default value (based on old acq year). - is_valid_default = False - if current_acq and current_life and current_target: - is_valid_default = current_target == (current_acq + current_life) - - # Check for changes - change_year = (eq.acquisition_year != first_year) - change_cost = (first_cost is not None and eq.acquisition_cost != first_cost) - - # 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}" + # 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 + + is_valid_default = False + if current_acq and current_life and current_target: + is_valid_default = current_target == (current_acq + current_life) + + # Fetch inflation rate from lcc_ms_master for value-of-money adjustment + inflation_rate = 0.05 # Default fallback + try: + rate_query = text("SELECT value_num / 100.0 FROM lcc_ms_master WHERE name = 'inflation_rate'") + rate_result = (await db_session.execute(rate_query)).scalar() + if rate_result is not None: + inflation_rate = float(rate_result) + except Exception as e: + print(f"Warning: Could not fetch inflation_rate for {assetnum}: {e}") - # --- ARCHIVE HISTORICAL DATA --- - - # 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() - - 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 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 - - await db_session.commit() - updates_performed = True + # Calculate initial cost from category/proportion (base acquisition cost) + initial_cost = 0.0 + if eq.category_no and eq.proportion: + _, aggregated_cost = await fetch_acquisition_cost_with_rollup( + db_session=db_session, base_category_no=eq.category_no + ) + if aggregated_cost: + initial_cost = (eq.proportion * 0.01) * aggregated_cost + + # Adjust initial cost to 2015 value (Base Year) + # Formula: Value_2015 = Value_Year / (1 + rate)^(Year - 2015) + adj_initial_cost = initial_cost + if current_acq and current_acq != 2015: + adj_initial_cost = initial_cost / ((1 + inflation_rate) ** (current_acq - 2015)) + + # Adjust replace cost to 2015 value (Base Year) + adj_replace_cost = (first_cost or 0.0) + if first_year and first_year != 2015: + adj_replace_cost = (first_cost or 0.0) / ((1 + inflation_rate) ** (first_year - 2015)) + + # Total cost is adjusted initial cost plus the adjusted replacement cost + total_cost = adj_initial_cost + adj_replace_cost + + change_cost = (eq.acquisition_cost != total_cost) + # Requirement: forecasting_start_year always starts from 2015 + change_start = (eq.forecasting_start_year != 2015) + + # Note: acquisition_year itself is no longer updated as per requirements. + + if change_cost or change_start: + if change_cost: + print( + f"Acquisition cost update for {assetnum}: {eq.acquisition_cost} -> {total_cost} " + f"(Adj. Initial: {adj_initial_cost} + Adj. Replacement: {adj_replace_cost} | Rate: {inflation_rate})" + ) + eq.acquisition_cost = total_cost + + if change_start: + print(f"Aligning forecasting_start_year to 2015 for {assetnum}") + eq.forecasting_start_year = 2015 + # If target was default, we update it to 2015 + design_life + if is_valid_default and current_life: + eq.forecasting_target_year = 2015 + current_life + + await db_session.commit() + updates_performed = True return updates_performed 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 11b06a39bc62d0850ec148e6fcba929eb903acad..ca6f66e1b503b73a760ce3447ad94d05d9481f2e 100644 GIT binary patch delta 3661 zcmc&$Yj9J?72drMNxpj6)?1ck$&&oGVBlv3V>X;{fD}KBc0RjQmgh6`YQOBAibe(gK&}b zSIv0=u8>Yz208dSuFgII-;}OrFJ=hIbhg1$QlGPqXMO>sQ{L+kO48o^=T*!Uz&Cuq zWbOg!Xt~HKu9KRf2NGBFDWjXp^2Iny65|Sqsav6jo21oG{NBP`hmzriw>ah~l-_@F zpOd-7NK5ugEUc04zgDkPR2c#IcCAz~be&}tZDvVz@EBBx=7`5)QVx6+Zyp8PtX*u{=)DcQDp;oXRdi;FBCzs@mVEZZez1$(4EkZX z{!tRjM+K-56`|t&wa9%s|D=-aHgYGgh!1&#Rub`cCw6gAzYT~5Z^1}8DqjiO)R78Q zfqbukATJgoAF5~|X@eS68k$2e@g`v1(fVjD$|EI&?nKQXBdWl^{Se=Y5l(G5*OO8*5UnC1I zmazsITOwmiWo+3a)|oBe?&N^6&u?@)(#~62{Hqa1^ebI~`8D{;RseuDvIqVjE0B0| z?Ra#=Zqd#6TZi(9MaGNd4Kah$xcB%)rEL%SwnZw*(eK@Y(i5Z#se>w%fz*4{0Eo-? zkQbS}7Rw{mVk5GTfa6T024#z?ZjSEWph0{BWzT!bmtx+lLN(%wq&o2}slmMmZJ8?* z$G|F7Lk@%0sB+Xr4-r$-2q&&VTH;%abOhI-s$|4NBTu4g@hS3bK5jS($q7L2 zzE`6-L;gVoOYqrqKTHWp|1-qA&tfEf_IL*KAuFBxX(lWvN)HauA@)-kr0`t|LlnM8 z;UI-Kar(~--~#F6pY<_LAN5pHSViHt6tXBxQ<%Zc=N)hX?mqvCKE0=}Z)bZb><@<9 z>2gh)@iRX$;|(9|wM{C6fe>x~q^`3o7)q=}LW8e-uvRyjMpvZ0JFq)63A?fNLJ_HW zuDT4zs@1svf}yaP^yYK&JqvxB4#0ysFh~AnPGp*1T{mH|$2jAJ(-Y&e<4g#@cVQLW zB|W&10bvMl_@))-e)JF3%WPUVb054c*)RSD7HfdhGw^g=0knoU)*o0Ov-@LWPpmf* z>)1P{iC)u0W18qxUQJBn#;<>THJSmm<{KKvxW*9&N@qq~2MqQh`~7sKHs>=yfOrXH z0E_E}xp3TEIA$)sW-cCRny9FYIU0|*j9N}*zMXZX{aE{O`}x*^^;3Goz=|2GBbHNh zw&&7@u?4HgtWDRfO>wY>(dOJw;1D;F;T)|T%kU0K-@0@FK4}F;cN}OlD@Mgrz2o@} zvHXTOla_0agNHD*Hf{tt1#!TbDiSaZC+yjy!tk=;WfP_4!|O)Bex&)qgvBixKB-nQ zTMc;4Y5?)0%Qn-xYW}j1r=Dt_dKNY*2pL;e^=uV4UI~dkUL~~H zmE#Sj7PIndCY4?_^Tc!2-c(7*O|9{3Zsuk#mELpet$uVJUW#+T}usfCc*^@3l)-)0$N z-{v8;6)d&WXrbF`s=2Lcq_e-Hhk+t+#~^I2WA8XJwpOus%6RIj;;AQE#}aZ^XYBBR zyB?vVg1uV~Db^UpG%%eeh-P-$1Sw`2#d0uRE{F~6^kPVG1{ecoFk==1UUtRGkN|IU8^hRj@z delta 3265 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 YC{JuF9`CN=tr)FlL&O6vDgPt+2gAKmGXMYp diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index 88f1764..d26b38a 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -39,72 +39,6 @@ def get_recursive_query(cursor, assetnum, worktype="CM"): Fungsi untuk menjalankan query rekursif berdasarkan assetnum dan worktype. worktype memiliki nilai default 'CM'. """ - # query = f""" - # SELECT - # ROW_NUMBER() OVER (ORDER BY tbl.assetnum, tbl.year, tbl.worktype) AS seq, - # * - # FROM ( - # SELECT - # a.worktype, - # a.assetnum, - # EXTRACT(YEAR FROM a.reportdate) AS year, - # COUNT(a.wonum) AS raw_corrective_failure_interval, - # SUM(a.total_cost_max) AS raw_corrective_material_cost, - # ROUND( - # SUM( - # EXTRACT(EPOCH FROM ( - # a.actfinish - - # a.actstart - # )) - # ) / 3600 - # , 2) AS raw_corrective_labor_time_jam, - # SUM(a.jumlah_labor) AS raw_corrective_labor_technician - # FROM - # public.wo_staging_3 AS a - # WHERE - # a.unit = '3' - # GROUP BY - # a.worktype, - # a.assetnum, - # EXTRACT(YEAR FROM a.reportdate) - # ) AS tbl - # WHERE - # tbl.worktype = '{worktype}' - # AND tbl.assetnum = '{assetnum}' - # ORDER BY - # tbl.assetnum, - # tbl.year, - # tbl.worktype - # """ -# query = f""" -# select d.tahun, SUM(d.actmatcost) AS raw_corrective_material_cost, sum(d.man_hour) as man_hour_peryear from -# ( -# SELECT -# a.wonum, -# a.actmatcost, -# DATE_PART('year', a.reportdate) AS tahun, -# ( -# ROUND(SUM(EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600), 2) -# ) AS man_hour, -# CASE -# WHEN COUNT(b.laborcode) = 0 THEN 3 -# ELSE COUNT(b.laborcode) -# END AS man_count -# FROM public.wo_maximo AS a -# LEFT JOIN public.wo_maximo_labtrans AS b -# ON b.wonum = a.wonum -# WHERE -# a.asset_unit = '3' -# AND a.worktype = '{worktype}' -# AND a.asset_assetnum = '{assetnum}' -# and a.wonum not like 'T%' -# GROUP BY -# a.wonum, -# a.actmatcost, -# DATE_PART('year', a.reportdate) -# ) as d group by d.tahun -# ; -# """ where_query = get_where_query_sql(assetnum, worktype) query = f""" @@ -360,48 +294,11 @@ def _build_tr_row_values( ) rc_cm_material_cost = raw_cm_material_cost_total - # rc_cm_labor_cost = ( - # data_cm_row.get("raw_cm_labor_time") - # * data_cm_row.get("rc_cm_labor_human") - # * man_hour_value - # if data_cm_row - # and data_cm_row.get("rc_cm_labor_cost") - # and data_cm_row.get("rc_cm_labor_human") - # and man_hour_value is not None - # else 0 - # ) rc_pm_material_cost = raw_pm_material_cost - # rc_pm_labor_cost = ( - # data_pm_row.get("raw_pm_labor_time") - # * data_pm_row.get("rc_pm_labor_human") - # * man_hour_value - # if data_pm_row - # and data_pm_row.get("rc_pm_labor_cost") - # and data_pm_row.get("rc_pm_labor_human") - # and man_hour_value is not None - # else 0 - # ) rc_oh_material_cost = raw_oh_material_cost - # rc_oh_labor_cost = ( - # data_oh_row.get("raw_oh_labor_time") - # * data_oh_row.get("rc_oh_labor_human") - # * man_hour_value - # if data_oh_row - # and data_oh_row.get("rc_oh_labor_cost") - # and data_oh_row.get("rc_oh_labor_human") - # and man_hour_value is not None - # else 0 - # ) - - # rc_predictive_labor_cost = ( - # data_predictive_row.get("raw_predictive_labor_human") * man_hour_value - # if data_predictive_row - # and data_predictive_row.get("rc_predictive_labor_cost") - # and man_hour_value is not None - # else 0 - # ) + if labour_cost_lookup and year is not None: cm_lookup = labour_cost_lookup.get("CM", {}) @@ -987,18 +884,14 @@ async def query_data(target_assetnum: str = None): print(f"Error checking acquisition data for {assetnum}: {exc}") - forecasting_start_year_db = row.get("forecasting_start_year") - acquisition_year = row.get("acquisition_year") + # Calculation start is always 2014 (forecasting start is 2015) + # Forecasting and calculation start configuration + loop_start_year = 2014 + + # Delete data before calculation start (2014) + cursor.execute("DELETE FROM lcc_equipment_tr_data WHERE assetnum = %s AND tahun < %s", (assetnum, loop_start_year)) - 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 - elif forecasting_start_year_db: - # If no acquisition_year but forecasting_start_year defined in DB - forecasting_start_year = forecasting_start_year_db - else: - forecasting_start_year = 2014 + forecasting_start_year = loop_start_year asset_start = datetime.now() processed_assets += 1 @@ -1024,6 +917,18 @@ async def query_data(target_assetnum: str = None): "OH": get_labour_cost_totals(cursor_wo, assetnum, "OH"), } + # Find first year with replace_cost > 0 in Maximo (Requirement: ignore costs in this year) + cursor_wo.execute(""" + 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; + """, (assetnum,)) + res_rep = cursor_wo.fetchone() + first_rep_year = int(res_rep[0]) if res_rep else None + seq = 0 # Looping untuk setiap tahun for year in range(forecasting_start_year, current_year + 1): @@ -1074,6 +979,23 @@ async def query_data(target_assetnum: str = None): year=year, labour_cost_lookup=labour_cost_lookup, ) + + # Requirement: At the first year of the replace cost detected > 0, + # The material cost/ labor cost is ignored. + if first_rep_year and year == first_rep_year: + cost_keys = [ + "raw_cm_material_cost", "raw_cm_labor_time", + "raw_pm_material_cost", "raw_pm_labor_time", + "raw_oh_material_cost", "raw_oh_labor_time", + "raw_predictive_material_cost", "raw_predictive_labor_time", + "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" + ] + for k in cost_keys: + if k in row_values: + row_values[k] = 0.0 if not data_exists: cursor.execute( insert_query, diff --git a/src/modules/equipment/run.py b/src/modules/equipment/run.py index 4c7637d..6795c94 100644 --- a/src/modules/equipment/run.py +++ b/src/modules/equipment/run.py @@ -20,12 +20,12 @@ def format_execution_time(execution_time): return f"{execution_time:.2f} seconds." # Alternative calling function to just predict and calculate eac without inserting actual data -async def simulate(): +async def simulate(assetnum: str = None): start_time = time.time() - print("Starting simulation (predict + eac)...") + print(f"Starting simulation (predict + eac) {'for ' + assetnum if assetnum else 'for all assets'}...") try: - prediction_result = await predict_run() + prediction_result = await predict_run(assetnum=assetnum) if prediction_result is False: print("Prediction step failed or was skipped. Skipping EAC run.") return @@ -34,7 +34,7 @@ async def simulate(): return try: - result = eac_run() + result = eac_run(assetnum=assetnum) if asyncio.iscoroutine(result): result = await result print("EAC run completed.") @@ -48,17 +48,18 @@ async def simulate(): return message # Panggil fungsi -async def main(): +async def main(assetnum: str = None): start_time = time.time() + print(f"Starting calculation workflow {'for ' + assetnum if assetnum else 'for all assets'}...") try: - await query_data() + await query_data(target_assetnum=assetnum) except Exception as e: print(f"Error in query_data: {str(e)}") return try: - prediction_result = await predict_run() + prediction_result = await predict_run(assetnum=assetnum) if prediction_result is False: print("Prediction step failed or was skipped. Skipping EAC run.") return @@ -67,7 +68,7 @@ async def main(): return try: - result = eac_run() + result = eac_run(assetnum=assetnum) if asyncio.iscoroutine(result): result = await result print("EAC run completed.") @@ -81,9 +82,14 @@ async def main(): return message if __name__ == "__main__": - import sys - # Use 'simulate' argument to run without query_data - if len(sys.argv) > 1 and sys.argv[1] == "simulate": - asyncio.run(simulate()) + import argparse + parser = argparse.ArgumentParser(description="Run LCCA Simulation") + parser.add_argument("mode", nargs="?", choices=["main", "simulate"], default="main", help="Mode to run: 'main' (full) or 'simulate' (no data refresh)") + parser.add_argument("--assetnum", type=str, help="Specific asset number to process") + + args = parser.parse_args() + + if args.mode == "simulate": + asyncio.run(simulate(assetnum=args.assetnum)) else: - asyncio.run(main()) + asyncio.run(main(assetnum=args.assetnum))