diff --git a/src/api.py b/src/api.py index 9dc1405..0bd7dd8 100644 --- a/src/api.py +++ b/src/api.py @@ -14,6 +14,8 @@ from src.calculation_time_constrains.router import router as calculation_time_co from src.overhaul_history.router import router as overhaul_history_router from src.scope_equipment_activity.router import router as scope_equipment_activity_router from src.overhaul_schedule.router import router as ovehaul_schedule_router +from src.scope_equipment_part.router import router as scope_equipment_part_router +from src.calculation_target_reliability.router import router as calculation_target_reliability class ErrorMessage(BaseModel): @@ -61,7 +63,11 @@ authenticated_api_router.include_router( ) authenticated_api_router.include_router( - scope_equipment_activity_router, prefix="/equipment-activities", tags=["overhaul_activities"] + scope_equipment_activity_router, prefix="/equipment-activities", tags=["scope_equipment_activities"] +) + +authenticated_api_router.include_router( + scope_equipment_part_router, prefix="/equipment-parts", tags=["scope_equipment_parts"] ) authenticated_api_router.include_router( @@ -73,9 +79,12 @@ calculation_router = APIRouter(prefix="/calculation", tags=["calculations"]) # Time constrains calculation_router.include_router( - calculation_time_constrains_router, prefix="/time-constraint", tags=["time_constraint"]) + calculation_time_constrains_router, prefix="/time-constraint", tags=["calculation", "time_constraint"]) # Target reliability +calculation_router.include_router( + calculation_target_reliability, prefix="/target-reliability", tags=["calculation", "target_reliability"] +) # Budget Constrain diff --git a/src/calculation_target_reliability/__init__.py b/src/calculation_target_reliability/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/calculation_target_reliability/router.py b/src/calculation_target_reliability/router.py new file mode 100644 index 0000000..484f150 --- /dev/null +++ b/src/calculation_target_reliability/router.py @@ -0,0 +1,21 @@ + +from typing import Dict, List, Optional +from fastapi import APIRouter, HTTPException, status +from fastapi.params import Query + +from .service import get_all_target_reliability + +from src.models import StandardResponse +from src.database.core import DbSession +router = APIRouter() + + +@router.get("", response_model=StandardResponse[List[Dict]]) +async def get_target_reliability(db_session: DbSession, scope_name: Optional[str] = Query(None), eaf_threshold: float = Query(100)): + """Get all scope pagination.""" + results = await get_all_target_reliability(db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold) + + return StandardResponse( + data=results, + message="Data retrieved successfully", + ) diff --git a/src/calculation_target_reliability/schema.py b/src/calculation_target_reliability/schema.py new file mode 100644 index 0000000..b2a59bc --- /dev/null +++ b/src/calculation_target_reliability/schema.py @@ -0,0 +1,71 @@ + +from datetime import datetime +from typing import Any, Dict, List, Optional +from uuid import UUID + +from pydantic import Field, BaseModel +from src.models import DefultBase, Pagination + + +class OverhaulBase(BaseModel): + pass + + +class OverhaulCriticalParts(OverhaulBase): + criticalParts: List[str] = Field(..., description="List of critical parts") + + +class OverhaulSchedules(OverhaulBase): + schedules: List[Dict[str, Any] + ] = Field(..., description="List of schedules") + + +class OverhaulSystemComponents(OverhaulBase): + systemComponents: Dict[str, + Any] = Field(..., description="List of system components") + + +class OverhaulRead(OverhaulBase): + overview: Dict[str, Any] + criticalParts: List[str] + schedules: List[Dict[str, Any]] + systemComponents: Dict[str, Any] + + +# { +# "overview": { +# "totalEquipment": 30, +# "nextSchedule": { +# "date": "2025-01-12", +# "Overhaul": "B", +# "equipmentCount": 30 +# } +# }, +# "criticalParts": [ +# "Boiler feed pump", +# "Boiler reheater system", +# "Drum Level (Right) Root Valve A", +# "BCP A Discharge Valve", +# "BFPT A EXH Press HI Root VLV" +# ], +# "schedules": [ +# { +# "date": "2025-01-12", +# "Overhaul": "B", +# "status": "upcoming" +# } +# // ... other scheduled overhauls +# ], +# "systemComponents": { +# "boiler": { +# "status": "operational", +# "lastOverhaul": "2024-06-15" +# }, +# "turbine": { +# "hpt": { "status": "operational" }, +# "ipt": { "status": "operational" }, +# "lpt": { "status": "operational" } +# } +# // ... other major components +# } +# } diff --git a/src/calculation_target_reliability/service.py b/src/calculation_target_reliability/service.py new file mode 100644 index 0000000..c6c8ee6 --- /dev/null +++ b/src/calculation_target_reliability/service.py @@ -0,0 +1,63 @@ + + +from sqlalchemy import Select, Delete +from typing import Optional + +from src.database.core import DbSession +from src.auth.service import CurrentUser +from src.overhaul_schedule.service import get_all as get_all_schedules +from src.scope.model import Scope +from src.scope_equipment.model import ScopeEquipment +from src.scope_equipment.service import get_by_scope_name + + +async def get_all_target_reliability(*, db_session: DbSession, scope_name: str, eaf_threshold: float = 100.0): + """Get all overhaul overview with EAF values that sum to 100%.""" + equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name) + + # If no equipments found, return empty list + if not equipments: + return [] + + import random + + + n = len(equipments) + base_value = 100 / n # Even distribution as base + + # Generate EAF values with ±30% variation from base + eaf_values = [ + base_value + random.uniform(-0.3 * base_value, 0.3 * base_value) + for _ in range(n) + ] + + # Normalize to ensure sum is 100 + total = sum(eaf_values) + eaf_values = [(v * 100 / total) for v in eaf_values] + + # Create result array of dictionaries + result = [ + { + 'id': equipment.id, + 'assetnum': equipment.assetnum, + 'location_tag': equipment.master_equipment.location_tag, + 'name' : equipment.master_equipment.name, + 'eaf': round(eaf, 4) # Add EAF value + } + for equipment, eaf in zip(equipments, eaf_values) + ] + + result.sort(key=lambda x: x['eaf'], reverse=True) + + # Filter equipment up to threshold + cumulative_eaf = 0 + filtered_result = [] + + for equipment in result: + cumulative_eaf += equipment['eaf'] + filtered_result.append(equipment) + + if cumulative_eaf >= eaf_threshold: + break + + return filtered_result diff --git a/src/scope_equipment/model.py b/src/scope_equipment/model.py index 26a70cd..7fc8abe 100644 --- a/src/scope_equipment/model.py +++ b/src/scope_equipment/model.py @@ -28,7 +28,7 @@ class ScopeEquipment(Base, DefaultMixin): backref="overhaul_equipments", lazy="raise" ) - + master_equipment = relationship( "MasterEquipment", lazy="raise", @@ -46,7 +46,19 @@ class ScopeEquipment(Base, DefaultMixin): class MasterEquipment(Base, DefaultMixin): __tablename__ = "ms_equipment_master" + parent_id = Column(UUID(as_uuid=True), nullable=True) assetnum = Column(String, nullable=True) system_tag = Column(String, nullable=True) location_tag = Column(String, nullable=True) name = Column(String, nullable=True) + equipment_tree_id = Column(UUID(as_uuid=True), ForeignKey( + "ms_equipment_tree.id"), nullable=True) + + equipment_tree = relationship( + "MasterEquipmentTree", backref="master_equipments") + + +class MasterEquipmentTree(Base, DefaultMixin): + __tablename__ = "ms_equipment_tree" + + level_no = Column(Integer) diff --git a/src/scope_equipment/service.py b/src/scope_equipment/service.py index 0a412a5..38126a9 100644 --- a/src/scope_equipment/service.py +++ b/src/scope_equipment/service.py @@ -4,7 +4,7 @@ from fastapi import HTTPException, status from sqlalchemy import Select, Delete, desc, func, not_, or_ from sqlalchemy.dialects.postgresql import insert from src.workorder.model import MasterWorkOrder -from .model import ScopeEquipment, MasterEquipment +from .model import MasterEquipmentTree, ScopeEquipment, MasterEquipment from src.scope.service import get_by_scope_name as get_scope_by_name_service from .schema import ScopeEquipmentCreate, ScopeEquipmentUpdate from typing import Optional, Union @@ -130,7 +130,7 @@ async def get_by_scope_name(*, db_session: DbSession, scope_name: Union[str, lis """Returns a document based on the given document id.""" scope = await get_scope_by_name_service(db_session=db_session, scope_name=scope_name) - query = Select(ScopeEquipment) + query = Select(ScopeEquipment).options(selectinload(ScopeEquipment.master_equipment)) if scope: query = query.filter(ScopeEquipment.current_scope_id == scope.id) @@ -169,3 +169,10 @@ async def get_all_master_equipment(*, db_session: DbSession, exclude: Optional[s results = await search_filter_sort_paginate(model=query, **common) return results + + +async def get_equipment_level_by_no(*, db_session: DbSession, level: int): + query = Select(MasterEquipmentTree).filter( + MasterEquipmentTree.level_no == level) + result = await db_session.scalar(query) + return result diff --git a/src/scope_equipment_part/__init__.py b/src/scope_equipment_part/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scope_equipment_part/model.py b/src/scope_equipment_part/model.py new file mode 100644 index 0000000..0867155 --- /dev/null +++ b/src/scope_equipment_part/model.py @@ -0,0 +1,19 @@ + +from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey +from src.database.core import Base +from src.models import DefaultMixin, IdentityMixin, TimeStampMixin +from sqlalchemy.orm import relationship +from src.workorder.model import MasterWorkOrder +from sqlalchemy.ext.hybrid import hybrid_property + + +class ScopeEquipmentPart(Base, DefaultMixin): + __tablename__ = "oh_tr_scope_equipment_part" + + assetnum = Column(String, nullable=False) + stock = Column(Integer, nullable=False, default=0) + + master_equipments = relationship( + "MasterEquipment", lazy="raise", primaryjoin="and_(ScopeEquipmentPart.assetnum == foreign(MasterEquipment.assetnum))", uselist=False) + + diff --git a/src/scope_equipment_part/router.py b/src/scope_equipment_part/router.py new file mode 100644 index 0000000..73ccd82 --- /dev/null +++ b/src/scope_equipment_part/router.py @@ -0,0 +1,25 @@ + +from fastapi import APIRouter, HTTPException, Query, status + + +from .service import get_all +from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityPagination, ScopeEquipmentActivityRead, ScopeEquipmentActivityUpdate + +from src.models import StandardResponse +from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession + +router = APIRouter() + + +@router.get("/{assetnum}", response_model=StandardResponse[ScopeEquipmentActivityPagination]) +async def get_scope_equipment_activities(common: CommonParameters, assetnum): + """Get all scope activity pagination.""" + # return + data = await get_all(common=common, assetnum=assetnum) + + raise Exception(data) + + return StandardResponse( + data=data, + message="Data retrieved successfully", + ) diff --git a/src/scope_equipment_part/schema.py b/src/scope_equipment_part/schema.py new file mode 100644 index 0000000..79ef18e --- /dev/null +++ b/src/scope_equipment_part/schema.py @@ -0,0 +1,69 @@ + +from datetime import datetime +from typing import Any, Dict, List, Optional +from uuid import UUID + +from pydantic import Field, BaseModel +from src.models import DefultBase, Pagination + + +class ScopeEquipmentActivityBase(DefultBase): + assetnum: str = Field(..., description="Assetnum is required") + + +class ScopeEquipmentActivityCreate(ScopeEquipmentActivityBase): + name: str + cost: Optional[float] = Field(0) + + +class ScopeEquipmentActivityUpdate(ScopeEquipmentActivityBase): + name: Optional[str] = Field(None) + cost: Optional[str] = Field(0) + + +class ScopeEquipmentActivityRead(ScopeEquipmentActivityBase): + name: str + cost: float + + +class ScopeEquipmentActivityPagination(Pagination): + items: List[ScopeEquipmentActivityRead] = [] + + +# { +# "overview": { +# "totalEquipment": 30, +# "nextSchedule": { +# "date": "2025-01-12", +# "Overhaul": "B", +# "equipmentCount": 30 +# } +# }, +# "criticalParts": [ +# "Boiler feed pump", +# "Boiler reheater system", +# "Drum Level (Right) Root Valve A", +# "BCP A Discharge Valve", +# "BFPT A EXH Press HI Root VLV" +# ], +# "schedules": [ +# { +# "date": "2025-01-12", +# "Overhaul": "B", +# "status": "upcoming" +# } +# // ... other scheduled overhauls +# ], +# "systemComponents": { +# "boiler": { +# "status": "operational", +# "lastOverhaul": "2024-06-15" +# }, +# "turbine": { +# "hpt": { "status": "operational" }, +# "ipt": { "status": "operational" }, +# "lpt": { "status": "operational" } +# } +# // ... other major components +# } +# } diff --git a/src/scope_equipment_part/service.py b/src/scope_equipment_part/service.py new file mode 100644 index 0000000..c03893e --- /dev/null +++ b/src/scope_equipment_part/service.py @@ -0,0 +1,83 @@ + + +from sqlalchemy import Select, Delete, and_ +from sqlalchemy.orm import selectinload +from typing import Optional + +from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree +from src.scope_equipment.service import get_equipment_level_by_no + +from .model import ScopeEquipmentPart +from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate + +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate +from src.auth.service import CurrentUser + + +# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]: +# """Returns a document based on the given document id.""" +# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id) +# return result + + +async def get_all(common: CommonParameters, assetnum: Optional[str]): + if not assetnum: + raise ValueError("assetnum parameter is required") + + db_session: DbSession = common.get("db_session") + + # First get the parent equipment + equipment_stmt = Select(MasterEquipment).where( + MasterEquipment.assetnum == assetnum) + equipment: MasterEquipment = await db_session.scalar(equipment_stmt) + + if not equipment: + raise ValueError(f"No equipment found with assetnum: {assetnum}") + + # Build query for parts + stmt = ( + Select(ScopeEquipmentPart) + .join(ScopeEquipmentPart.master_equipments) + .join(MasterEquipment.equipment_tree) + .where( + and_( + MasterEquipment.parent_id == equipment.id, + MasterEquipmentTree.level_no == 4 + ) + ).options(selectinload(ScopeEquipmentPart.master_equipments)) + ) + + results = await search_filter_sort_paginate(model=stmt, **common) + + return results + + +# async def create(*, db_session: DbSession, scope_equipment_activty_in: ScopeEquipmentActivityCreate): +# activity = ScopeEquipmentActivity( +# **scope_equipment_activty_in.model_dump()) +# db_session.add(activity) +# await db_session.commit() +# return activity + + +# async def update(*, db_session: DbSession, activity: ScopeEquipmentActivity, scope_equipment_activty_in: ScopeEquipmentActivityUpdate): +# """Updates a document.""" +# data = scope_equipment_activty_in.model_dump() + +# update_data = scope_equipment_activty_in.model_dump(exclude_defaults=True) + +# for field in data: +# if field in update_data: +# setattr(activity, field, update_data[field]) + +# await db_session.commit() + +# return activity + + +# async def delete(*, db_session: DbSession, scope_equipment_activity_id: str): +# """Deletes a document.""" +# activity = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id) +# await db_session.delete(activity) +# await db_session.commit()