diff --git a/src/config.py b/src/config.py index 2c40d5f..e26634d 100644 --- a/src/config.py +++ b/src/config.py @@ -63,10 +63,18 @@ DATABASE_ENGINE_POOL_SIZE = config("DATABASE_ENGINE_POOL_SIZE", cast=int, defaul DATABASE_ENGINE_MAX_OVERFLOW = config( "DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0 ) -# Deal with DB disconnects -# https://docs.sqlalchemy.org/en/20/core/pooling.html#pool-disconnects -DATABASE_ENGINE_POOL_PING = config("DATABASE_ENGINE_POOL_PING", default=False) + +COLLECTOR_HOSTNAME = config("COLLECTOR_HOSTNAME") +COLLECTOR_PORT = config("COLLECTOR_PORT", default="5432") +COLLECTOR_CREDENTIAL_USER = config("COLLECTOR_CREDENTIAL_USER") +COLLECTOR_CREDENTIAL_PASSWORD = config("COLLECTOR_CREDENTIAL_PASSWORD") +QUOTED_COLLECTOR_CREDENTIAL_PASSWORD = parse.quote(str(COLLECTOR_CREDENTIAL_PASSWORD)) +COLLECTOR_NAME = config("COLLECTOR_NAME") + +# Deal w SQLALCHEMY_DATABASE_URI = f"postgresql+asyncpg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}" +SQLALCHEMY_COLLECTOR_URI = f"postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}" + TIMEZONE = "Asia/Jakarta" diff --git a/src/main.py b/src/main.py index 3d0d1e1..cb31470 100644 --- a/src/main.py +++ b/src/main.py @@ -23,7 +23,7 @@ from starlette.routing import compile_path from starlette.staticfiles import StaticFiles from src.api import api_router -from src.database.core import async_session, engine +from src.database.core import async_session, engine, async_collector_session from src.enums import ResponseStatus from src.exceptions import handle_exception from src.logging import configure_logging @@ -72,11 +72,16 @@ async def db_session_middleware(request: Request, call_next): try: session = async_scoped_session(async_session, scopefunc=get_request_id) request.state.db = session() + + collector_session = async_scoped_session(async_collector_session, scopefunc=get_request_id) + request.state.collector_db = collector_session() + response = await call_next(request) except Exception as e: raise e from None finally: await request.state.db.close() + await request.state.collector_db.close() _request_id_ctx_var.reset(ctx_token) return response diff --git a/src/maximo/model.py b/src/maximo/model.py new file mode 100644 index 0000000..87c158c --- /dev/null +++ b/src/maximo/model.py @@ -0,0 +1,42 @@ +from sqlalchemy import Column, BigInteger, Integer, Float, String, Text, DateTime +from src.database.core import CollectorBase + + +class WorkOrderData(CollectorBase): + __tablename__ = "wo_staging_3" + + id = Column(BigInteger, primary_key=True, autoincrement=True) + assetnum = Column(Text, nullable=True) + description1 = Column(Text, nullable=True) + unit = Column(Integer, nullable=True) + location = Column(String(25), nullable=True) + system_tag = Column(String(25), nullable=True) + wonum = Column(String(10), nullable=True) + description2 = Column(Text, nullable=True) + wo_complt_comment = Column(Text, nullable=True) + worktype = Column(String(10), nullable=True) + jpnum = Column(String(10), nullable=True) + workgroup = Column(String(30), nullable=True) + mat_cost_max = Column(Float, nullable=True) + serv_cost_max = Column(Float, nullable=True) + total_cost_max = Column(Float, nullable=True) + wo_start = Column(DateTime, nullable=True) + wo_finish = Column(DateTime, nullable=True) + wo_start_olah = Column(DateTime, nullable=True) + wo_finish_olah = Column(DateTime, nullable=True) + reportdate = Column(DateTime, nullable=True) + reportdate_olah = Column(DateTime, nullable=True) + time_to_event = Column(Float, nullable=True) + actstart = Column(DateTime, nullable=True) + actfinish = Column(DateTime, nullable=True) + actstart_olah = Column(DateTime, nullable=True) + actfinish_olah = Column(DateTime, nullable=True) + act_repair_time = Column(Float, nullable=True) + jumlah_labor = Column(Integer, nullable=True) + need_downtime = Column(String(100), nullable=True) + validation_downtime = Column(Integer, nullable=True) + down_0_and_not_oh = Column(Integer, nullable=True) + downtime = Column(Integer, nullable=True) + failure_code = Column(String(10), nullable=True) + problem_code = Column(String(10), nullable=True) + act_finish_wo_start = Column(Float, nullable=True) diff --git a/src/maximo/service.py b/src/maximo/service.py index b30b53e..ac618c3 100644 --- a/src/maximo/service.py +++ b/src/maximo/service.py @@ -1,154 +1,43 @@ -from datetime import datetime, timedelta -from typing import Any, Dict - -import httpx -from fastapi import HTTPException -from starlette.config import Config - -from src.config import MAXIMO_API_KEY, MAXIMO_BASE_URL - - -class MaximoDataMapper: - """ - Helper class to map MAXIMO API response to our data structure. - Update these mappings according to actual MAXIMO API documentation. - """ - - def __init__(self, maximo_data: Dict[Any, Any]): - self.data = maximo_data - - def get_start_date(self) -> datetime: - """ - Extract start date from MAXIMO data. - TODO: Update this based on actual MAXIMO API response structure - Example: might be data['startDate'] or data['SCHEDSTART'] etc. - """ - # This is a placeholder - update with actual MAXIMO field name - start_date_str = self.data.get("scheduleStart") - if not start_date_str: - raise ValueError("Start date not found in MAXIMO data") - return datetime.fromisoformat(start_date_str) - - def get_end_date(self) -> datetime: - """ - Extract end date from MAXIMO data. - TODO: Update this based on actual MAXIMO API response structure - """ - # This is a placeholder - update with actual MAXIMO field name - end_date_str = self.data.get("scheduleEnd") - if not end_date_str: - raise ValueError("End date not found in MAXIMO data") - return datetime.fromisoformat(end_date_str) - - def get_maximo_id(self) -> str: - """ - Extract MAXIMO ID from response. - TODO: Update this based on actual MAXIMO API response structure - """ - # This is a placeholder - update with actual MAXIMO field name - maximo_id = self.data.get("workOrderId") - if not maximo_id: - raise ValueError("MAXIMO ID not found in response") - return str(maximo_id) - - def get_status(self) -> str: - """ - Extract status from MAXIMO data. - TODO: Update this based on actual MAXIMO API response structure - """ - # This is a placeholder - update with actual MAXIMO status field and values - status = self.data.get("status", "").upper() - return status - - def get_total_cost(self) -> float: - """ - Extract total cost from MAXIMO data. - TODO: Update this based on actual MAXIMO API response structure - """ - # This is a placeholder - update with actual MAXIMO field name - cost = self.data.get("totalCost", 0) - return float(cost) - - def get_scope_name(self) -> str: - scope_name = self.data.get("location", "A") - return scope_name - - -class MaximoService: - def __init__(self): - # TODO: Update these settings based on actual MAXIMO API configuration - self.base_url = MAXIMO_BASE_URL - self.api_key = MAXIMO_API_KEY - - async def get_recent_overhaul(self) -> dict: - """ - Fetch most recent overhaul from MAXIMO. - TODO: Update this method based on actual MAXIMO API endpoints and parameters - """ - current_date = datetime.now() - schedule_start = current_date + timedelta(days=30) # Starting in 30 days - schedule_end = schedule_start + timedelta(days=90) # 90 day overhaul period - - return { - "scheduleStart": schedule_start.isoformat(), - "scheduleEnd": schedule_end.isoformat(), - "workOrderId": "WO-2024-12345", - "status": "PLAN", # Common Maximo statuses: SCHEDULED, INPRG, COMP, CLOSE - "totalCost": 10000000.00, - "description": "Annual Turbine Overhaul", - "priority": 1, - "location": "A", - "assetDetails": [ - { - "assetnum": "ASSET001", - "description": "Gas Turbine", - "status": "OPERATING", - }, - { - "assetnum": "ASSET002", - "description": "Steam Turbine", - "status": "OPERATING", - }, - ], - "workType": "OH", # OH for Overhaul - "createdBy": "MAXADMIN", - "createdDate": (current_date - timedelta(days=10)).isoformat(), - "lastModifiedBy": "MAXADMIN", - "lastModifiedDate": current_date.isoformat(), - } - - async with httpx.AsyncClient() as client: - try: - # TODO: Update endpoint and parameters based on actual MAXIMO API - response = await client.get( - f"{self.base_url}/your-endpoint-here", - headers={ - "Authorization": f"Bearer {self.api_key}", - # Add any other required headers - }, - params={ - # Update these parameters based on actual MAXIMO API - "orderBy": "-scheduleEnd", # Example parameter - "limit": 1, - }, - ) - - if response.status_code != 200: - raise HTTPException( - status_code=response.status_code, - detail=f"MAXIMO API error: {response.text}", - ) - - data = response.json() - if not data: - raise HTTPException( - status_code=404, detail="No recent overhaul found" - ) - - # TODO: Update this based on actual MAXIMO response structure - return data[0] if isinstance(data, list) else data - - except httpx.RequestError as e: - raise HTTPException( - status_code=503, detail=f"Failed to connect to MAXIMO: {str(e)}" - ) +from datetime import datetime +from sqlalchemy import select, func, cast, Numeric +from sqlalchemy.orm import Session +from sqlalchemy import and_ +from sqlalchemy.sql import not_ +from src.maximo.model import WorkOrderData # Assuming this is where your model is +from src.database.core import CollectorDbSession + + +async def get_cm_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime): + query = select( + WorkOrderData.location, + (func.sum(WorkOrderData.total_cost_max).cast(Numeric) / func.count(WorkOrderData.wonum)).label('avg_cost') + ).where( + and_( + # WorkOrderData.wo_start >= last_oh_date, + # WorkOrderData.wo_start <= upcoming_oh_date, + WorkOrderData.worktype.in_(['CM', 'EM', 'PROACTIVE']), + WorkOrderData.system_tag.in_(['HPB', 'AH', 'APC', 'SCR', 'CL', 'DM', 'CRH', 'ASH', 'BAD', 'DS', 'WTP', + 'MT', 'SUP', 'DCS', 'FF', 'EG', 'AI', 'SPS', 'EVM', 'SCW', 'KLH', 'CH', + 'TUR', 'LOT', 'HRH', 'ESP', 'CAE', 'GMC', 'BFT', 'LSH', 'CHB', 'BSS', + 'LOS', 'LPB', 'SAC', 'CP', 'EHS', 'RO', 'GG', 'MS', 'CW', 'SO', 'ATT', + 'AFG', 'EHB', 'RP', 'FO', 'PC', 'APE', 'AF', 'DMW', 'BRS', 'GEN', 'ABS', + 'CHA', 'TR', 'H2', 'BDW', 'LOM', 'ACR', 'AL', 'FW', 'COND', 'CCCW', 'IA', + 'GSS', 'BOL', 'SSB', 'CO', 'OA', 'CTH-UPD', 'AS', 'DP']), + WorkOrderData.reportdate.is_not(None), + WorkOrderData.actstart.is_not(None), + WorkOrderData.actfinish.is_not(None), + WorkOrderData.unit.in_([3, 0]), + WorkOrderData.reportdate >= datetime.strptime('2015-01-01', '%Y-%m-%d'), + not_(WorkOrderData.wonum.like('T%')) + ) + ).group_by( + WorkOrderData.location + ).order_by( + func.count(WorkOrderData.wonum).desc() + ) + result = await collector_db.execute(query) + data = result.all() + + return { + data.location: data.avg_cost for data in data + } diff --git a/src/overhaul/service.py b/src/overhaul/service.py index c8e79d0..df4c412 100644 --- a/src/overhaul/service.py +++ b/src/overhaul/service.py @@ -38,6 +38,31 @@ async def get_overhaul_schedules(*, db_session: DbSession): def get_overhaul_system_components(): """Get all overhaul system components with dummy data.""" + + powerplant_reliability = { + "Plant Control": 98, + "SPS": 98, + "Turbine": 98, + "Generator": 98, + "Condensate Water": 98, + "Feedwater System": 98, + "Cooling Water": 98, + "SCR": 98, + "Ash Handling": 98, + "Air Flue Gas": 98, + "Boiler": 98, + "SAC-IAC": 98, + "KLH": 98, + "CL": 98, + "Desalination": 98, + "FGD": 98, + "CHS": 98, + "SSB": 98, + "WTP": 98, + } + + return powerplant_reliability + return { "HPT": { "efficiency": "92%", diff --git a/src/overhaul_activity/router.py b/src/overhaul_activity/router.py index ad18fdd..1368054 100644 --- a/src/overhaul_activity/router.py +++ b/src/overhaul_activity/router.py @@ -3,6 +3,7 @@ from uuid import UUID from fastapi import APIRouter, HTTPException, Query, status +from src.database.core import CollectorDbSession from src.database.service import (CommonParameters, DbSession, search_filter_sort_paginate) from src.models import StandardResponse @@ -20,6 +21,7 @@ router = APIRouter() async def get_scope_equipments( common: CommonParameters, overhaul_session: str, + collector_db: CollectorDbSession, location_tag: Optional[str] = Query(None), scope_name: Optional[str] = Query(None), ): @@ -30,6 +32,7 @@ async def get_scope_equipments( location_tag=location_tag, scope_name=scope_name, overhaul_session_id=overhaul_session, + collector_db=collector_db, ) diff --git a/src/overhaul_activity/service.py b/src/overhaul_activity/service.py index 72c509a..31c7a04 100644 --- a/src/overhaul_activity/service.py +++ b/src/overhaul_activity/service.py @@ -12,7 +12,7 @@ from src.database.core import DbSession from src.database.service import CommonParameters, search_filter_sort_paginate from src.overhaul_activity.utils import get_material_cost, get_service_cost from src.overhaul_scope.model import OverhaulScope -from src.overhaul_scope.service import get as get_session +from src.overhaul_scope.service import get as get_session, get_prev_oh from src.standard_scope.model import MasterEquipment, StandardScope from src.standard_scope.service import get_by_oh_session_id from src.workscope_group.model import MasterActivity @@ -26,6 +26,8 @@ from .schema import (OverhaulActivityCreate, OverhaulActivityRead, OverhaulActivityUpdate) import json +from src.database.core import CollectorDbSession +from src.maximo.service import get_cm_cost_summary async def get( *, db_session: DbSession, assetnum: str, overhaul_session_id: Optional[UUID] = None @@ -55,7 +57,8 @@ async def get_all( overhaul_session_id: UUID, location_tag: Optional[str] = None, scope_name: Optional[str] = None, - all: bool = False + all: bool = False, + collector_db: CollectorDbSession ): # query = ( # Select(OverhaulActivity) @@ -84,6 +87,7 @@ async def get_all( # ) overhaul = await get_overhaul(db_session=common['db_session'], overhaul_session_id=overhaul_session_id) + prev_oh_scope = await get_prev_oh(db_session=common['db_session'], overhaul_session=overhaul) query = ( Select(StandardScope) @@ -102,25 +106,25 @@ async def get_all( num_equipments = len((await common['db_session'].execute(query)).scalars().all()) - data_cost = get_cost_per_failute() - + material_cost = await get_cm_cost_summary(collector_db=collector_db, last_oh_date=prev_oh_scope.end_date, upcoming_oh_date=overhaul.start_date) + service_cost = get_service_cost(scope=overhaul.maintenance_type.name, total_equipment=num_equipments) equipments = await search_filter_sort_paginate(model=query, **common) data = equipments['items'] - results = [] for equipment in data: if not equipment.master_equipment: continue - cost = next((item for item in data_cost if item['location'] == equipment.location_tag), None) + cost = material_cost.get(equipment.location_tag, 0) + res = OverhaulActivityRead( id=equipment.id, - material_cost=cost.get('avg_cost', 0) if cost else 0, - service_cost=200000000, + material_cost=float(cost), + service_cost=float(service_cost), location_tag=equipment.location_tag, equipment_name=equipment.master_equipment.name if equipment.master_equipment else None, oh_scope=overhaul.maintenance_type.name, @@ -132,8 +136,9 @@ async def get_all( return equipments -async def get_standard_scope_by_session_id(*, db_session: DbSession, overhaul_session_id: UUID): - overhaul = await get_overhaul(db_session=db_session, overhaul_session_id=overhaul_session_id) +async def get_standard_scope_by_session_id(*, db_session: DbSession, overhaul_session_id: UUID, collector_db: CollectorDbSession): + overhaul = await get_session(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id) + prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=overhaul) query = ( Select(StandardScope) @@ -153,16 +158,17 @@ async def get_standard_scope_by_session_id(*, db_session: DbSession, overhaul_se data = await db_session.execute(query) eqs = data.scalars().all() - material_cost = get_material_cost("B", len(eqs)) results = [] - data_cost = get_cost_per_failute() + material_cost = await get_cm_cost_summary(collector_db=collector_db, last_oh_date=prev_oh_scope.end_date, upcoming_oh_date=overhaul.start_date) + service_cost = get_service_cost(scope=overhaul.maintenance_type.name, total_equipment=len(eqs)) + for equipment in eqs: - cost = next((item for item in data_cost if item['location'] == equipment.location_tag), None) + cost = material_cost.get(equipment.location_tag,0) res = OverhaulActivityRead( id=equipment.id, - material_cost=cost.get('avg_cost', 0) if cost else 0, - service_cost=200000000, + material_cost=float(cost), + service_cost=float(service_cost), location_tag=equipment.location_tag, equipment_name=equipment.master_equipment.name if equipment.master_equipment else None, oh_scope=overhaul.maintenance_type.name,