You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

434 lines
16 KiB
Python

import os
from sqlalchemy import Select, Delete, Float, func, cast, String
from sqlalchemy.orm import selectinload
from src.database.service import search_filter_sort_paginate
from src.equipment.model import Equipment, EquipmentTransactionRecords
from src.yeardata.model import Yeardata
from ..equipment_master.model import EquipmentMaster
from .schema import EquipmentCreate, EquipmentUpdate, MasterBase
from typing import Optional
from src.database.core import DbSession
from src.auth.service import CurrentUser
from src.config import RELIABILITY_APP_URL
import httpx
from src.modules.equipment.run import main
import datetime
import math
from sqlalchemy import text
async def get_master_by_assetnum(
*, db_session: DbSession, assetnum: str
) -> tuple[list[EquipmentTransactionRecords], float | None]:
"""Returns master records with equipment data based on asset number."""
# First query to get equipment record
equipment_master_query = Select(EquipmentMaster).filter(
EquipmentMaster.assetnum == assetnum
)
equipment_master_result = await db_session.execute(equipment_master_query)
equipment_master_record = equipment_master_result.scalars().one_or_none()
equipment_query = Select(Equipment).filter(Equipment.assetnum == assetnum)
equipment_result = await db_session.execute(equipment_query)
equipment_record = equipment_result.scalars().one_or_none()
# Second query to get master records
master_query = (
Select(EquipmentTransactionRecords)
.join(EquipmentTransactionRecords.equipment)
.options(selectinload(EquipmentTransactionRecords.equipment))
.filter(Equipment.assetnum == assetnum)
.order_by(EquipmentTransactionRecords.tahun.asc())
)
master_result = await db_session.execute(master_query)
records = master_result.scalars().all()
# Get all yeardata
yeardata_query = Select(Yeardata)
yeardata_result = await db_session.execute(yeardata_query)
yeardata_records = yeardata_result.scalars().all()
yeardata_dict = {y.year: y for y in yeardata_records}
# Compute asset criticality per record/year and attach to each record.
# Use safe attribute access for Yeardata; if a value or attribute is missing,
# fall back to 0 so N/A data does not raise AttributeError.
for record in records:
year = record.tahun
y = yeardata_dict.get(year)
asset_crit_ens_energy_not_served = (
getattr(y, "asset_crit_ens_energy_not_served", 0) if y is not None else 0
)
asset_crit_bpp_system = (
getattr(y, "asset_crit_bpp_system", 0) if y is not None else 0
)
asset_crit_bpp_pembangkit = (
getattr(y, "asset_crit_bpp_pembangkit", 0) if y is not None else 0
)
asset_crit_marginal_cost = (
getattr(y, "asset_crit_marginal_cost", 0) if y is not None else 0
)
asset_crit_dmn_daya_mampu_netto = (
getattr(y, "asset_crit_dmn_daya_mampu_netto", 0) if y is not None else 0
)
# Convert to floats and compute criticality safely
ens = float(asset_crit_ens_energy_not_served or 0)
bpp_system = float(asset_crit_bpp_system or 0)
bpp_pembangkit = float(asset_crit_bpp_pembangkit or 0)
marginal_cost = float(asset_crit_marginal_cost or 0)
dmn = float(asset_crit_dmn_daya_mampu_netto or 0)
extra_fuel_cost = marginal_cost - bpp_pembangkit
asset_criticality = ens * (0.07 * bpp_system) + (dmn - ens * extra_fuel_cost)
# if NaN or None, return 0
if asset_criticality is None or (isinstance(asset_criticality, float) and math.isnan(asset_criticality)):
asset_criticality = 0.0
setattr(record, "asset_criticality", asset_criticality)
# Get the last actual year
last_actual_year_query = (
Select(func.max(EquipmentTransactionRecords.tahun))
.join(EquipmentTransactionRecords.equipment)
.filter(Equipment.assetnum == assetnum)
.filter(EquipmentTransactionRecords.is_actual == 1)
)
last_actual_year_result = await db_session.execute(last_actual_year_query)
last_actual_year = last_actual_year_result.scalar()
# Third query specifically for minimum eac_eac
min_query = (
Select(func.min(func.cast(EquipmentTransactionRecords.eac_eac, Float)), EquipmentTransactionRecords.seq)
.join(EquipmentTransactionRecords.equipment)
.filter(Equipment.assetnum == assetnum)
.group_by(EquipmentTransactionRecords.seq)
.order_by(func.min(func.cast(EquipmentTransactionRecords.eac_eac, Float)))
.limit(1)
)
min_result = await db_session.execute(min_query)
min_record = min_result.first()
min_eac_value = (
float(min_record[0]) if min_record and min_record[0] is not None else None
)
min_seq = min_record[1] if min_record else None
return (
equipment_master_record,
equipment_record,
records,
min_eac_value,
min_seq,
last_actual_year,
)
# return result.scalars().all()
async def get_maximo_by_assetnum(*, db_session: DbSession, assetnum: str) -> Optional[MasterBase]:
"""Returns a document based on the given document id."""
# default worktype; change if you need a different filtering
# worktype = "CM"
# where_worktype = (
# "AND a.worktype in ('CM', 'PROACTIVE', 'WA')"
# if worktype == "CM"
# else "AND a.worktype = :worktype"
# )
# where_wojp8 = "AND a.wojp8 != 'S1'" if worktype == "CM" else ""
# sql = f"""
# SELECT
# DATE_PART('year', a.reportdate) AS tahun,
# COUNT(a.wonum) AS raw_{worktype.lower()}_interval,
# SUM(a.actmatcost) AS raw_{worktype.lower()}_material_cost,
# ROUND(SUM(EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600), 2) AS raw_{worktype.lower()}_labor_time,
# CASE WHEN COUNT(b.laborcode) = 0 THEN 3 ELSE COUNT(b.laborcode) END AS raw_{worktype.lower()}_labor_human
# FROM public.wo_maximo AS a
# LEFT JOIN public.wo_maximo_labtrans AS b ON b.wonum = a.wonum
# WHERE a.asset_unit = '3'
# {where_worktype}
# AND a.asset_assetnum = :assetnum
# AND a.wonum NOT LIKE 'T%'
# {where_wojp8}
# GROUP BY DATE_PART('year', a.reportdate);
# """
sql = f"""
SELECT
*
FROM public.wo_maximo AS a
WHERE a.asset_unit = '3'
AND a.asset_assetnum = '{assetnum}'
AND a.wonum NOT LIKE 'T%'
"""
query = text(sql)
# Pass parameters to execute to avoid bindparam/name mismatches
result = await db_session.execute(query)
record = result.mappings().all()
if record:
return record
return None
async def get_by_assetnum(*, db_session: DbSession, assetnum: str) -> Optional[Equipment]:
"""Returns a document based on the given document id."""
print("assetnum service:", assetnum)
query = Select(Equipment).filter(Equipment.assetnum == assetnum)
result = await db_session.execute(query)
return result.scalars().one_or_none()
async def get_by_id(*, db_session: DbSession, equipment_id: str) -> Optional[Equipment]:
"""Returns a document based on the given document id."""
query = Select(Equipment).filter(Equipment.id == equipment_id)
result = await db_session.execute(query)
return result.scalars().one_or_none()
async def get_all(
*, db_session: DbSession, items_per_page: int, search: str = None, common
) -> list[Equipment]:
"""Returns all documents."""
query = (
Select(Equipment)
.join(EquipmentMaster, Equipment.assetnum == EquipmentMaster.assetnum)
.options(selectinload(Equipment.equipment_master))
)
if search:
query = query.filter(
cast(Equipment.acquisition_year, String).ilike(f"%{search}%")
| cast(Equipment.assetnum, String).ilike(f"%{search}%")
| cast(EquipmentMaster.name, String).ilike(f"%{search}%")
)
common["items_per_page"] = items_per_page
result = await search_filter_sort_paginate(model=query, **common)
return result
async def get_top_10_economic_life(*, db_session: DbSession) -> list[Equipment]:
"""Returns top 10 economic life."""
query = (
Select(Equipment)
.join(EquipmentMaster, Equipment.assetnum == EquipmentMaster.assetnum)
.options(selectinload(Equipment.equipment_master))
)
current_year = datetime.datetime.now().year
query = (
query.add_columns(
func.abs(current_year - Equipment.minimum_eac_year).label("economic_life")
)
.filter(Equipment.minimum_eac_year != None)
.order_by(func.abs(current_year - Equipment.minimum_eac_year).desc())
.limit(10)
)
result = await db_session.execute(query)
equipment_list = []
for row in result.all():
equipment = row[0]
equipment.economic_life = row[1]
equipment_list.append(equipment)
return equipment_list
async def get_top_10_replacement_priorities(*, db_session: DbSession) -> list[Equipment]:
"""Returns top 10 replacement priorities."""
query = (
Select(Equipment)
.join(EquipmentMaster, Equipment.assetnum == EquipmentMaster.assetnum)
.options(selectinload(Equipment.equipment_master))
)
current_year = datetime.datetime.now().year
query = (
query.add_columns(
func.abs(current_year - Equipment.minimum_eac_year).label("economic_life")
)
.filter(Equipment.minimum_eac_year != None)
.order_by(func.abs(current_year - Equipment.minimum_eac_year).asc())
.order_by(func.abs(Equipment.minimum_eac).desc())
.limit(10)
)
result = await db_session.execute(query)
equipment_list = []
for row in result.all():
equipment = row[0]
equipment.economic_life = row[1]
equipment_list.append(equipment)
return equipment_list
async def generate_all_transaction(*, db_session: DbSession, token):
"""Generate transaction for all equipments in the database based on equipments assetnum."""
query = Select(Equipment)
query_result = await db_session.execute(query)
equipments = query_result.scalars().all()
for equipment in equipments:
await main(equipment.assetnum, token, RELIABILITY_APP_URL)
return [equipment.assetnum for equipment in equipments]
async def generate_transaction(
*, db_session: DbSession, data_in: EquipmentCreate, token
):
# Delete all existing master records for this asset number and prediction data
query = (
Delete(EquipmentTransactionRecords)
.where(EquipmentTransactionRecords.assetnum == data_in.assetnum)
.where(EquipmentTransactionRecords.is_actual == 0)
)
await db_session.execute(query)
await db_session.commit()
"""Generate transaction for equipment."""
prediction = await main(data_in.assetnum, token, RELIABILITY_APP_URL)
# # Fetch data from external API
# async def fetch_api_data(assetnum: str, year: int) -> dict:
# async with httpx.AsyncClient() as client:
# try:
# response = await client.get(
# f"{os.environ.get('RELIABILITY_APP_URL')}/main/number-of-failures/{assetnum}/{year}/{year}",
# timeout=30.0,
# headers={"Authorization": f"Bearer {token}"},
# )
# response.raise_for_status()
# return response.json()
# except httpx.HTTPError as e:
# print(f"HTTP error occurred: {e}")
# return {}
# # Initialize base transaction with default values
# base_transaction = {
# "assetnum": data_in.assetnum,
# "is_actual": 0,
# "raw_cm_interval": None,
# "raw_cm_material_cost": None,
# "raw_cm_labor_time": None,
# "raw_cm_labor_human": None,
# "raw_pm_interval": None,
# "raw_pm_material_cost": None,
# "raw_pm_labor_time": None,
# "raw_pm_labor_human": None,
# "raw_predictive_labor_time": None,
# "raw_predictive_labor_human": None,
# "raw_oh_material_cost": None,
# "raw_oh_labor_time": None,
# "raw_oh_labor_human": None,
# "raw_project_task_material_cost": None,
# "raw_loss_output_MW": None,
# "raw_loss_output_price": None,
# "raw_operational_cost": None,
# "raw_maintenance_cost": None,
# "rc_cm_material_cost": None,
# "rc_cm_labor_cost": None,
# "rc_pm_material_cost": None,
# "rc_pm_labor_cost": None,
# "rc_predictive_labor_cost": None,
# "rc_oh_material_cost": None,
# "rc_oh_labor_cost": None,
# "rc_project_material_cost": None,
# "rc_lost_cost": None,
# "rc_operation_cost": None,
# "rc_maintenance_cost": None,
# "rc_total_cost": None,
# "eac_npv": None,
# "eac_annual_mnt_cost": None,
# "eac_annual_acq_cost": None,
# "eac_eac": None,
# }
# transactions = []
# # Query existing records with is_actual=1
# actual_years_query = (
# Select(EquipmentTransactionRecords.tahun)
# .filter(EquipmentTransactionRecords.assetnum == data_in.assetnum)
# .filter(EquipmentTransactionRecords.is_actual == 1)
# )
# result = await db_session.execute(actual_years_query)
# actual_years = {row[0] for row in result.all()}
# for sequence, year in enumerate(
# range(data_in.acquisition_year - 1, data_in.forecasting_target_year + 1), 0
# ):
# # Skip if year already has actual data
# if year in actual_years:
# continue
# transaction = base_transaction.copy()
# # Update values from API
# api_data = await fetch_api_data(data_in.assetnum, year)
# if api_data:
# # # Get current num_fail
# current_num_fail = api_data["data"][0]["num_fail"]
# # # Calculate sum of previous failures for this asset
# # previous_failures_query = (
# # Select(func.sum(EquipmentTransactionRecords.raw_cm_interval))
# # .filter(EquipmentTransactionRecords.assetnum == data_in.assetnum)
# # .filter(EquipmentTransactionRecords.tahun < year)
# # )
# # previous_failures_result = await db_session.execute(previous_failures_query)
# # previous_failures_sum = previous_failures_result.scalar() or 0
# # # Update with current minus sum of previous
# # transaction.update({"raw_cm_interval": current_num_fail - previous_failures_sum})
# transaction.update({"raw_cm_interval": current_num_fail})
# transaction.update({"tahun": int(year), "seq": int(sequence)})
# transactions.append(EquipmentTransactionRecords(**transaction))
# db_session.add_all(transactions)
# await db_session.commit()
# # Return the number of transactions created
# return len(transactions)
return prediction
async def create(*, db_session: DbSession, equipment_in: EquipmentCreate, token):
"""Creates a new document."""
equipment = Equipment(**equipment_in.model_dump())
db_session.add(equipment)
await db_session.commit()
# await generate_transaction(db_session=db_session, data_in=equipment_in, token=token)
return equipment
async def update(
*, db_session: DbSession, equipment: Equipment, equipment_in: EquipmentUpdate, token
):
"""Updates a document."""
data = equipment_in.model_dump()
update_data = equipment_in.model_dump(exclude_defaults=True)
for field in data:
if field in update_data:
setattr(equipment, field, update_data[field])
await db_session.commit()
updated_data = vars(equipment)
# equipment_create = EquipmentCreate(**updated_data)
# await generate_transaction(
# db_session=db_session, data_in=equipment_create, token=token
# )
return updated_data
async def delete(*, db_session: DbSession, equipment_id: str):
"""Deletes a document."""
query = Delete(Equipment).where(Equipment.id == equipment_id)
await db_session.execute(query)
await db_session.commit()