diff --git a/src/modules/equipment/Eac.py b/src/modules/equipment/Eac.py index cf7cbdd..c26b432 100644 --- a/src/modules/equipment/Eac.py +++ b/src/modules/equipment/Eac.py @@ -83,7 +83,12 @@ class Eac: # dimana PV = final_value, r = history_future_inflation_rate, n = row["seq"] pmt_mnt_cost = -npf.pmt(history_future_inflation_rate, row["seq"], final_value) - eac_disposal_cost = 0.07 * pmt_mnt_cost + # Menghitung PMT biaya disposal + # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] + # dimana PV = 0.05 * rc_total_cost_0, r = disc_rate, n = row["seq"] + # rc_total_cost_0 adalah biaya akuisisi awal (seq = 0) + eac_disposal_cost = -npf.pmt(disc_rate, row["seq"], 0, 0.05 * rc_total_cost_0) if row["seq"] > 0 else 0.0 + # Menghitung PMT biaya akuisisi # Rumus PMT: PMT = PV * [r(1 + r)^n] / [(1 + r)^n – 1] # dimana PV = rc_total_cost_0, r = disc_rate, n = row["seq"] diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index e1b4782..58c9a48 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -394,8 +394,8 @@ class Prediksi: part2 = max(0.0, (dmn - ens)) * extra_fuel asset_criticality = part1 + part2 - efdh = _f("asset_crit_efdh_equivalent_forced_derated_hours") # EFDH per Year - foh = _f("asset_crit_foh_forced_outage_hours") # FOH per Year + # efdh = _f("asset_crit_efdh_equivalent_forced_derated_hours") # EFDH per Year + # foh = _f("asset_crit_foh_forced_outage_hours") # FOH per Year query_each_equipment = """ SELECT @@ -431,6 +431,113 @@ class Prediksi: if connection: connection.close() + # Fungsi untuk mengambil labour cost + def __get_labour_cost(self, equipment_id, worktype, existing_connection=None): + """Return labour cost totals per tahun for the given worktype.""" + labour_costs = {} + connection = existing_connection + cursor = None + close_connection = False + + try: + if connection is None: + connections = get_production_connection() + connection = connections[0] if isinstance(connections, tuple) else connections + close_connection = True + + if connection is None: + print("Database connection failed.") + return labour_costs + + cursor = connection.cursor() + + worktype_upper = (worktype or "").upper() + worktype_condition = "AND a.worktype IN ('CM', 'PROACTIVE', 'WA')" + params = [equipment_id] + + if worktype_upper != "CM": + worktype_condition = "AND a.worktype = %s" + params.append(worktype_upper) + + exclude_condition = "AND a.wojp8 != 'S1'" if worktype_upper == "CM" else "" + + query = f""" + SELECT + EXTRACT(YEAR FROM x.reportdate)::int AS tahun, + SUM(x.upah_per_wonum) AS total_upah_per_tahun + FROM ( + SELECT + bw.wonum, + bw.reportdate, + bw.jumlah_jam_kerja, + CASE + WHEN COUNT(b.laborcode) FILTER (WHERE b.laborcode IS NOT NULL) > 0 THEN + SUM( + COALESCE(emp_cost.salary_per_hour_idr, + (SELECT salary_per_hour_idr FROM lcc_manpower_cost WHERE UPPER(staff_job_level) = UPPER('Junior') LIMIT 1)) + * bw.jumlah_jam_kerja + ) + ELSE + 3 * (SELECT salary_per_hour_idr FROM lcc_manpower_cost WHERE UPPER(staff_job_level) = UPPER('Junior') LIMIT 1) * bw.jumlah_jam_kerja + END AS upah_per_wonum + FROM ( + SELECT + a.wonum, + a.reportdate, + CASE + WHEN (EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600.0) = 0 + THEN 1 + ELSE (EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600.0) + END AS jumlah_jam_kerja + FROM public.wo_maximo a + WHERE + a.asset_unit = '3' + AND a.wonum NOT LIKE 'T%' + AND a.asset_assetnum = %s + AND (EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600.0) <= 730 + {worktype_condition} + {exclude_condition} + ) bw + LEFT JOIN public.wo_maximo_labtrans b ON b.wonum = bw.wonum + LEFT JOIN lcc_ms_manpower emp ON UPPER(TRIM(emp."ID Number")) = UPPER(TRIM(b.laborcode)) + LEFT JOIN lcc_manpower_cost emp_cost ON UPPER(TRIM(emp_cost.staff_job_level)) = UPPER(TRIM(emp."Position")) + GROUP BY bw.wonum, bw.reportdate, bw.jumlah_jam_kerja + ) x + GROUP BY 1 + ORDER BY 1; + """ + + cursor.execute(query, tuple(params)) + rows = cursor.fetchall() + for tahun, total in rows: + try: + year_int = int(tahun) if tahun is not None else None + except (TypeError, ValueError): + year_int = None + if year_int is None: + continue + try: + labour_costs[year_int] = float(total) if total is not None else 0.0 + except (TypeError, ValueError): + labour_costs[year_int] = 0.0 + + return labour_costs + except Exception as e: + print(f"Error saat mendapatkan labour cost dari database: {e}") + return labour_costs + finally: + if cursor: + try: + cursor.close() + except Exception: + pass + if close_connection and connection: + try: + connection.close() + except Exception: + pass + + # Fungsi untuk menghapus data proyeksi pada tahun tertentu def __update_data_lcc(self, equipment_id): try: @@ -448,6 +555,17 @@ class Prediksi: cursor = connection.cursor(cursor_factory=DictCursor) + labour_cost_maps = { + "CM": self.__get_labour_cost(equipment_id, "CM", production_connection), + "PM": self.__get_labour_cost(equipment_id, "PM", production_connection), + "PDM": self.__get_labour_cost(equipment_id, "PDM", production_connection), + "OH": self.__get_labour_cost(equipment_id, "OH", production_connection), + } + cm_labour_costs = labour_cost_maps.get("CM", {}) or {} + pm_labour_costs = labour_cost_maps.get("PM", {}) or {} + pdm_labour_costs = labour_cost_maps.get("PDM", {}) or {} + oh_labour_costs = labour_cost_maps.get("OH", {}) or {} + # Ambil semua baris untuk assetnum select_q = ''' SELECT id, seq, tahun, @@ -493,9 +611,6 @@ class Prediksi: rc_predictive_labor_cost = %s, rc_oh_material_cost = %s, rc_oh_labor_cost = %s, - rc_lost_cost = %s, - rc_operation_cost = %s, - rc_maintenance_cost = %s, rc_total_cost = %s, updated_by = 'Sys', updated_at = NOW() WHERE id = %s; @@ -506,6 +621,8 @@ class Prediksi: yr = r.get("tahun") if isinstance(r, dict) else r[2] man_hour = _get_man_hour_for_year(yr) + seq = int(r.get("seq") or 0) if isinstance(r, dict) else int(r[1] or 0) + raw_cm_interval = float(r.get("raw_cm_interval") or 0.0) raw_cm_labor_time = float(r.get("raw_cm_labor_time") or 0.0) raw_cm_labor_human = float(r.get("raw_cm_labor_human") or 0.0) @@ -525,11 +642,11 @@ class Prediksi: raw_oh_labor_time = float(r.get("raw_oh_labor_time") or 0.0) raw_oh_labor_human = float(r.get("raw_oh_labor_human") or 0.0) - raw_loss_output_mw = float(r.get("raw_loss_output_mw") or 0.0) - raw_loss_output_price = float(r.get("raw_loss_output_price") or 0.0) + # raw_loss_output_mw = float(r.get("raw_loss_output_mw") or 0.0) + # raw_loss_output_price = float(r.get("raw_loss_output_price") or 0.0) - raw_operational_cost = float(r.get("raw_operational_cost") or 0.0) - raw_maintenance_cost = float(r.get("raw_maintenance_cost") or 0.0) + # raw_operational_cost = float(r.get("raw_operational_cost") or 0.0) + # raw_maintenance_cost = float(r.get("raw_maintenance_cost") or 0.0) efdh_equivalent_forced_derated_hours = float(r.get("efdh_equivalent_forced_derated_hours") or 0.0) foh_forced_outage_hours = float(r.get("foh_forced_outage_hours") or 0.0) @@ -538,6 +655,12 @@ class Prediksi: # compute per-column costs using helpers rc_cm_material = rc_cm_material_cost rc_cm_labor = rc_labor_cost(raw_cm_interval, raw_cm_labor_time, raw_cm_labor_human, man_hour) + cm_labor_total = cm_labour_costs.get(yr) + if cm_labor_total is not None: + try: + rc_cm_labor = float(cm_labor_total) + except (TypeError, ValueError): + rc_cm_labor = 0.0 try: # if np.isfinite(raw_pm_interval) and raw_pm_interval != 0: @@ -547,6 +670,12 @@ class Prediksi: except Exception: rc_pm_material = 0.0 rc_pm_labor = rc_labor_cost(raw_pm_interval, raw_pm_labor_time, raw_pm_labor_human, man_hour) + pm_labor_total = pm_labour_costs.get(yr) + if pm_labor_total is not None: + try: + rc_pm_labor = float(pm_labor_total) + except (TypeError, ValueError): + rc_pm_labor = 0.0 try: # if np.isfinite(raw_predictive_interval) and raw_predictive_interval != 0: @@ -561,9 +690,21 @@ class Prediksi: rc_predictive_labor = rc_labor_cost(raw_predictive_interval, raw_predictive_labor_time, raw_predictive_labor_human, man_hour) except Exception: rc_predictive_labor = 0.0 + predictive_labor_total = pdm_labour_costs.get(yr) + if predictive_labor_total is not None: + try: + rc_predictive_labor = float(predictive_labor_total) + except (TypeError, ValueError): + rc_predictive_labor = 0.0 rc_oh_material = raw_oh_material_cost rc_oh_labor = rc_labor_cost(raw_oh_interval, raw_oh_labor_time, raw_oh_labor_human, man_hour) + oh_labor_total = oh_labour_costs.get(yr) + if oh_labor_total is not None: + try: + rc_oh_labor = float(oh_labor_total) + except (TypeError, ValueError): + rc_oh_labor = 0.0 # rc_lost = rc_lost_cost(raw_loss_output_mw, raw_loss_output_price, raw_cm_interval) diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index 0a06a9e..3147471 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -145,6 +145,89 @@ def get_data_tahun(cursor): return cursor.fetchall() +def get_labour_cost_totals(cursor, assetnum: str, worktype: str) -> dict: + """Return yearly labor cost totals for a worktype using the standardized query.""" + if not assetnum or not worktype: + return {} + + worktype_condition = "AND a.worktype IN ('CM', 'PROACTIVE', 'WA')" + worktype_params = [assetnum] + + if worktype.upper() != "CM": + worktype_condition = "AND a.worktype = %s" + worktype_params.append(worktype.upper()) + + exclude_condition = "AND a.wojp8 != 'S1'" if worktype.upper() == "CM" else "" + + query = f""" + SELECT + EXTRACT(YEAR FROM x.reportdate)::int AS tahun, + SUM(x.upah_per_wonum) AS total_upah_per_tahun + FROM ( + SELECT + bw.wonum, + bw.reportdate, + bw.jumlah_jam_kerja, + CASE + WHEN COUNT(b.laborcode) FILTER (WHERE b.laborcode IS NOT NULL) > 0 THEN + SUM( + COALESCE(emp_cost.salary_per_hour_idr, + (SELECT salary_per_hour_idr FROM lcc_manpower_cost WHERE UPPER(staff_job_level) = UPPER('Junior') LIMIT 1)) + * bw.jumlah_jam_kerja + ) + ELSE + 3 * (SELECT salary_per_hour_idr FROM lcc_manpower_cost WHERE UPPER(staff_job_level) = UPPER('Junior') LIMIT 1) * bw.jumlah_jam_kerja + END AS upah_per_wonum + FROM ( + SELECT + a.wonum, + a.reportdate, + CASE + WHEN (EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600.0) = 0 + THEN 1 + ELSE (EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600.0) + END AS jumlah_jam_kerja + FROM public.wo_maximo a + WHERE + a.asset_unit = '3' + AND a.wonum NOT LIKE 'T%' + AND a.asset_assetnum = %s + AND (EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600.0) <= 730 + {worktype_condition} + {exclude_condition} + ) bw + LEFT JOIN public.wo_maximo_labtrans b ON b.wonum = bw.wonum + LEFT JOIN lcc_ms_manpower emp ON UPPER(TRIM(emp."ID Number")) = UPPER(TRIM(b.laborcode)) + LEFT JOIN lcc_manpower_cost emp_cost ON UPPER(TRIM(emp_cost.staff_job_level)) = UPPER(TRIM(emp."Position")) + GROUP BY bw.wonum, bw.reportdate, bw.jumlah_jam_kerja + ) x + GROUP BY 1 + ORDER BY 1; + """ + + try: + cursor.execute(query, tuple(worktype_params)) + rows = cursor.fetchall() + except Exception as exc: + print(f"Error fetching labour cost for {assetnum} ({worktype}): {exc}") + return {} + + labour_costs = {} + for tahun, total in rows: + try: + year_int = int(tahun) if tahun is not None else None + except (TypeError, ValueError): + year_int = None + if year_int is None: + continue + try: + labour_costs[year_int] = float(total) if total is not None else 0.0 + except (TypeError, ValueError): + labour_costs[year_int] = 0.0 + + return labour_costs + + def _parse_decimal(value: str, decimal_separator: str = ".") -> Decimal: """Parse numeric strings that may use comma decimal separators.""" if value is None: @@ -215,6 +298,8 @@ def _build_tr_row_values( data_oh_row, data_predictive_row, data_tahunan_row, + year=None, + labour_cost_lookup=None, ): """Return sanitized numeric values for equipment transaction rows.""" @@ -347,6 +432,38 @@ def _build_tr_row_values( else 0 ) + if labour_cost_lookup and year is not None: + cm_lookup = labour_cost_lookup.get("CM", {}) + pm_lookup = labour_cost_lookup.get("PM", {}) + oh_lookup = labour_cost_lookup.get("OH", {}) + pdm_lookup = labour_cost_lookup.get("PDM", {}) + + cm_value = cm_lookup.get(year) + pm_value = pm_lookup.get(year) + oh_value = oh_lookup.get(year) + pdm_value = pdm_lookup.get(year) + + if cm_value is not None: + try: + rc_cm_labor_cost = float(cm_value) + except (TypeError, ValueError): + rc_cm_labor_cost = 0.0 + if pm_value is not None: + try: + rc_pm_labor_cost = float(pm_value) + except (TypeError, ValueError): + rc_pm_labor_cost = 0.0 + if oh_value is not None: + try: + rc_oh_labor_cost = float(oh_value) + except (TypeError, ValueError): + rc_oh_labor_cost = 0.0 + if pdm_value is not None: + try: + rc_predictive_labor_cost = float(pdm_value) + except (TypeError, ValueError): + rc_predictive_labor_cost = 0.0 + return { "raw_cm_interval": raw_cm_interval, "raw_cm_material_cost": raw_cm_material_cost, @@ -914,6 +1031,13 @@ async def query_data(): # Data Tahun data_tahunan = get_data_tahun(cursor) + labour_cost_lookup = { + "CM": get_labour_cost_totals(cursor_wo, assetnum, "CM"), + "PM": get_labour_cost_totals(cursor_wo, assetnum, "PM"), + "PDM": get_labour_cost_totals(cursor_wo, assetnum, "PDM"), + "OH": get_labour_cost_totals(cursor_wo, assetnum, "OH"), + } + seq = 0 # Looping untuk setiap tahun for year in range(forecasting_start_year, current_year + 1): @@ -961,6 +1085,8 @@ async def query_data(): data_oh_row, data_predictive_row, data_tahunan_row, + year=year, + labour_cost_lookup=labour_cost_lookup, ) if not data_exists: diff --git a/src/modules/plant/run2.py b/src/modules/plant/run2.py index cb95529..b88743a 100644 --- a/src/modules/plant/run2.py +++ b/src/modules/plant/run2.py @@ -396,7 +396,8 @@ def main(): + cost_a_pm # + cost_a_pinjaman # + cost_a_depreciation - ) + ) + else: cost_a_replacement = 0 cost_a_pm = 0 @@ -412,11 +413,15 @@ def main(): chart_capex_component_a = cost_a_acquisition chart_capex_annualized = cost_a_annualized + + cost_disposal_cost = -npf.pmt(discount_rate, seq, 0, 0.05 * total_project_cost) else: chart_capex_component_a = total_project_cost chart_capex_annualized = 0 cost_a_pv = 0 cost_a_annualized = 0 + cost_disposal_cost = 0 + chart_capex_biaya_investasi_tambahan = 0 chart_capex_acquisition_cost = 0 @@ -501,7 +506,7 @@ def main(): calc4_free_cash_flow_on_equity_array.append(calc4_free_cash_flow_on_equity) calc4_discounted_fcf_on_equity = hitung_pv(wacc_on_equity, seq, calc4_free_cash_flow_on_equity) - cost_disposal_cost = -npf.pmt(discount_rate, seq, cost_a_acquisition) if seq > 0 else 0 + row_params = ( net_capacity_factor,