add session to scope job

main
Cizz22 11 months ago
parent 2ae301a78b
commit 9a126d013c

@ -1,10 +1,6 @@
import uvicorn import uvicorn
from src.config import PORT, HOST
from src.config import HOST, PORT
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run( uvicorn.run("src.main:app", host=HOST, port=PORT, reload=True)
"src.main:app",
host=HOST,
port=PORT,
reload=True
)

@ -1,11 +1,23 @@
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
from src.auth.service import JWTBearer from src.auth.service import JWTBearer
from src.calculation_budget_constrains.router import \
router as calculation_budget_constraint
from src.calculation_target_reliability.router import \
router as calculation_target_reliability
from src.calculation_time_constrains.router import \
router as calculation_time_constrains_router
from src.job.router import router as job_router
from src.overhaul.router import router as overhaul_router
from src.overhaul_activity.router import router as overhaul_activity_router
from src.overhaul_job.router import router as job_overhaul_router
from src.overhaul_scope.router import router as scope_router
from src.scope_equipment.router import router as scope_equipment_router
from src.scope_equipment_job.router import router as scope_equipment_job_router
# from src.overhaul_scope.router import router as scope_router # from src.overhaul_scope.router import router as scope_router
# from src.scope_equipment.router import router as scope_equipment_router # from src.scope_equipment.router import router as scope_equipment_router
@ -15,20 +27,11 @@ from src.auth.service import JWTBearer
# # from src.overhaul_schedule.router import router as ovehaul_schedule_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.scope_equipment_part.router import router as scope_equipment_part_router
# from src.calculation_target_reliability.router import router as calculation_target_reliability # from src.calculation_target_reliability.router import router as calculation_target_reliability
# #
# from src.master_activity.router import router as activity_router # from src.master_activity.router import router as activity_router
from src.overhaul.router import router as overhaul_router
from src.scope_equipment.router import router as scope_equipment_router
from src.overhaul_scope.router import router as scope_router
from src.overhaul_activity.router import router as overhaul_activity_router
from src.calculation_target_reliability.router import router as calculation_target_reliability
from src.scope_equipment_job.router import router as scope_equipment_job_router
from src.job.router import router as job_router
from src.calculation_time_constrains.router import router as calculation_time_constrains_router
from src.calculation_budget_constrains.router import router as calculation_budget_constraint
from src.overhaul_job.router import router as job_overhaul_router
class ErrorMessage(BaseModel): class ErrorMessage(BaseModel):
msg: str msg: str
@ -55,18 +58,20 @@ def healthcheck():
return {"status": "ok"} return {"status": "ok"}
authenticated_api_router = APIRouter(dependencies=[Depends(JWTBearer())], authenticated_api_router = APIRouter(
) dependencies=[Depends(JWTBearer())],
)
# overhaul data # overhaul data
authenticated_api_router.include_router( authenticated_api_router.include_router(
overhaul_router, prefix="/overhauls", tags=["overhaul"]) overhaul_router, prefix="/overhauls", tags=["overhaul"]
)
authenticated_api_router.include_router( authenticated_api_router.include_router(job_router, prefix="/jobs", tags=["job"])
job_router, prefix="/jobs", tags=["job"])
# # Overhaul session data # # Overhaul session data
authenticated_api_router.include_router( authenticated_api_router.include_router(
scope_router, prefix="/overhaul-session", tags=["overhaul-session"]) scope_router, prefix="/overhaul-session", tags=["overhaul-session"]
)
authenticated_api_router.include_router( authenticated_api_router.include_router(
scope_equipment_router, prefix="/scope-equipments", tags=["scope_equipment"] scope_equipment_router, prefix="/scope-equipments", tags=["scope_equipment"]
@ -77,7 +82,9 @@ authenticated_api_router.include_router(
) )
authenticated_api_router.include_router( authenticated_api_router.include_router(
scope_equipment_job_router, prefix="/scope-equipment-jobs", tags=["scope_equipment", "job"] scope_equipment_job_router,
prefix="/scope-equipment-jobs",
tags=["scope_equipment", "job"],
) )
authenticated_api_router.include_router( authenticated_api_router.include_router(
@ -109,20 +116,25 @@ calculation_router = APIRouter(prefix="/calculation", tags=["calculations"])
# Time constrains # Time constrains
calculation_router.include_router( calculation_router.include_router(
calculation_time_constrains_router, prefix="/time-constraint", tags=["calculation", "time_constraint"]) calculation_time_constrains_router,
prefix="/time-constraint",
tags=["calculation", "time_constraint"],
)
# Target reliability # Target reliability
calculation_router.include_router( calculation_router.include_router(
calculation_target_reliability, prefix="/target-reliability", tags=["calculation", "target_reliability"] calculation_target_reliability,
prefix="/target-reliability",
tags=["calculation", "target_reliability"],
) )
# # Budget Constrain # # Budget Constrain
calculation_router.include_router( calculation_router.include_router(
calculation_budget_constraint, prefix="/budget-constraint", tags=["calculation", "budget_constraint"] calculation_budget_constraint,
prefix="/budget-constraint",
tags=["calculation", "budget_constraint"],
) )
authenticated_api_router.include_router( authenticated_api_router.include_router(calculation_router)
calculation_router
)
api_router.include_router(authenticated_api_router) api_router.include_router(authenticated_api_router)

@ -1,5 +1,3 @@
from pydantic import BaseModel from pydantic import BaseModel

@ -1,10 +1,13 @@
# app/auth/auth_bearer.py # app/auth/auth_bearer.py
from typing import Annotated, Optional from typing import Annotated, Optional
from fastapi import Depends, Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import requests import requests
from fastapi import Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
import src.config as config import src.config as config
from .model import UserBase from .model import UserBase
@ -13,21 +16,24 @@ class JWTBearer(HTTPBearer):
super(JWTBearer, self).__init__(auto_error=auto_error) super(JWTBearer, self).__init__(auto_error=auto_error)
async def __call__(self, request: Request): async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request) credentials: HTTPAuthorizationCredentials = await super(
JWTBearer, self
).__call__(request)
if credentials: if credentials:
if not credentials.scheme == "Bearer": if not credentials.scheme == "Bearer":
raise HTTPException( raise HTTPException(
status_code=403, detail="Invalid authentication scheme.") status_code=403, detail="Invalid authentication scheme."
)
user_info = self.verify_jwt(credentials.credentials) user_info = self.verify_jwt(credentials.credentials)
if not user_info: if not user_info:
raise HTTPException( raise HTTPException(
status_code=403, detail="Invalid token or expired token.") status_code=403, detail="Invalid token or expired token."
)
request.state.user = user_info request.state.user = user_info
return user_info return user_info
else: else:
raise HTTPException( raise HTTPException(status_code=403, detail="Invalid authorization code.")
status_code=403, detail="Invalid authorization code.")
def verify_jwt(self, jwtoken: str) -> Optional[UserBase]: def verify_jwt(self, jwtoken: str) -> Optional[UserBase]:
try: try:
@ -40,7 +46,7 @@ class JWTBearer(HTTPBearer):
return None return None
user_data = response.json() user_data = response.json()
return UserBase(**user_data['data']) return UserBase(**user_data["data"])
except Exception as e: except Exception as e:
print(f"Token verification error: {str(e)}") print(f"Token verification error: {str(e)}")
@ -51,6 +57,7 @@ class JWTBearer(HTTPBearer):
async def get_current_user(request: Request) -> UserBase: async def get_current_user(request: Request) -> UserBase:
return request.state.user return request.state.user
async def get_token(request: Request): async def get_token(request: Request):
token = request.headers.get("Authorization") token = request.headers.get("Authorization")
@ -59,5 +66,6 @@ async def get_token(request: Request):
return "" return ""
CurrentUser = Annotated[UserBase, Depends(get_current_user)] CurrentUser = Annotated[UserBase, Depends(get_current_user)]
Token = Annotated[str, Depends(get_token)] Token = Annotated[str, Depends(get_token)]

@ -1,19 +1,26 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from fastapi.params import Query from fastapi.params import Query
from src.database.core import DbSession
from src.models import StandardResponse
from .service import get_all_budget_constrains from .service import get_all_budget_constrains
from src.models import StandardResponse
from src.database.core import DbSession
router = APIRouter() router = APIRouter()
@router.get("", response_model=StandardResponse[List[Dict]]) @router.get("", response_model=StandardResponse[List[Dict]])
async def get_target_reliability(db_session: DbSession, scope_name: Optional[str] = Query(None), cost_threshold: float = Query(100)): async def get_target_reliability(
db_session: DbSession,
scope_name: Optional[str] = Query(None),
cost_threshold: float = Query(100),
):
"""Get all scope pagination.""" """Get all scope pagination."""
results = await get_all_budget_constrains(db_session=db_session, scope_name=scope_name, cost_threshold=cost_threshold) results = await get_all_budget_constrains(
db_session=db_session, scope_name=scope_name, cost_threshold=cost_threshold
)
return StandardResponse( return StandardResponse(
data=results, data=results,

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, BaseModel from pydantic import BaseModel, Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
@ -16,13 +16,13 @@ class OverhaulCriticalParts(OverhaulBase):
class OverhaulSchedules(OverhaulBase): class OverhaulSchedules(OverhaulBase):
schedules: List[Dict[str, Any] schedules: List[Dict[str, Any]] = Field(..., description="List of schedules")
] = Field(..., description="List of schedules")
class OverhaulSystemComponents(OverhaulBase): class OverhaulSystemComponents(OverhaulBase):
systemComponents: Dict[str, systemComponents: Dict[str, Any] = Field(
Any] = Field(..., description="List of system components") ..., description="List of system components"
)
class OverhaulRead(OverhaulBase): class OverhaulRead(OverhaulBase):

@ -1,16 +1,17 @@
import random import random
from sqlalchemy import Select, Delete
from typing import Optional from typing import Optional
from src.database.core import DbSession from sqlalchemy import Delete, Select
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.scope_equipment.model import ScopeEquipment from src.scope_equipment.model import ScopeEquipment
from src.scope_equipment.service import get_by_scope_name from src.scope_equipment.service import get_by_scope_name
async def get_all_budget_constrains(*, db_session: DbSession, scope_name: str, cost_threshold: float = 100.0): async def get_all_budget_constrains(
*, db_session: DbSession, scope_name: str, cost_threshold: float = 100.0
):
"""Get all overhaul overview with EAF values that sum to 100%.""" """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) equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name)
@ -20,23 +21,23 @@ async def get_all_budget_constrains(*, db_session: DbSession, scope_name: str, c
# Create result array of dictionaries # Create result array of dictionaries
result = [ result = [
{ {
'id': equipment.id, "id": equipment.id,
'assetnum': equipment.assetnum, "assetnum": equipment.assetnum,
'location_tag': equipment.master_equipment.location_tag, "location_tag": equipment.master_equipment.location_tag,
'name': equipment.master_equipment.name, "name": equipment.master_equipment.name,
'total_cost': 1000000 + random.randint(10000, 5000000) "total_cost": 1000000 + random.randint(10000, 5000000),
} }
for equipment in equipments for equipment in equipments
] ]
result.sort(key=lambda x: x['total_cost'], reverse=True) result.sort(key=lambda x: x["total_cost"], reverse=True)
# Filter equipment up to threshold # Filter equipment up to threshold
cumulative_cost = 0 cumulative_cost = 0
filtered_result = [] filtered_result = []
for equipment in result: for equipment in result:
cumulative_cost += equipment['total_cost'] cumulative_cost += equipment["total_cost"]
if cumulative_cost >= cost_threshold: if cumulative_cost >= cost_threshold:
break break

@ -1,19 +1,26 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from fastapi.params import Query from fastapi.params import Query
from src.database.core import DbSession
from src.models import StandardResponse
from .service import get_all_target_reliability from .service import get_all_target_reliability
from src.models import StandardResponse
from src.database.core import DbSession
router = APIRouter() router = APIRouter()
@router.get("", response_model=StandardResponse[List[Dict]]) @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)): async def get_target_reliability(
db_session: DbSession,
scope_name: Optional[str] = Query(None),
eaf_threshold: float = Query(100),
):
"""Get all scope pagination.""" """Get all scope pagination."""
results = await get_all_target_reliability(db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold) results = await get_all_target_reliability(
db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold
)
return StandardResponse( return StandardResponse(
data=results, data=results,

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, BaseModel from pydantic import BaseModel, Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
@ -16,13 +16,13 @@ class OverhaulCriticalParts(OverhaulBase):
class OverhaulSchedules(OverhaulBase): class OverhaulSchedules(OverhaulBase):
schedules: List[Dict[str, Any] schedules: List[Dict[str, Any]] = Field(..., description="List of schedules")
] = Field(..., description="List of schedules")
class OverhaulSystemComponents(OverhaulBase): class OverhaulSystemComponents(OverhaulBase):
systemComponents: Dict[str, systemComponents: Dict[str, Any] = Field(
Any] = Field(..., description="List of system components") ..., description="List of system components"
)
class OverhaulRead(OverhaulBase): class OverhaulRead(OverhaulBase):

@ -1,20 +1,24 @@
from sqlalchemy import Select, Delete
from typing import Optional from typing import Optional
from src.database.core import DbSession from sqlalchemy import Delete, Select
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.scope_equipment.model import ScopeEquipment from src.scope_equipment.model import ScopeEquipment
from src.scope_equipment.service import get_by_scope_name from src.scope_equipment.service import get_by_scope_name
from src.scope_equipment_job.service import get_equipment_level_by_no from src.scope_equipment_job.service import get_equipment_level_by_no
async def get_all_target_reliability(*, db_session: DbSession, scope_name: str, eaf_threshold: float = 100.0):
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%, aggregated by system.""" """Get all overhaul overview with EAF values that sum to 100%, aggregated by system."""
equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name) equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name)
equipment_system = await get_equipment_level_by_no(db_session=db_session, level=1) equipment_system = await get_equipment_level_by_no(db_session=db_session, level=1)
equipment_subsystem = await get_equipment_level_by_no(db_session=db_session, level=2) equipment_subsystem = await get_equipment_level_by_no(
db_session=db_session, level=2
)
# If no equipments found, return empty list # If no equipments found, return empty list
if not equipments: if not equipments:
return [] return []
@ -23,13 +27,13 @@ async def get_all_target_reliability(*, db_session: DbSession, scope_name: str,
n = len(equipments) n = len(equipments)
base_value = 100 / n # Even distribution as base base_value = 100 / n # Even distribution as base
# Generate EAF values with ±30% variation from base # Generate EAF values with ±30% variation from base
eaf_values = [ eaf_values = [
base_value + random.uniform(-0.3 * base_value, 0.3 * base_value) base_value + random.uniform(-0.3 * base_value, 0.3 * base_value)
for _ in range(n) for _ in range(n)
] ]
# Normalize to ensure sum is 100 # Normalize to ensure sum is 100
total = sum(eaf_values) total = sum(eaf_values)
eaf_values = [(v * 100 / total) for v in eaf_values] eaf_values = [(v * 100 / total) for v in eaf_values]
@ -37,49 +41,53 @@ async def get_all_target_reliability(*, db_session: DbSession, scope_name: str,
# Create result array of dictionaries # Create result array of dictionaries
result = [ result = [
{ {
'id': equipment.id, "id": equipment.id,
'assetnum': equipment.assetnum, "assetnum": equipment.assetnum,
'location_tag': equipment.master_equipment.location_tag, "location_tag": equipment.master_equipment.location_tag,
'name': equipment.master_equipment.name, "name": equipment.master_equipment.name,
'parent_id': equipment.master_equipment.parent_id, # Add parent_id to identify the system "parent_id": equipment.master_equipment.parent_id, # Add parent_id to identify the system
'eaf': round(eaf, 4) # Add EAF value "eaf": round(eaf, 4), # Add EAF value
} }
for equipment, eaf in zip(equipments, eaf_values) for equipment, eaf in zip(equipments, eaf_values)
] ]
# Group equipment by system # Group equipment by system
sub_system = {subsystem.id: subsystem.parent_id for subsystem in equipment_subsystem} sub_system = {
systems = {system.id: {'name': system.name, 'total_eaf': 0, 'equipments': []} for system in equipment_system} subsystem.id: subsystem.parent_id for subsystem in equipment_subsystem
}
systems = {
system.id: {"name": system.name, "total_eaf": 0, "equipments": []}
for system in equipment_system
}
for equipment in result: for equipment in result:
if equipment['parent_id'] in sub_system: if equipment["parent_id"] in sub_system:
systems[sub_system[equipment['parent_id']]]['equipments'].append(equipment) systems[sub_system[equipment["parent_id"]]]["equipments"].append(equipment)
systems[sub_system[equipment['parent_id']]]['total_eaf'] += equipment['eaf'] systems[sub_system[equipment["parent_id"]]]["total_eaf"] += equipment["eaf"]
# Convert the systems dictionary to a list of aggregated results # Convert the systems dictionary to a list of aggregated results
aggregated_result = [ aggregated_result = [
{ {
'system_id': system_id, "system_id": system_id,
'system_name': system_data['name'], "system_name": system_data["name"],
'total_eaf': round(system_data['total_eaf'], 4), "total_eaf": round(system_data["total_eaf"], 4),
'equipments': system_data['equipments'] "equipments": system_data["equipments"],
} }
for system_id, system_data in systems.items() for system_id, system_data in systems.items()
] ]
# Sort the aggregated result by total_eaf in descending order # Sort the aggregated result by total_eaf in descending order
aggregated_result.sort(key=lambda x: x['total_eaf'], reverse=True) aggregated_result.sort(key=lambda x: x["total_eaf"], reverse=True)
# Filter systems up to the threshold # Filter systems up to the threshold
cumulative_eaf = 0 cumulative_eaf = 0
filtered_aggregated_result = [] filtered_aggregated_result = []
for system in aggregated_result: for system in aggregated_result:
cumulative_eaf += system['total_eaf'] cumulative_eaf += system["total_eaf"]
filtered_aggregated_result.append(system) filtered_aggregated_result.append(system)
if cumulative_eaf >= eaf_threshold: if cumulative_eaf >= eaf_threshold:
break break
return filtered_aggregated_result return filtered_aggregated_result

@ -2,37 +2,36 @@ from typing import Optional
from uuid import UUID from uuid import UUID
import numpy as np import numpy as np
from fastapi import HTTPException from fastapi import HTTPException, status
from fastapi import status from sqlalchemy import Select, func, select
from sqlalchemy import Select
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from src.database.core import DbSession
from src.auth.service import Token from src.auth.service import Token
from src.database.core import DbSession
from src.overhaul_scope.service import get_all from src.overhaul_scope.service import get_all
from src.scope_equipment.model import ScopeEquipment from src.scope_equipment.model import ScopeEquipment
from src.scope_equipment.service import get_by_assetnum from src.scope_equipment.service import get_by_assetnum
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from .schema import CalculationTimeConstrainsParametersCreate from .schema import (CalculationTimeConstrainsParametersCreate,
from .schema import CalculationTimeConstrainsParametersRead CalculationTimeConstrainsParametersRead,
from .schema import CalculationTimeConstrainsParametersRetrive CalculationTimeConstrainsParametersRetrive,
from .schema import CalculationTimeConstrainsRead CalculationTimeConstrainsRead)
from .service import create_calculation_result_service from .service import (create_calculation_result_service, create_param_and_data,
from .service import create_param_and_data get_avg_cost_by_asset,
from .service import get_avg_cost_by_asset get_calculation_by_reference_and_parameter,
from .service import get_calculation_by_reference_and_parameter get_calculation_data_by_id, get_calculation_result,
from .service import get_calculation_data_by_id get_corrective_cost_time_chart,
from .service import get_calculation_result get_overhaul_cost_by_time_chart)
from .service import get_corrective_cost_time_chart
from .service import get_overhaul_cost_by_time_chart
async def get_create_calculation_parameters(
*, db_session: DbSession, calculation_id: Optional[str] = None
async def get_create_calculation_parameters(*, db_session: DbSession, calculation_id: Optional[str] = None): ):
if calculation_id is not None: if calculation_id is not None:
calculation = await get_calculation_data_by_id(calculation_id=calculation_id, db_session=db_session) calculation = await get_calculation_data_by_id(
calculation_id=calculation_id, db_session=db_session
)
if not calculation: if not calculation:
raise HTTPException( raise HTTPException(
@ -43,13 +42,13 @@ async def get_create_calculation_parameters(*, db_session: DbSession, calculatio
return CalculationTimeConstrainsParametersRead( return CalculationTimeConstrainsParametersRead(
costPerFailure=calculation.parameter.avg_failure_cost, costPerFailure=calculation.parameter.avg_failure_cost,
overhaulCost=calculation.parameter.overhaul_cost, overhaulCost=calculation.parameter.overhaul_cost,
reference=calculation reference=calculation,
) )
stmt = ( stmt = (
select( select(
ScopeEquipment.scope_id, ScopeEquipment.scope_id,
func.avg(MasterWorkOrder.total_cost_max).label('average_cost') func.avg(MasterWorkOrder.total_cost_max).label("average_cost"),
) )
.outerjoin(MasterWorkOrder, ScopeEquipment.assetnum == MasterWorkOrder.assetnum) .outerjoin(MasterWorkOrder, ScopeEquipment.assetnum == MasterWorkOrder.assetnum)
.group_by(ScopeEquipment.scope_id) .group_by(ScopeEquipment.scope_id)
@ -60,8 +59,10 @@ async def get_create_calculation_parameters(*, db_session: DbSession, calculatio
costFailure = results.all() costFailure = results.all()
scopes = await get_all(db_session=db_session) scopes = await get_all(db_session=db_session)
avaiableScopes = {scope.id: scope.scope_name for scope in scopes} avaiableScopes = {scope.id: scope.scope_name for scope in scopes}
costFailurePerScope = {avaiableScopes.get( costFailurePerScope = {
costPerFailure[0]): costPerFailure[1] for costPerFailure in costFailure} avaiableScopes.get(costPerFailure[0]): costPerFailure[1]
for costPerFailure in costFailure
}
return CalculationTimeConstrainsParametersRetrive( return CalculationTimeConstrainsParametersRetrive(
costPerFailure=costFailurePerScope, costPerFailure=costFailurePerScope,
@ -78,18 +79,35 @@ async def get_create_calculation_parameters(*, db_session: DbSession, calculatio
) )
async def create_calculation(*,token:str, db_session: DbSession, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, created_by: str): async def create_calculation(
*,
token: str,
db_session: DbSession,
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
created_by: str
):
calculation_data = await create_param_and_data( calculation_data = await create_param_and_data(
db_session=db_session, calculation_param_in=calculation_time_constrains_in, created_by=created_by) db_session=db_session,
calculation_param_in=calculation_time_constrains_in,
created_by=created_by,
)
results = await create_calculation_result_service(db_session=db_session, calculation=calculation_data, token=token) results = await create_calculation_result_service(
db_session=db_session, calculation=calculation_data, token=token
)
return results return results
async def get_or_create_scope_equipment_calculation(*, db_session: DbSession, scope_calculation_id, calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]): async def get_or_create_scope_equipment_calculation(
scope_calculation = await get_calculation_data_by_id(db_session=db_session, calculation_id=scope_calculation_id) *,
db_session: DbSession,
scope_calculation_id,
calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]
):
scope_calculation = await get_calculation_data_by_id(
db_session=db_session, calculation_id=scope_calculation_id
)
if not scope_calculation: if not scope_calculation:
raise HTTPException( raise HTTPException(
@ -103,5 +121,5 @@ async def get_or_create_scope_equipment_calculation(*, db_session: DbSession, sc
reference=scope_calculation.overhaul_session_id, reference=scope_calculation.overhaul_session_id,
results=scope_calculation.results, results=scope_calculation.results,
optimum_oh=scope_calculation.optimum_oh_day, optimum_oh=scope_calculation.optimum_oh_day,
equipment_results=scope_calculation.equipment_results equipment_results=scope_calculation.equipment_results,
) )

@ -1,9 +1,10 @@
from enum import Enum from enum import Enum
from typing import List, Optional, Union from typing import List, Optional, Union
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String, JSON, Numeric, Boolean
from sqlalchemy import (JSON, UUID, Boolean, Column, Float, ForeignKey,
Integer, Numeric, String)
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from src.database.core import Base, DbSession from src.database.core import Base, DbSession
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin, UUIDMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin, UUIDMixin
@ -20,8 +21,7 @@ class CalculationParam(Base, DefaultMixin, IdentityMixin):
overhaul_cost = Column(Float, nullable=False) overhaul_cost = Column(Float, nullable=False)
# Relationships # Relationships
calculation_data = relationship( calculation_data = relationship("CalculationData", back_populates="parameter")
"CalculationData", back_populates="parameter")
results = relationship("CalculationResult", back_populates="parameter") results = relationship("CalculationResult", back_populates="parameter")
# @classmethod # @classmethod
@ -60,42 +60,40 @@ class CalculationParam(Base, DefaultMixin, IdentityMixin):
class CalculationData(Base, DefaultMixin, IdentityMixin): class CalculationData(Base, DefaultMixin, IdentityMixin):
__tablename__ = "oh_tr_calculation_data" __tablename__ = "oh_tr_calculation_data"
parameter_id = Column(UUID(as_uuid=True), ForeignKey( parameter_id = Column(
'oh_ms_calculation_param.id'), nullable=True) UUID(as_uuid=True), ForeignKey("oh_ms_calculation_param.id"), nullable=True
overhaul_session_id= Column(UUID(as_uuid=True), ForeignKey('oh_ms_overhaul_scope.id')) )
overhaul_session_id = Column(
UUID(as_uuid=True), ForeignKey("oh_ms_overhaul_scope.id")
)
optimum_oh_day = Column(Integer, nullable=True) optimum_oh_day = Column(Integer, nullable=True)
session = relationship( session = relationship("OverhaulScope", lazy="raise")
"OverhaulScope", lazy="raise")
parameter = relationship("CalculationParam", back_populates="calculation_data")
parameter = relationship(
"CalculationParam", back_populates="calculation_data")
equipment_results = relationship( equipment_results = relationship(
"CalculationEquipmentResult", lazy="raise", viewonly=True "CalculationEquipmentResult", lazy="raise", viewonly=True
) )
results = relationship(
"CalculationResult", lazy="raise", viewonly=True
)
results = relationship("CalculationResult", lazy="raise", viewonly=True)
@classmethod @classmethod
async def create_with_param( async def create_with_param(
cls, cls,
overhaul_session_id: str, overhaul_session_id: str,
db: DbSession, db: DbSession,
avg_failure_cost: Optional[float], avg_failure_cost: Optional[float],
overhaul_cost: Optional[float], overhaul_cost: Optional[float],
created_by: str, created_by: str,
params_id: Optional[UUID] params_id: Optional[UUID],
): ):
if not params_id: if not params_id:
# Create Params # Create Params
params = CalculationParam( params = CalculationParam(
avg_failure_cost=avg_failure_cost, avg_failure_cost=avg_failure_cost,
overhaul_cost=overhaul_cost, overhaul_cost=overhaul_cost,
created_by=created_by created_by=created_by,
) )
db.add(params) db.add(params)
@ -105,7 +103,7 @@ class CalculationData(Base, DefaultMixin, IdentityMixin):
calculation_data = cls( calculation_data = cls(
overhaul_session_id=overhaul_session_id, overhaul_session_id=overhaul_session_id,
created_by=created_by, created_by=created_by,
parameter_id=params_id parameter_id=params_id,
) )
db.add(calculation_data) db.add(calculation_data)
@ -120,10 +118,12 @@ class CalculationResult(Base, DefaultMixin):
__tablename__ = "oh_tr_calculation_result" __tablename__ = "oh_tr_calculation_result"
parameter_id = Column(UUID(as_uuid=True), ForeignKey( parameter_id = Column(
'oh_ms_calculation_param.id'), nullable=False) UUID(as_uuid=True), ForeignKey("oh_ms_calculation_param.id"), nullable=False
calculation_data_id = Column(UUID(as_uuid=True), ForeignKey( )
'oh_tr_calculation_data.id'), nullable=False) calculation_data_id = Column(
UUID(as_uuid=True), ForeignKey("oh_tr_calculation_data.id"), nullable=False
)
day = Column(Integer, nullable=False) day = Column(Integer, nullable=False)
corrective_cost = Column(Float, nullable=False) corrective_cost = Column(Float, nullable=False)
overhaul_cost = Column(Float, nullable=False) overhaul_cost = Column(Float, nullable=False)
@ -136,25 +136,22 @@ class CalculationResult(Base, DefaultMixin):
class CalculationEquipmentResult(Base, DefaultMixin): class CalculationEquipmentResult(Base, DefaultMixin):
__tablename__ = "oh_tr_calculation_equipment_result" __tablename__ = "oh_tr_calculation_equipment_result"
corrective_costs = Column(JSON, nullable=False) corrective_costs = Column(JSON, nullable=False)
overhaul_costs = Column(JSON, nullable=False) overhaul_costs = Column(JSON, nullable=False)
daily_failures = Column(JSON, nullable=False) daily_failures = Column(JSON, nullable=False)
assetnum = Column(String(255), nullable=False) assetnum = Column(String(255), nullable=False)
material_cost = Column(Float, nullable=False) material_cost = Column(Float, nullable=False)
service_cost = Column(Float, nullable=False) service_cost = Column(Float, nullable=False)
calculation_data_id = Column(UUID(as_uuid=True), ForeignKey('oh_tr_calculation_data.id'), nullable=True) calculation_data_id = Column(
UUID(as_uuid=True), ForeignKey("oh_tr_calculation_data.id"), nullable=True
)
optimum_day = Column(Integer, default=1) optimum_day = Column(Integer, default=1)
is_included = Column(Boolean, default=True) is_included = Column(Boolean, default=True)
master_equipment = relationship( master_equipment = relationship(
"MasterEquipment", "MasterEquipment",
lazy="joined", lazy="joined",
primaryjoin="and_(CalculationEquipmentResult.assetnum == foreign(MasterEquipment.assetnum))", primaryjoin="and_(CalculationEquipmentResult.assetnum == foreign(MasterEquipment.assetnum))",
uselist=False # Add this if it's a one-to-one relationship uselist=False, # Add this if it's a one-to-one relationship
) )

@ -1,7 +1,4 @@
from typing import List, Optional, Union
from typing import List
from typing import Optional
from typing import Union
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.params import Query from fastapi.params import Query
@ -10,32 +7,47 @@ from src.auth.service import CurrentUser, Token
from src.database.core import DbSession from src.database.core import DbSession
from src.models import StandardResponse from src.models import StandardResponse
from .flows import create_calculation from .flows import (create_calculation, get_create_calculation_parameters,
from .flows import get_create_calculation_parameters get_or_create_scope_equipment_calculation)
from .flows import get_or_create_scope_equipment_calculation from .schema import (CalculationResultsRead,
from .schema import CalculationResultsRead CalculationSelectedEquipmentUpdate,
from .schema import CalculationSelectedEquipmentUpdate CalculationTimeConstrainsCreate,
from .schema import CalculationTimeConstrainsCreate CalculationTimeConstrainsParametersCreate,
from .schema import CalculationTimeConstrainsParametersCreate CalculationTimeConstrainsParametersRead,
from .schema import CalculationTimeConstrainsParametersRead CalculationTimeConstrainsParametersRetrive,
from .schema import CalculationTimeConstrainsParametersRetrive CalculationTimeConstrainsRead)
from .schema import CalculationTimeConstrainsRead from .service import (bulk_update_equipment, get_calculation_result,
from .service import get_calculation_result get_calculation_result_by_day)
from .service import get_calculation_result_by_day
from .service import bulk_update_equipment
router = APIRouter() router = APIRouter()
@router.post("", response_model=StandardResponse[Union[str, CalculationTimeConstrainsRead]]) @router.post(
async def create_calculation_time_constrains(token:Token ,db_session: DbSession, current_user: CurrentUser, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, scope_calculation_id: Optional[str] = Query(None), with_results: Optional[int] = Query(0)): "", response_model=StandardResponse[Union[str, CalculationTimeConstrainsRead]]
)
async def create_calculation_time_constrains(
token: Token,
db_session: DbSession,
current_user: CurrentUser,
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
scope_calculation_id: Optional[str] = Query(None),
with_results: Optional[int] = Query(0),
):
"""Save calculation time constrains Here""" """Save calculation time constrains Here"""
if scope_calculation_id: if scope_calculation_id:
results = await get_or_create_scope_equipment_calculation(db_session=db_session, scope_calculation_id=scope_calculation_id, calculation_time_constrains_in=calculation_time_constrains_in) results = await get_or_create_scope_equipment_calculation(
db_session=db_session,
scope_calculation_id=scope_calculation_id,
calculation_time_constrains_in=calculation_time_constrains_in,
)
else: else:
results = await create_calculation(token=token ,db_session=db_session, calculation_time_constrains_in=calculation_time_constrains_in, created_by=current_user.name) results = await create_calculation(
token=token,
db_session=db_session,
calculation_time_constrains_in=calculation_time_constrains_in,
created_by=current_user.name,
)
if not with_results: if not with_results:
results = str(results.id) results = str(results.id)
@ -43,11 +55,23 @@ async def create_calculation_time_constrains(token:Token ,db_session: DbSession,
return StandardResponse(data=results, message="Data created successfully") return StandardResponse(data=results, message="Data created successfully")
@router.get("/parameters", response_model=StandardResponse[Union[CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsParametersRead]]) @router.get(
async def get_calculation_parameters(db_session: DbSession, calculation_id: Optional[str] = Query(default=None)): "/parameters",
response_model=StandardResponse[
Union[
CalculationTimeConstrainsParametersRetrive,
CalculationTimeConstrainsParametersRead,
]
],
)
async def get_calculation_parameters(
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
):
"""Get all calculation parameter.""" """Get all calculation parameter."""
parameters = await get_create_calculation_parameters(db_session=db_session, calculation_id=calculation_id) parameters = await get_create_calculation_parameters(
db_session=db_session, calculation_id=calculation_id
)
return StandardResponse( return StandardResponse(
data=parameters, data=parameters,
@ -55,9 +79,13 @@ async def get_calculation_parameters(db_session: DbSession, calculation_id: Opti
) )
@router.get("/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]) @router.get(
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
)
async def get_calculation_results(db_session: DbSession, calculation_id): async def get_calculation_results(db_session: DbSession, calculation_id):
results = await get_calculation_result(db_session=db_session, calculation_id=calculation_id) results = await get_calculation_result(
db_session=db_session, calculation_id=calculation_id
)
return StandardResponse( return StandardResponse(
data=results, data=results,
@ -65,15 +93,37 @@ async def get_calculation_results(db_session: DbSession, calculation_id):
) )
@router.post("/{calculation_id}/simulation", response_model=StandardResponse[CalculationResultsRead]) @router.post(
async def get_simulation_result(db_session:DbSession, calculation_id, calculation_simuation_in: CalculationTimeConstrainsCreate): "/{calculation_id}/simulation",
simulation_result = await get_calculation_result_by_day(db_session=db_session, calculation_id=calculation_id, simulation_day=calculation_simuation_in.intervalDays) response_model=StandardResponse[CalculationResultsRead],
)
async def get_simulation_result(
db_session: DbSession,
calculation_id,
calculation_simuation_in: CalculationTimeConstrainsCreate,
):
simulation_result = await get_calculation_result_by_day(
db_session=db_session,
calculation_id=calculation_id,
simulation_day=calculation_simuation_in.intervalDays,
)
return StandardResponse(
data=simulation_result, message="Data retrieved successfully"
)
return StandardResponse(data=simulation_result, message="Data retrieved successfully")
@router.put("/{calculation_id}", response_model=StandardResponse[List[str]]) @router.put("/{calculation_id}", response_model=StandardResponse[List[str]])
async def update_selected_equipment(db_session: DbSession, calculation_id, calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate]): async def update_selected_equipment(
results = await bulk_update_equipment(db=db_session, selected_equipments=calculation_time_constrains_in, calculation_data_id=calculation_id) db_session: DbSession,
calculation_id,
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
):
results = await bulk_update_equipment(
db=db_session,
selected_equipments=calculation_time_constrains_in,
calculation_data_id=calculation_id,
)
return StandardResponse( return StandardResponse(
data=results, data=results,

@ -1,47 +1,41 @@
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from uuid import UUID from uuid import UUID
from pydantic import Field from pydantic import Field
from src.models import DefultBase
from dataclasses import dataclass
from src.models import DefultBase
from src.scope_equipment.schema import MasterEquipmentBase from src.scope_equipment.schema import MasterEquipmentBase
class CalculationTimeConstrainsBase(DefultBase): class CalculationTimeConstrainsBase(DefultBase):
pass pass
class ReferenceLinkBase(DefultBase): class ReferenceLinkBase(DefultBase):
reference_id: str = Field(..., description="Reference ID") reference_id: str = Field(..., description="Reference ID")
overhaul_reference_type: str = Field(..., overhaul_reference_type: str = Field(..., description="Overhaul reference type")
description="Overhaul reference type")
class CalculationTimeConstrainsParametersRetrive(CalculationTimeConstrainsBase): class CalculationTimeConstrainsParametersRetrive(CalculationTimeConstrainsBase):
# type: ignore # type: ignore
costPerFailure: Union[dict, costPerFailure: Union[dict, float] = Field(..., description="Cost per failure")
float] = Field(..., description="Cost per failure")
availableScopes: List[str] = Field(..., description="Available scopes") availableScopes: List[str] = Field(..., description="Available scopes")
recommendedScope: str = Field(..., description="Recommended scope") recommendedScope: str = Field(..., description="Recommended scope")
# historicalData: Dict[str, Any] = Field(..., description="Historical data") # historicalData: Dict[str, Any] = Field(..., description="Historical data")
class CalculationTimeConstrainsParametersRead(CalculationTimeConstrainsBase): class CalculationTimeConstrainsParametersRead(CalculationTimeConstrainsBase):
costPerFailure: Union[dict, costPerFailure: Union[dict, float] = Field(..., description="Cost per failure")
float] = Field(..., description="Cost per failure")
overhaulCost: Optional[float] = Field(None, description="Overhaul cost") overhaulCost: Optional[float] = Field(None, description="Overhaul cost")
reference: Optional[List[ReferenceLinkBase]] = Field( reference: Optional[List[ReferenceLinkBase]] = Field(None, description="Reference")
None, description="Reference")
class CalculationTimeConstrainsParametersCreate(CalculationTimeConstrainsBase): class CalculationTimeConstrainsParametersCreate(CalculationTimeConstrainsBase):
overhaulCost: Optional[float] = Field(0, description="Overhaul cost") overhaulCost: Optional[float] = Field(0, description="Overhaul cost")
ohSessionId: Optional[UUID] = Field(None, description="Scope OH") ohSessionId: Optional[UUID] = Field(None, description="Scope OH")
costPerFailure: Optional[float] = Field(0, costPerFailure: Optional[float] = Field(0, description="Cost per failure")
description="Cost per failure")
# class CalculationTimeConstrainsCreate(CalculationTimeConstrainsBase): # class CalculationTimeConstrainsCreate(CalculationTimeConstrainsBase):
@ -50,12 +44,14 @@ class CalculationTimeConstrainsParametersCreate(CalculationTimeConstrainsBase):
# costPerFailure: float = Field(..., description="Cost per failure") # costPerFailure: float = Field(..., description="Cost per failure")
# metadata: Dict[str, Any] = Field(..., description="Metadata") # metadata: Dict[str, Any] = Field(..., description="Metadata")
class CalculationResultsRead(CalculationTimeConstrainsBase): class CalculationResultsRead(CalculationTimeConstrainsBase):
day: int day: int
corrective_cost: float corrective_cost: float
overhaul_cost: float overhaul_cost: float
num_failures: int num_failures: int
class OptimumResult(CalculationTimeConstrainsBase): class OptimumResult(CalculationTimeConstrainsBase):
overhaul_cost: float overhaul_cost: float
corrective_cost: float corrective_cost: float
@ -74,6 +70,7 @@ class EquipmentResult(CalculationTimeConstrainsBase):
is_included: bool is_included: bool
master_equipment: Optional[MasterEquipmentBase] = Field(None) master_equipment: Optional[MasterEquipmentBase] = Field(None)
class CalculationTimeConstrainsRead(CalculationTimeConstrainsBase): class CalculationTimeConstrainsRead(CalculationTimeConstrainsBase):
id: UUID id: UUID
reference: UUID reference: UUID

@ -1,16 +1,11 @@
from typing import List import datetime
from typing import Optional from typing import List, Optional, Tuple
from typing import Tuple
from uuid import UUID from uuid import UUID
import numpy as np import numpy as np
from fastapi import HTTPException import requests
from fastapi import status from fastapi import HTTPException, status
from sqlalchemy import and_ from sqlalchemy import and_, case, func, select, update
from sqlalchemy import case
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from src.database.core import DbSession from src.database.core import DbSession
@ -18,20 +13,17 @@ from src.overhaul_activity.service import get_all_by_session_id
from src.overhaul_scope.service import get as get_scope from src.overhaul_scope.service import get as get_scope
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from .model import CalculationData from .model import (CalculationData, CalculationEquipmentResult,
from .model import CalculationEquipmentResult CalculationResult)
from .model import CalculationResult from .schema import (CalculationResultsRead,
from .schema import CalculationResultsRead CalculationSelectedEquipmentUpdate,
from .schema import CalculationTimeConstrainsParametersCreate CalculationTimeConstrainsParametersCreate,
from .schema import CalculationTimeConstrainsRead CalculationTimeConstrainsRead, OptimumResult)
from .schema import OptimumResult
from .schema import CalculationSelectedEquipmentUpdate
import requests
import datetime
def get_overhaul_cost_by_time_chart(overhaul_cost: float, days: int,numEquipments:int ,decay_base: float = 1.01) -> np.ndarray: def get_overhaul_cost_by_time_chart(
overhaul_cost: float, days: int, numEquipments: int, decay_base: float = 1.01
) -> np.ndarray:
if overhaul_cost < 0: if overhaul_cost < 0:
raise ValueError("Overhaul cost cannot be negative") raise ValueError("Overhaul cost cannot be negative")
if days <= 0: if days <= 0:
@ -40,7 +32,7 @@ def get_overhaul_cost_by_time_chart(overhaul_cost: float, days: int,numEquipment
exponents = np.arange(0, days) exponents = np.arange(0, days)
cost_per_equipment = overhaul_cost / numEquipments cost_per_equipment = overhaul_cost / numEquipments
# Using a slower decay base to spread the budget depletion over more days # Using a slower decay base to spread the budget depletion over more days
results = cost_per_equipment / (decay_base ** exponents) results = cost_per_equipment / (decay_base**exponents)
results = np.where(np.isfinite(results), results, 0) results = np.where(np.isfinite(results), results, 0)
return results return results
@ -61,7 +53,10 @@ def get_overhaul_cost_by_time_chart(overhaul_cost: float, days: int,numEquipment
# results = np.where(np.isfinite(results), results, 0) # results = np.where(np.isfinite(results), results, 0)
# return results # return results
async def get_corrective_cost_time_chart(material_cost: float, service_cost: float, location_tag: str, token) -> Tuple[np.ndarray, np.ndarray]:
async def get_corrective_cost_time_chart(
material_cost: float, service_cost: float, location_tag: str, token
) -> Tuple[np.ndarray, np.ndarray]:
""" """
Fetch failure data from API and calculate corrective costs, ensuring 365 days of data. Fetch failure data from API and calculate corrective costs, ensuring 365 days of data.
@ -74,14 +69,14 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo
Returns: Returns:
Tuple of (corrective_costs, daily_failure_rate) Tuple of (corrective_costs, daily_failure_rate)
""" """
url = f'http://192.168.1.82:8000/reliability/main/number-of-failures/{location_tag}/2024-01-01/2024-12-31' url = f"http://192.168.1.82:8000/reliability/main/number-of-failures/{location_tag}/2024-01-01/2024-12-31"
try: try:
response = requests.get( response = requests.get(
url, url,
headers={ headers={
'Content-Type': 'application/json', "Content-Type": "application/json",
'Authorization': f'Bearer {token}' "Authorization": f"Bearer {token}",
}, },
) )
data = response.json() data = response.json()
@ -92,8 +87,8 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo
# Create a dictionary of existing data # Create a dictionary of existing data
data_dict = { data_dict = {
datetime.datetime.strptime(item['date'], '%d %b %Y'): item['num_fail'] datetime.datetime.strptime(item["date"], "%d %b %Y"): item["num_fail"]
for item in data['data'] for item in data["data"]
} }
# Fill in missing dates with nearest available value # Fill in missing dates with nearest available value
@ -121,6 +116,7 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo
print(f"Error fetching or processing data: {str(e)}") print(f"Error fetching or processing data: {str(e)}")
raise raise
# def get_corrective_cost_time_chart(material_cost: float, service_cost: float, days: int, numEquipments: int) -> Tuple[np.ndarray, np.ndarray]: # def get_corrective_cost_time_chart(material_cost: float, service_cost: float, days: int, numEquipments: int) -> Tuple[np.ndarray, np.ndarray]:
# day_points = np.arange(0, days) # day_points = np.arange(0, days)
@ -147,12 +143,18 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo
# return corrective_costs, daily_failure_rate # return corrective_costs, daily_failure_rate
async def create_param_and_data(*, db_session: DbSession, calculation_param_in: CalculationTimeConstrainsParametersCreate, created_by: str, parameter_id: Optional[UUID] = None): async def create_param_and_data(
*,
db_session: DbSession,
calculation_param_in: CalculationTimeConstrainsParametersCreate,
created_by: str,
parameter_id: Optional[UUID] = None,
):
"""Creates a new document.""" """Creates a new document."""
if calculation_param_in.ohSessionId is None: if calculation_param_in.ohSessionId is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="overhaul_session_id is required" detail="overhaul_session_id is required",
) )
calculationData = await CalculationData.create_with_param( calculationData = await CalculationData.create_with_param(
@ -161,36 +163,39 @@ async def create_param_and_data(*, db_session: DbSession, calculation_param_in:
avg_failure_cost=calculation_param_in.costPerFailure, avg_failure_cost=calculation_param_in.costPerFailure,
overhaul_cost=calculation_param_in.overhaulCost, overhaul_cost=calculation_param_in.overhaulCost,
created_by=created_by, created_by=created_by,
params_id=parameter_id params_id=parameter_id,
) )
return calculationData return calculationData
async def get_calculation_result(db_session: DbSession, calculation_id: str): async def get_calculation_result(db_session: DbSession, calculation_id: str):
days=365 days = 365
scope_calculation = await get_calculation_data_by_id(db_session=db_session, calculation_id=calculation_id) scope_calculation = await get_calculation_data_by_id(
db_session=db_session, calculation_id=calculation_id
)
if not scope_calculation: if not scope_calculation:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.", detail="A data with this id does not exist.",
) )
scope_overhaul = await get_scope(db_session=db_session, overhaul_session_id=scope_calculation.overhaul_session_id) scope_overhaul = await get_scope(
db_session=db_session, overhaul_session_id=scope_calculation.overhaul_session_id
)
if not scope_overhaul: if not scope_overhaul:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.", detail="A data with this id does not exist.",
) )
calculation_results = [] calculation_results = []
for i in range(days): for i in range(days):
result = { result = {
"overhaul_cost": 0, "overhaul_cost": 0,
"corrective_cost": 0, "corrective_cost": 0,
"num_failures": 0, "num_failures": 0,
"day": i + 1 "day": i + 1,
} }
for eq in scope_calculation.equipment_results: for eq in scope_calculation.equipment_results:
@ -200,10 +205,8 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str):
result["overhaul_cost"] += float(eq.overhaul_costs[i]) result["overhaul_cost"] += float(eq.overhaul_costs[i])
result["num_failures"] += int(eq.daily_failures[i]) result["num_failures"] += int(eq.daily_failures[i])
calculation_results.append(CalculationResultsRead(**result)) calculation_results.append(CalculationResultsRead(**result))
# Check if calculation already exist # Check if calculation already exist
return CalculationTimeConstrainsRead( return CalculationTimeConstrainsRead(
id=scope_calculation.id, id=scope_calculation.id,
@ -211,18 +214,22 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str):
scope=scope_overhaul.type, scope=scope_overhaul.type,
results=calculation_results, results=calculation_results,
optimum_oh=scope_calculation.optimum_oh_day, optimum_oh=scope_calculation.optimum_oh_day,
equipment_results=scope_calculation.equipment_results equipment_results=scope_calculation.equipment_results,
) )
async def get_calculation_data_by_id(db_session: DbSession, calculation_id) -> CalculationData: async def get_calculation_data_by_id(
stmt = select(CalculationData).filter( db_session: DbSession, calculation_id
CalculationData.id == calculation_id ) -> CalculationData:
).options( stmt = (
joinedload(CalculationData.equipment_results), joinedload(CalculationData.parameter) select(CalculationData)
.filter(CalculationData.id == calculation_id)
.options(
joinedload(CalculationData.equipment_results),
joinedload(CalculationData.parameter),
)
) )
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
return result.unique().scalar() return result.unique().scalar()
@ -287,20 +294,21 @@ async def get_calculation_data_by_id(db_session: DbSession, calculation_id) -> C
async def create_calculation_result_service( async def create_calculation_result_service(
db_session: DbSession, db_session: DbSession, calculation: CalculationData, token: str
calculation: CalculationData,
token: str
) -> CalculationTimeConstrainsRead: ) -> CalculationTimeConstrainsRead:
days = 365 # Changed to 365 days as per requirement days = 365 # Changed to 365 days as per requirement
# Get all equipment for this calculation session # Get all equipment for this calculation session
equipments = await get_all_by_session_id(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id) equipments = await get_all_by_session_id(
scope = await get_scope(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id) db_session=db_session, overhaul_session_id=calculation.overhaul_session_id
)
calculation_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calculation.id) scope = await get_scope(
db_session=db_session, overhaul_session_id=calculation.overhaul_session_id
)
calculation_data = await get_calculation_data_by_id(
db_session=db_session, calculation_id=calculation.id
)
# Store results for each equipment # Store results for each equipment
equipment_results: List[CalculationEquipmentResult] = [] equipment_results: List[CalculationEquipmentResult] = []
@ -313,31 +321,33 @@ async def create_calculation_result_service(
material_cost=eq.material_cost, material_cost=eq.material_cost,
service_cost=eq.service_cost, service_cost=eq.service_cost,
token=token, token=token,
location_tag=eq.equipment.location_tag location_tag=eq.equipment.location_tag,
) )
overhaul_cost_points = get_overhaul_cost_by_time_chart( overhaul_cost_points = get_overhaul_cost_by_time_chart(
calculation_data.parameter.overhaul_cost, calculation_data.parameter.overhaul_cost,
days=len(corrective_costs), days=len(corrective_costs),
numEquipments=len(equipments) numEquipments=len(equipments),
) )
# Calculate individual equipment optimum points # Calculate individual equipment optimum points
equipment_total_cost = corrective_costs + overhaul_cost_points equipment_total_cost = corrective_costs + overhaul_cost_points
equipment_optimum_index = np.argmin(equipment_total_cost) equipment_optimum_index = np.argmin(equipment_total_cost)
equipment_failure_sum = sum(daily_failures[:equipment_optimum_index]) equipment_failure_sum = sum(daily_failures[:equipment_optimum_index])
equipment_results.append(CalculationEquipmentResult( equipment_results.append(
corrective_costs=corrective_costs.tolist(), CalculationEquipmentResult(
overhaul_costs=overhaul_cost_points.tolist(), corrective_costs=corrective_costs.tolist(),
daily_failures=daily_failures.tolist(), overhaul_costs=overhaul_cost_points.tolist(),
assetnum=eq.assetnum, daily_failures=daily_failures.tolist(),
material_cost=eq.material_cost, assetnum=eq.assetnum,
service_cost=eq.service_cost, material_cost=eq.material_cost,
optimum_day=int(equipment_optimum_index + 1), service_cost=eq.service_cost,
calculation_data_id=calculation.id, optimum_day=int(equipment_optimum_index + 1),
master_equipment=eq.equipment calculation_data_id=calculation.id,
)) master_equipment=eq.equipment,
)
)
# Add to totals # Add to totals
total_corrective_costs += corrective_costs total_corrective_costs += corrective_costs
@ -345,7 +355,6 @@ async def create_calculation_result_service(
db_session.add_all(equipment_results) db_session.add_all(equipment_results)
# Calculate optimum points using total costs # Calculate optimum points using total costs
total_cost = total_corrective_costs + overhaul_cost_points total_cost = total_corrective_costs + overhaul_cost_points
optimum_oh_index = np.argmin(total_cost) optimum_oh_index = np.argmin(total_cost)
@ -355,7 +364,7 @@ async def create_calculation_result_service(
overhaul_cost=float(overhaul_cost_points[optimum_oh_index]), overhaul_cost=float(overhaul_cost_points[optimum_oh_index]),
corrective_cost=float(total_corrective_costs[optimum_oh_index]), corrective_cost=float(total_corrective_costs[optimum_oh_index]),
num_failures=int(numbers_of_failure), num_failures=int(numbers_of_failure),
days=int(optimum_oh_index + 1) days=int(optimum_oh_index + 1),
) )
# # Create calculation results for database # # Create calculation results for database
@ -376,7 +385,6 @@ async def create_calculation_result_service(
await db_session.commit() await db_session.commit()
# Return results including individual equipment data # Return results including individual equipment data
return CalculationTimeConstrainsRead( return CalculationTimeConstrainsRead(
id=calculation.id, id=calculation.id,
@ -384,26 +392,34 @@ async def create_calculation_result_service(
scope=scope.type, scope=scope.type,
results=[], results=[],
optimum_oh=optimum, optimum_oh=optimum,
equipment_results=equipment_results equipment_results=equipment_results,
) )
async def get_calculation_by_reference_and_parameter(*, db_session: DbSession, calculation_reference_id, parameter_id): async def get_calculation_by_reference_and_parameter(
stmt = select(CalculationData).filter(and_( *, db_session: DbSession, calculation_reference_id, parameter_id
CalculationData.reference_id == calculation_reference_id, ):
CalculationData.parameter_id == parameter_id, stmt = select(CalculationData).filter(
)) and_(
CalculationData.reference_id == calculation_reference_id,
CalculationData.parameter_id == parameter_id,
)
)
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
return result.scalar() return result.scalar()
async def get_calculation_result_by_day(*, db_session: DbSession, calculation_id, simulation_day): async def get_calculation_result_by_day(
stmt = select(CalculationResult).filter(and_( *, db_session: DbSession, calculation_id, simulation_day
CalculationResult.day == simulation_day, ):
CalculationResult.calculation_data_id == calculation_id stmt = select(CalculationResult).filter(
)) and_(
CalculationResult.day == simulation_day,
CalculationResult.calculation_data_id == calculation_id,
)
)
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
@ -411,21 +427,22 @@ async def get_calculation_result_by_day(*, db_session: DbSession, calculation_id
async def get_avg_cost_by_asset(*, db_session: DbSession, assetnum: str): async def get_avg_cost_by_asset(*, db_session: DbSession, assetnum: str):
stmt = ( stmt = select(func.avg(MasterWorkOrder.total_cost_max).label("average_cost")).where(
select(func.avg(MasterWorkOrder.total_cost_max).label('average_cost')) MasterWorkOrder.assetnum == assetnum
.where(MasterWorkOrder.assetnum == assetnum)
) )
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def bulk_update_equipment(*, db: DbSession, selected_equipments: List[CalculationSelectedEquipmentUpdate], calculation_data_id: UUID): async def bulk_update_equipment(
*,
db: DbSession,
selected_equipments: List[CalculationSelectedEquipmentUpdate],
calculation_data_id: UUID,
):
# Create a dictionary mapping assetnum to is_included status # Create a dictionary mapping assetnum to is_included status
case_mappings = { case_mappings = {asset.assetnum: asset.is_included for asset in selected_equipments}
asset.assetnum: asset.is_included
for asset in selected_equipments
}
# Get all assetnums that need to be updated # Get all assetnums that need to be updated
assetnums = list(case_mappings.keys()) assetnums = list(case_mappings.keys())
@ -441,9 +458,13 @@ async def bulk_update_equipment(*, db: DbSession, selected_equipments: List[Calc
update(CalculationEquipmentResult) update(CalculationEquipmentResult)
.where(CalculationEquipmentResult.calculation_data_id == calculation_data_id) .where(CalculationEquipmentResult.calculation_data_id == calculation_data_id)
.where(CalculationEquipmentResult.assetnum.in_(assetnums)) .where(CalculationEquipmentResult.assetnum.in_(assetnums))
.values({ .values(
"is_included": case(*when_clauses) # Unpack the when clauses as separate arguments {
}) "is_included": case(
*when_clauses
) # Unpack the when clauses as separate arguments
}
)
) )
await db.execute(stmt) await db.execute(stmt)

@ -1,14 +1,13 @@
import base64
import logging import logging
import os import os
import base64
from urllib import parse
from typing import List from typing import List
from pydantic import BaseModel from urllib import parse
from pydantic import BaseModel
from starlette.config import Config from starlette.config import Config
from starlette.datastructures import CommaSeparatedStrings from starlette.datastructures import CommaSeparatedStrings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -60,10 +59,10 @@ _QUOTED_DATABASE_PASSWORD = parse.quote(str(_DATABASE_CREDENTIAL_PASSWORD))
DATABASE_NAME = config("DATABASE_NAME", default="digital_twin") DATABASE_NAME = config("DATABASE_NAME", default="digital_twin")
DATABASE_PORT = config("DATABASE_PORT", default="5432") DATABASE_PORT = config("DATABASE_PORT", default="5432")
DATABASE_ENGINE_POOL_SIZE = config( DATABASE_ENGINE_POOL_SIZE = config("DATABASE_ENGINE_POOL_SIZE", cast=int, default=20)
"DATABASE_ENGINE_POOL_SIZE", cast=int, default=20)
DATABASE_ENGINE_MAX_OVERFLOW = config( DATABASE_ENGINE_MAX_OVERFLOW = config(
"DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0) "DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0
)
# Deal with DB disconnects # Deal with DB disconnects
# https://docs.sqlalchemy.org/en/20/core/pooling.html#pool-disconnects # https://docs.sqlalchemy.org/en/20/core/pooling.html#pool-disconnects
DATABASE_ENGINE_POOL_PING = config("DATABASE_ENGINE_POOL_PING", default=False) DATABASE_ENGINE_POOL_PING = config("DATABASE_ENGINE_POOL_PING", default=False)
@ -74,5 +73,4 @@ TIMEZONE = "Asia/Jakarta"
MAXIMO_BASE_URL = config("MAXIMO_BASE_URL", default="http://example.com") MAXIMO_BASE_URL = config("MAXIMO_BASE_URL", default="http://example.com")
MAXIMO_API_KEY = config("MAXIMO_API_KEY", default="keys") MAXIMO_API_KEY = config("MAXIMO_API_KEY", default="keys")
AUTH_SERVICE_API = config( AUTH_SERVICE_API = config("AUTH_SERVICE_API", default="http://192.168.1.82:8000/auth")
"AUTH_SERVICE_API", default="http://192.168.1.82:8000/auth")

@ -1,27 +1,23 @@
# src/database.py # src/database.py
from starlette.requests import Request
from sqlalchemy_utils import get_mapper
from sqlalchemy.sql.expression import true
from sqlalchemy.orm import object_session, sessionmaker, Session
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy import create_engine, inspect
from pydantic import BaseModel
from fastapi import Depends
from typing import Annotated, Any
from contextlib import contextmanager
import re
import functools import functools
from typing import AsyncGenerator import re
from contextlib import contextmanager
from typing import Annotated, Any, AsyncGenerator
from fastapi import Depends
from pydantic import BaseModel
from sqlalchemy import create_engine, inspect
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import (DeclarativeBase, Session, object_session,
sessionmaker)
from sqlalchemy.sql.expression import true
from sqlalchemy_utils import get_mapper
from starlette.requests import Request
from src.config import SQLALCHEMY_DATABASE_URI from src.config import SQLALCHEMY_DATABASE_URI
engine = create_async_engine( engine = create_async_engine(SQLALCHEMY_DATABASE_URI, echo=False, future=True)
SQLALCHEMY_DATABASE_URI,
echo=False,
future=True
)
async_session = sessionmaker( async_session = sessionmaker(
engine, engine,
@ -68,9 +64,8 @@ class CustomBase:
for key in self.__repr_attrs__: for key in self.__repr_attrs__:
if not hasattr(self, key): if not hasattr(self, key):
raise KeyError( raise KeyError(
"{} has incorrect attribute '{}' in " "__repr__attrs__".format( "{} has incorrect attribute '{}' in "
self.__class__, key "__repr__attrs__".format(self.__class__, key)
)
) )
value = getattr(self, key) value = getattr(self, key)
wrap_in_quote = isinstance(value, str) wrap_in_quote = isinstance(value, str)

@ -1,15 +1,13 @@
import logging import logging
from typing import Annotated, List from typing import Annotated, List
from sqlalchemy import desc, func, or_, Select from fastapi import Depends, Query
from sqlalchemy_filters import apply_pagination from pydantic.types import Json, constr
from sqlalchemy import Select, desc, func, or_
from sqlalchemy.exc import ProgrammingError from sqlalchemy.exc import ProgrammingError
from .core import DbSession from sqlalchemy_filters import apply_pagination
from fastapi import Query, Depends from .core import DbSession
from pydantic.types import Json, constr
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -27,7 +25,7 @@ def common_parameters(
sort_by: List[str] = Query([], alias="sortBy[]"), sort_by: List[str] = Query([], alias="sortBy[]"),
descending: List[bool] = Query([], alias="descending[]"), descending: List[bool] = Query([], alias="descending[]"),
exclude: List[str] = Query([], alias="exclude[]"), exclude: List[str] = Query([], alias="exclude[]"),
all: int = Query(0) all: int = Query(0),
# role: QueryStr = Depends(get_current_role), # role: QueryStr = Depends(get_current_role),
): ):
return { return {
@ -39,14 +37,13 @@ def common_parameters(
"sort_by": sort_by, "sort_by": sort_by,
"descending": descending, "descending": descending,
"current_user": current_user, "current_user": current_user,
"all": bool(all) "all": bool(all),
# "role": role, # "role": role,
} }
CommonParameters = Annotated[ CommonParameters = Annotated[
dict[str, int | str | DbSession | QueryStr | dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool,
Json | List[str] | List[bool]] | bool,
Depends(common_parameters), Depends(common_parameters),
] ]
@ -75,8 +72,7 @@ def search(*, query_str: str, query: Query, model, sort=False):
query = query.filter(or_(*search)) query = query.filter(or_(*search))
if sort: if sort:
query = query.order_by( query = query.order_by(desc(func.ts_rank_cd(vector, func.tsq_parse(query_str))))
desc(func.ts_rank_cd(vector, func.tsq_parse(query_str))))
return query.params(term=query_str) return query.params(term=query_str)
@ -92,7 +88,7 @@ async def search_filter_sort_paginate(
descending: List[bool] = None, descending: List[bool] = None,
current_user: str = None, current_user: str = None,
exclude: List[str] = None, exclude: List[str] = None,
all: bool = False all: bool = False,
): ):
"""Common functionality for searching, filtering, sorting, and pagination.""" """Common functionality for searching, filtering, sorting, and pagination."""
# try: # try:
@ -104,8 +100,7 @@ async def search_filter_sort_paginate(
if query_str: if query_str:
sort = False if sort_by else True sort = False if sort_by else True
query = search(query_str=query_str, query=query, query = search(query_str=query_str, query=query, model=model, sort=sort)
model=model, sort=sort)
# Get total count # Get total count
count_query = Select(func.count()).select_from(query.subquery()) count_query = Select(func.count()).select_from(query.subquery())
@ -123,11 +118,7 @@ async def search_filter_sort_paginate(
"totalPages": 1, "totalPages": 1,
} }
query = ( query = query.offset((page - 1) * items_per_page).limit(items_per_page)
query
.offset((page - 1) * items_per_page)
.limit(items_per_page)
)
result = await db_session.execute(query) result = await db_session.execute(query)
items = result.scalars().all() items = result.scalars().all()

@ -21,4 +21,4 @@ class OptimumOHEnum(StrEnum):
class ResponseStatus(OptimumOHEnum): class ResponseStatus(OptimumOHEnum):
SUCCESS = "success" SUCCESS = "success"
ERROR = "error" ERROR = "error"

@ -1,17 +1,19 @@
# Define base error model # Define base error model
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from asyncpg.exceptions import DataError as AsyncPGDataError
from asyncpg.exceptions import PostgresError
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
from src.enums import ResponseStatus
from slowapi import _rate_limit_exceeded_handler from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from sqlalchemy.exc import (DataError, DBAPIError, IntegrityError,
SQLAlchemyError)
from sqlalchemy.exc import SQLAlchemyError, IntegrityError, DataError, DBAPIError from src.enums import ResponseStatus
from asyncpg.exceptions import DataError as AsyncPGDataError, PostgresError
class ErrorDetail(BaseModel): class ErrorDetail(BaseModel):
@ -27,6 +29,7 @@ class ErrorResponse(BaseModel):
status: ResponseStatus = ResponseStatus.ERROR status: ResponseStatus = ResponseStatus.ERROR
errors: Optional[List[ErrorDetail]] = None errors: Optional[List[ErrorDetail]] = None
# Custom exception handler setup # Custom exception handler setup
@ -64,7 +67,7 @@ def handle_sqlalchemy_error(error: SQLAlchemyError):
""" """
Handle SQLAlchemy errors and return user-friendly error messages. Handle SQLAlchemy errors and return user-friendly error messages.
""" """
original_error = getattr(error, 'orig', None) original_error = getattr(error, "orig", None)
print(original_error) print(original_error)
if isinstance(error, IntegrityError): if isinstance(error, IntegrityError):
@ -113,12 +116,8 @@ def handle_exception(request: Request, exc: Exception):
"data": None, "data": None,
"message": str(exc.detail), "message": str(exc.detail),
"status": ResponseStatus.ERROR, "status": ResponseStatus.ERROR,
"errors": [ "errors": [ErrorDetail(message=str(exc.detail)).model_dump()],
ErrorDetail( },
message=str(exc.detail)
).model_dump()
]
}
) )
if isinstance(exc, SQLAlchemyError): if isinstance(exc, SQLAlchemyError):
@ -134,12 +133,8 @@ def handle_exception(request: Request, exc: Exception):
"data": None, "data": None,
"message": error_message, "message": error_message,
"status": ResponseStatus.ERROR, "status": ResponseStatus.ERROR,
"errors": [ "errors": [ErrorDetail(message=error_message).model_dump()],
ErrorDetail( },
message=error_message
).model_dump()
]
}
) )
# Log unexpected errors # Log unexpected errors
@ -155,9 +150,7 @@ def handle_exception(request: Request, exc: Exception):
"message": str(exc), "message": str(exc),
"status": ResponseStatus.ERROR, "status": ResponseStatus.ERROR,
"errors": [ "errors": [
ErrorDetail( ErrorDetail(message="An unexpected error occurred.").model_dump()
message="An unexpected error occurred." ],
).model_dump() },
]
}
) )

@ -1,12 +1,10 @@
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from sqlalchemy.ext.hybrid import hybrid_property
class MasterActivitytask(Base, DefaultMixin): class MasterActivitytask(Base, DefaultMixin):
@ -14,8 +12,11 @@ class MasterActivitytask(Base, DefaultMixin):
description = Column(String, nullable=False) description = Column(String, nullable=False)
oh_type = Column(String, nullable=False) oh_type = Column(String, nullable=False)
job_id = Column(UUID(as_uuid=True), ForeignKey( job_id = Column(
"oh_ms_job.id", ondelete="cascade"), nullable=False) UUID(as_uuid=True),
ForeignKey("oh_ms_job.id", ondelete="cascade"),
nullable=False,
)
class MasterActivity(Base, DefaultMixin): class MasterActivity(Base, DefaultMixin):

@ -1,12 +1,12 @@
from fastapi import APIRouter, HTTPException, Query, status from fastapi import APIRouter, HTTPException, Query, status
from src.database.service import (CommonParameters, DbSession,
from .service import get_all, create, get, update, delete search_filter_sort_paginate)
from .schema import ActivityMaster, ActivityMasterCreate, ActivityMasterPagination
from src.models import StandardResponse from src.models import StandardResponse
from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession
from .schema import (ActivityMaster, ActivityMasterCreate,
ActivityMasterPagination)
from .service import create, delete, get, get_all, update
router = APIRouter() router = APIRouter()
@ -31,7 +31,9 @@ async def create_activity(db_session: DbSession, activity_in: ActivityMasterCrea
return StandardResponse(data=activity, message="Data created successfully") return StandardResponse(data=activity, message="Data created successfully")
@router.get("/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]) @router.get(
"/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
)
async def get_activity(db_session: DbSession, activity_id: str): async def get_activity(db_session: DbSession, activity_id: str):
activity = await get(db_session=db_session, activity_id=activity_id) activity = await get(db_session=db_session, activity_id=activity_id)
if not activity: if not activity:
@ -43,8 +45,12 @@ async def get_activity(db_session: DbSession, activity_id: str):
return StandardResponse(data=activity, message="Data retrieved successfully") return StandardResponse(data=activity, message="Data retrieved successfully")
@router.put("/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]) @router.put(
async def update_scope(db_session: DbSession, activity_in: ActivityMasterCreate, activity_id): "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
)
async def update_scope(
db_session: DbSession, activity_in: ActivityMasterCreate, activity_id
):
activity = await get(db_session=db_session, activity_id=activity_id) activity = await get(db_session=db_session, activity_id=activity_id)
if not activity: if not activity:
@ -53,10 +59,17 @@ async def update_scope(db_session: DbSession, activity_in: ActivityMasterCreate,
detail="A data with this id does not exist.", detail="A data with this id does not exist.",
) )
return StandardResponse(data=await update(db_session=db_session, activity=activity, activity_in=activity_in), message="Data updated successfully") return StandardResponse(
data=await update(
db_session=db_session, activity=activity, activity_in=activity_in
),
message="Data updated successfully",
)
@router.delete("/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]) @router.delete(
"/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
)
async def delete_scope(db_session: DbSession, activity_id: str): async def delete_scope(db_session: DbSession, activity_id: str):
activity = await get(db_session=db_session, activity_id=activity_id) activity = await get(db_session=db_session, activity_id=activity_id)

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, BaseModel from pydantic import BaseModel, Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
@ -18,10 +18,12 @@ class ActivityMasterDetail(DefultBase):
class ActivityMasterCreate(ActivityMaster): class ActivityMasterCreate(ActivityMaster):
description: str description: str
class ActivityMasterTasks(DefultBase): class ActivityMasterTasks(DefultBase):
description: str description: str
oh_type: str oh_type: str
class ActivityMasterRead(ActivityMaster): class ActivityMasterRead(ActivityMaster):
id: UUID id: UUID
workscope: str workscope: str

@ -1,15 +1,14 @@
from sqlalchemy import Select, Delete
from sqlalchemy.orm import joinedload, selectinload
from typing import Optional from typing import Optional
from .model import MasterActivity from sqlalchemy import Delete, Select
from .schema import ActivityMaster, ActivityMasterCreate from sqlalchemy.orm import joinedload, selectinload
from src.auth.service import CurrentUser
from src.database.core import DbSession from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate from src.database.service import CommonParameters, search_filter_sort_paginate
from src.auth.service import CurrentUser
from .model import MasterActivity
from .schema import ActivityMaster, ActivityMasterCreate
async def get(*, db_session: DbSession, activity_id: str) -> Optional[ActivityMaster]: async def get(*, db_session: DbSession, activity_id: str) -> Optional[ActivityMaster]:
@ -27,14 +26,18 @@ async def get_all(common: CommonParameters):
async def create(*, db_session: DbSession, activty_in: ActivityMasterCreate): async def create(*, db_session: DbSession, activty_in: ActivityMasterCreate):
activity = MasterActivity( activity = MasterActivity(**activty_in.model_dump())
**activty_in.model_dump())
db_session.add(activity) db_session.add(activity)
await db_session.commit() await db_session.commit()
return activity return activity
async def update(*, db_session: DbSession, activity: MasterActivity, activity_in: ActivityMasterCreate): async def update(
*,
db_session: DbSession,
activity: MasterActivity,
activity_in: ActivityMasterCreate
):
"""Updates a document.""" """Updates a document."""
data = activity_in.model_dump() data = activity_in.model_dump()

@ -3,7 +3,6 @@ import logging
from src.config import LOG_LEVEL from src.config import LOG_LEVEL
from src.enums import OptimumOHEnum from src.enums import OptimumOHEnum
LOG_FORMAT_DEBUG = "%(levelname)s:%(message)s:%(pathname)s:%(funcName)s:%(lineno)d" LOG_FORMAT_DEBUG = "%(levelname)s:%(message)s:%(pathname)s:%(funcName)s:%(lineno)d"

@ -1,37 +1,33 @@
import time
import logging import logging
import time
from contextvars import ContextVar
from os import path from os import path
from typing import Final, Optional
from uuid import uuid1 from uuid import uuid1
from typing import Optional, Final
from contextvars import ContextVar
from fastapi import FastAPI, HTTPException, status from fastapi import FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import ValidationError from pydantic import ValidationError
from slowapi import _rate_limit_exceeded_handler from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from sqlalchemy import inspect from sqlalchemy import inspect
from sqlalchemy.orm import scoped_session
from sqlalchemy.ext.asyncio import async_scoped_session from sqlalchemy.ext.asyncio import async_scoped_session
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from sqlalchemy.orm import scoped_session
from starlette.middleware.base import (BaseHTTPMiddleware,
RequestResponseEndpoint)
from starlette.middleware.gzip import GZipMiddleware
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import FileResponse, Response, StreamingResponse
from starlette.routing import compile_path from starlette.routing import compile_path
from starlette.middleware.gzip import GZipMiddleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import Response, StreamingResponse, FileResponse
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
import logging
from src.api import api_router
from src.database.core import async_session, engine
from src.enums import ResponseStatus from src.enums import ResponseStatus
from src.exceptions import handle_exception
from src.logging import configure_logging from src.logging import configure_logging
from src.rate_limiter import limiter from src.rate_limiter import limiter
from src.api import api_router
from src.database.core import engine, async_session
from src.exceptions import handle_exception
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -42,9 +38,13 @@ configure_logging()
exception_handlers = {Exception: handle_exception} exception_handlers = {Exception: handle_exception}
# we create the ASGI for the app # we create the ASGI for the app
app = FastAPI(exception_handlers=exception_handlers, openapi_url="", title="LCCA API", app = FastAPI(
description="Welcome to LCCA's API documentation!", exception_handlers=exception_handlers,
version="0.1.0") openapi_url="",
title="LCCA API",
description="Welcome to LCCA's API documentation!",
version="0.1.0",
)
app.state.limiter = limiter app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(GZipMiddleware, minimum_size=2000) app.add_middleware(GZipMiddleware, minimum_size=2000)
@ -53,7 +53,8 @@ app.add_middleware(GZipMiddleware, minimum_size=2000)
REQUEST_ID_CTX_KEY: Final[str] = "request_id" REQUEST_ID_CTX_KEY: Final[str] = "request_id"
_request_id_ctx_var: ContextVar[Optional[str]] = ContextVar( _request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(
REQUEST_ID_CTX_KEY, default=None) REQUEST_ID_CTX_KEY, default=None
)
def get_request_id() -> Optional[str]: def get_request_id() -> Optional[str]:
@ -84,9 +85,12 @@ async def db_session_middleware(request: Request, call_next):
@app.middleware("http") @app.middleware("http")
async def add_security_headers(request: Request, call_next): async def add_security_headers(request: Request, call_next):
response = await call_next(request) response = await call_next(request)
response.headers["Strict-Transport-Security"] = "max-age=31536000 ; includeSubDomains" response.headers["Strict-Transport-Security"] = (
"max-age=31536000 ; includeSubDomains"
)
return response return response
# class MetricsMiddleware(BaseHTTPMiddleware): # class MetricsMiddleware(BaseHTTPMiddleware):
# async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: # async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
# method = request.method # method = request.method

@ -1,10 +1,10 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict from typing import Any, Dict
from fastapi import HTTPException
import httpx import httpx
from fastapi import HTTPException
from starlette.config import Config from starlette.config import Config
from src.config import MAXIMO_API_KEY, MAXIMO_BASE_URL from src.config import MAXIMO_API_KEY, MAXIMO_BASE_URL
@ -24,7 +24,7 @@ class MaximoDataMapper:
Example: might be data['startDate'] or data['SCHEDSTART'] etc. Example: might be data['startDate'] or data['SCHEDSTART'] etc.
""" """
# This is a placeholder - update with actual MAXIMO field name # This is a placeholder - update with actual MAXIMO field name
start_date_str = self.data.get('scheduleStart') start_date_str = self.data.get("scheduleStart")
if not start_date_str: if not start_date_str:
raise ValueError("Start date not found in MAXIMO data") raise ValueError("Start date not found in MAXIMO data")
return datetime.fromisoformat(start_date_str) return datetime.fromisoformat(start_date_str)
@ -35,7 +35,7 @@ class MaximoDataMapper:
TODO: Update this based on actual MAXIMO API response structure TODO: Update this based on actual MAXIMO API response structure
""" """
# This is a placeholder - update with actual MAXIMO field name # This is a placeholder - update with actual MAXIMO field name
end_date_str = self.data.get('scheduleEnd') end_date_str = self.data.get("scheduleEnd")
if not end_date_str: if not end_date_str:
raise ValueError("End date not found in MAXIMO data") raise ValueError("End date not found in MAXIMO data")
return datetime.fromisoformat(end_date_str) return datetime.fromisoformat(end_date_str)
@ -46,7 +46,7 @@ class MaximoDataMapper:
TODO: Update this based on actual MAXIMO API response structure TODO: Update this based on actual MAXIMO API response structure
""" """
# This is a placeholder - update with actual MAXIMO field name # This is a placeholder - update with actual MAXIMO field name
maximo_id = self.data.get('workOrderId') maximo_id = self.data.get("workOrderId")
if not maximo_id: if not maximo_id:
raise ValueError("MAXIMO ID not found in response") raise ValueError("MAXIMO ID not found in response")
return str(maximo_id) return str(maximo_id)
@ -57,7 +57,7 @@ class MaximoDataMapper:
TODO: Update this based on actual MAXIMO API response structure TODO: Update this based on actual MAXIMO API response structure
""" """
# This is a placeholder - update with actual MAXIMO status field and values # This is a placeholder - update with actual MAXIMO status field and values
status = self.data.get('status', '').upper() status = self.data.get("status", "").upper()
return status return status
def get_total_cost(self) -> float: def get_total_cost(self) -> float:
@ -66,11 +66,11 @@ class MaximoDataMapper:
TODO: Update this based on actual MAXIMO API response structure TODO: Update this based on actual MAXIMO API response structure
""" """
# This is a placeholder - update with actual MAXIMO field name # This is a placeholder - update with actual MAXIMO field name
cost = self.data.get('totalCost', 0) cost = self.data.get("totalCost", 0)
return float(cost) return float(cost)
def get_scope_name(self) -> str: def get_scope_name(self) -> str:
scope_name = self.data.get('location', "A") scope_name = self.data.get("location", "A")
return scope_name return scope_name
@ -86,10 +86,8 @@ class MaximoService:
TODO: Update this method based on actual MAXIMO API endpoints and parameters TODO: Update this method based on actual MAXIMO API endpoints and parameters
""" """
current_date = datetime.now() current_date = datetime.now()
schedule_start = current_date + \ schedule_start = current_date + timedelta(days=30) # Starting in 30 days
timedelta(days=30) # Starting in 30 days schedule_end = schedule_start + timedelta(days=90) # 90 day overhaul period
schedule_end = schedule_start + \
timedelta(days=90) # 90 day overhaul period
return { return {
"scheduleStart": schedule_start.isoformat(), "scheduleStart": schedule_start.isoformat(),
@ -104,19 +102,19 @@ class MaximoService:
{ {
"assetnum": "ASSET001", "assetnum": "ASSET001",
"description": "Gas Turbine", "description": "Gas Turbine",
"status": "OPERATING" "status": "OPERATING",
}, },
{ {
"assetnum": "ASSET002", "assetnum": "ASSET002",
"description": "Steam Turbine", "description": "Steam Turbine",
"status": "OPERATING" "status": "OPERATING",
} },
], ],
"workType": "OH", # OH for Overhaul "workType": "OH", # OH for Overhaul
"createdBy": "MAXADMIN", "createdBy": "MAXADMIN",
"createdDate": (current_date - timedelta(days=10)).isoformat(), "createdDate": (current_date - timedelta(days=10)).isoformat(),
"lastModifiedBy": "MAXADMIN", "lastModifiedBy": "MAXADMIN",
"lastModifiedDate": current_date.isoformat() "lastModifiedDate": current_date.isoformat(),
} }
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -131,21 +129,20 @@ class MaximoService:
params={ params={
# Update these parameters based on actual MAXIMO API # Update these parameters based on actual MAXIMO API
"orderBy": "-scheduleEnd", # Example parameter "orderBy": "-scheduleEnd", # Example parameter
"limit": 1 "limit": 1,
} },
) )
if response.status_code != 200: if response.status_code != 200:
raise HTTPException( raise HTTPException(
status_code=response.status_code, status_code=response.status_code,
detail=f"MAXIMO API error: {response.text}" detail=f"MAXIMO API error: {response.text}",
) )
data = response.json() data = response.json()
if not data: if not data:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404, detail="No recent overhaul found"
detail="No recent overhaul found"
) )
# TODO: Update this based on actual MAXIMO response structure # TODO: Update this based on actual MAXIMO response structure
@ -153,6 +150,5 @@ class MaximoService:
except httpx.RequestError as e: except httpx.RequestError as e:
raise HTTPException( raise HTTPException(
status_code=503, status_code=503, detail=f"Failed to connect to MAXIMO: {str(e)}"
detail=f"Failed to connect to MAXIMO: {str(e)}"
) )

@ -1,16 +1,18 @@
# src/common/models.py # src/common/models.py
import uuid
from datetime import datetime from datetime import datetime
from typing import Generic, Optional, TypeVar from typing import Generic, Optional, TypeVar
import uuid
import pytz
from pydantic import BaseModel, Field, SecretStr from pydantic import BaseModel, Field, SecretStr
from sqlalchemy import Column, DateTime, String, func, event from sqlalchemy import Column, DateTime, String, event, func
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from src.config import TIMEZONE
import pytz
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.config import TIMEZONE
from src.enums import ResponseStatus from src.enums import ResponseStatus
# SQLAlchemy Mixins # SQLAlchemy Mixins
@ -18,10 +20,12 @@ class TimeStampMixin(object):
"""Timestamping mixin""" """Timestamping mixin"""
created_at = Column( created_at = Column(
DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE))) DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE))
)
created_at._creation_order = 9998 created_at._creation_order = 9998
updated_at = Column( updated_at = Column(
DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE))) DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE))
)
updated_at._creation_order = 9998 updated_at._creation_order = 9998
@staticmethod @staticmethod
@ -35,17 +39,25 @@ class TimeStampMixin(object):
class UUIDMixin: class UUIDMixin:
"""UUID mixin""" """UUID mixin"""
id = Column(UUID(as_uuid=True), primary_key=True,
default=uuid.uuid4, unique=True, nullable=False) id = Column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
unique=True,
nullable=False,
)
class DefaultMixin(TimeStampMixin, UUIDMixin): class DefaultMixin(TimeStampMixin, UUIDMixin):
"""Default mixin""" """Default mixin"""
pass pass
class IdentityMixin: class IdentityMixin:
"""Identity mixin""" """Identity mixin"""
created_by = Column(String(100), nullable=True) created_by = Column(String(100), nullable=True)
updated_by = Column(String(100), nullable=True) updated_by = Column(String(100), nullable=True)
@ -77,7 +89,7 @@ class PrimaryKeyModel(BaseModel):
# Define data type variable for generic response # Define data type variable for generic response
T = TypeVar('T') T = TypeVar("T")
class StandardResponse(BaseModel, Generic[T]): class StandardResponse(BaseModel, Generic[T]):

@ -1,14 +1,18 @@
from typing import List from typing import List
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from src.overhaul.service import get_overhaul_critical_parts, get_overhaul_overview, get_overhaul_schedules, get_overhaul_system_components from src.database.core import DbSession
from src.models import StandardResponse
from src.overhaul.service import (get_overhaul_critical_parts,
get_overhaul_overview,
get_overhaul_schedules,
get_overhaul_system_components)
from src.overhaul_scope.schema import ScopeRead from src.overhaul_scope.schema import ScopeRead
from .schema import OverhaulRead, OverhaulCriticalParts, OverhaulSystemComponents from .schema import (OverhaulCriticalParts, OverhaulRead,
OverhaulSystemComponents)
from src.models import StandardResponse
from src.database.core import DbSession
router = APIRouter() router = APIRouter()
@ -51,7 +55,9 @@ async def get_critical_parts():
) )
@router.get("/system-components", response_model=StandardResponse[OverhaulSystemComponents]) @router.get(
"/system-components", response_model=StandardResponse[OverhaulSystemComponents]
)
async def get_system_components(): async def get_system_components():
"""Get all overhaul system components.""" """Get all overhaul system components."""
systemComponents = get_overhaul_system_components() systemComponents = get_overhaul_system_components()

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, BaseModel from pydantic import BaseModel, Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
from src.overhaul_scope.schema import ScopeRead from src.overhaul_scope.schema import ScopeRead
@ -17,13 +17,13 @@ class OverhaulCriticalParts(OverhaulBase):
class OverhaulSchedules(OverhaulBase): class OverhaulSchedules(OverhaulBase):
schedules: List[Dict[str, Any] schedules: List[Dict[str, Any]] = Field(..., description="List of schedules")
] = Field(..., description="List of schedules")
class OverhaulSystemComponents(OverhaulBase): class OverhaulSystemComponents(OverhaulBase):
systemComponents: Dict[str, systemComponents: Dict[str, Any] = Field(
Any] = Field(..., description="List of system components") ..., description="List of system components"
)
class OverhaulRead(OverhaulBase): class OverhaulRead(OverhaulBase):

@ -1,12 +1,12 @@
from sqlalchemy import Select, Delete
from typing import Optional from typing import Optional
from src.database.core import DbSession from sqlalchemy import Delete, Select
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.overhaul_scope.model import OverhaulScope from src.overhaul_scope.model import OverhaulScope
from src.overhaul_scope.service import get_all as get_all_session, get_overview_overhaul from src.overhaul_scope.service import get_all as get_all_session
from src.overhaul_scope.service import get_overview_overhaul
async def get_overhaul_overview(db_session: DbSession): async def get_overhaul_overview(db_session: DbSession):
@ -23,16 +23,16 @@ def get_overhaul_critical_parts():
"Boiler reheater system", "Boiler reheater system",
"Drum Level (Right) Root Valve A", "Drum Level (Right) Root Valve A",
"BCP A Discharge Valve", "BCP A Discharge Valve",
"BFPT A EXH Press HI Root VLV" "BFPT A EXH Press HI Root VLV",
] ]
async def get_overhaul_schedules(*, db_session: DbSession): async def get_overhaul_schedules(*, db_session: DbSession):
"""Get all overhaul schedules.""" """Get all overhaul schedules."""
query = Select(OverhaulScope) query = Select(OverhaulScope)
results = await db_session.execute(query) results = await db_session.execute(query)
return results.scalars().all() return results.scalars().all()
@ -107,7 +107,6 @@ def get_overhaul_system_components():
} }
# async def get(*, db_session: DbSession, scope_id: str) -> Optional[Scope]: # async def get(*, db_session: DbSession, scope_id: str) -> Optional[Scope]:
# """Returns a document based on the given document id.""" # """Returns a document based on the given document id."""
# query = Select(Scope).filter(Scope.id == scope_id) # query = Select(Scope).filter(Scope.id == scope_id)

@ -1,18 +1,19 @@
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from sqlalchemy.ext.hybrid import hybrid_property
class OverhaulActivity(Base, DefaultMixin): class OverhaulActivity(Base, DefaultMixin):
__tablename__ = "oh_tr_overhaul_activity" __tablename__ = "oh_tr_overhaul_activity"
assetnum = Column(String, nullable=True) assetnum = Column(String, nullable=True)
overhaul_scope_id = Column(UUID(as_uuid=True), ForeignKey( overhaul_scope_id = Column(
"oh_ms_overhaul_scope.id"), nullable=False) UUID(as_uuid=True), ForeignKey("oh_ms_overhaul_scope.id"), nullable=False
)
material_cost = Column(Float, nullable=False, default=0) material_cost = Column(Float, nullable=False, default=0)
service_cost = Column(Float, nullable=False, default=0) service_cost = Column(Float, nullable=False, default=0)
status = Column(String, nullable=False, default="pending") status = Column(String, nullable=False, default="pending")
@ -21,11 +22,10 @@ class OverhaulActivity(Base, DefaultMixin):
"MasterEquipment", "MasterEquipment",
lazy="raise", lazy="raise",
primaryjoin="and_(OverhaulActivity.assetnum == foreign(MasterEquipment.assetnum))", primaryjoin="and_(OverhaulActivity.assetnum == foreign(MasterEquipment.assetnum))",
uselist=False # Add this if it's a one-to-one relationship uselist=False, # Add this if it's a one-to-one relationship
) )
overhaul_scope = relationship( overhaul_scope = relationship(
"OverhaulScope", "OverhaulScope",
lazy="raise", lazy="raise",
) )

@ -1,23 +1,36 @@
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, HTTPException, Query, status
from .service import get_all, create, get, update, delete from fastapi import APIRouter, HTTPException, Query, status
from .schema import OverhaulActivityCreate, OverhaulActivityPagination, OverhaulActivityRead, OverhaulActivityUpdate
from src.database.service import (CommonParameters, DbSession,
search_filter_sort_paginate)
from src.models import StandardResponse from src.models import StandardResponse
from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession
from .schema import (OverhaulActivityCreate, OverhaulActivityPagination,
OverhaulActivityRead, OverhaulActivityUpdate)
from .service import create, delete, get, get_all, update
router = APIRouter() router = APIRouter()
@router.get("/{overhaul_session}", response_model=StandardResponse[OverhaulActivityPagination]) @router.get(
async def get_scope_equipments(common: CommonParameters, overhaul_session: str, assetnum: Optional[str] = Query(None), scope_name: Optional[str] = Query(None)): "/{overhaul_session}", response_model=StandardResponse[OverhaulActivityPagination]
)
async def get_scope_equipments(
common: CommonParameters,
overhaul_session: str,
assetnum: Optional[str] = Query(None),
scope_name: Optional[str] = Query(None),
):
"""Get all scope activity pagination.""" """Get all scope activity pagination."""
# return # return
data = await get_all(common=common, assetnum=assetnum, scope_name=scope_name, overhaul_session_id=overhaul_session) data = await get_all(
common=common,
assetnum=assetnum,
scope_name=scope_name,
overhaul_session_id=overhaul_session,
)
return StandardResponse( return StandardResponse(
data=data, data=data,
@ -25,17 +38,32 @@ async def get_scope_equipments(common: CommonParameters, overhaul_session: str,
) )
@ router.post("/{overhaul_session}", response_model=StandardResponse[List[str]]) @router.post("/{overhaul_session}", response_model=StandardResponse[List[str]])
async def create_overhaul_equipment(db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session: str): async def create_overhaul_equipment(
db_session: DbSession,
overhaul_activty_in: OverhaulActivityCreate,
overhaul_session: str,
):
activity = await create(db_session=db_session, overhaul_activty_in=overhaul_activty_in, overhaul_session_id=overhaul_session) activity = await create(
db_session=db_session,
overhaul_activty_in=overhaul_activty_in,
overhaul_session_id=overhaul_session,
)
return StandardResponse(data=activity, message="Data created successfully") return StandardResponse(data=activity, message="Data created successfully")
@ router.get("/{overhaul_session}/{assetnum}", response_model=StandardResponse[OverhaulActivityRead]) @router.get(
async def get_overhaul_equipment(db_session: DbSession, assetnum: str, overhaul_session): "/{overhaul_session}/{assetnum}",
equipment = await get(db_session=db_session, assetnum=assetnum, overhaul_session_id=overhaul_session) response_model=StandardResponse[OverhaulActivityRead],
)
async def get_overhaul_equipment(
db_session: DbSession, assetnum: str, overhaul_session
):
equipment = await get(
db_session=db_session, assetnum=assetnum, overhaul_session_id=overhaul_session
)
if not equipment: if not equipment:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -45,8 +73,15 @@ async def get_overhaul_equipment(db_session: DbSession, assetnum: str, overhaul_
return StandardResponse(data=equipment, message="Data retrieved successfully") return StandardResponse(data=equipment, message="Data retrieved successfully")
@ router.put("/{overhaul_session}/{assetnum}", response_model=StandardResponse[OverhaulActivityRead]) @router.put(
async def update_scope(db_session: DbSession, scope_equipment_activity_in: OverhaulActivityUpdate, assetnum: str): "/{overhaul_session}/{assetnum}",
response_model=StandardResponse[OverhaulActivityRead],
)
async def update_scope(
db_session: DbSession,
scope_equipment_activity_in: OverhaulActivityUpdate,
assetnum: str,
):
activity = await get(db_session=db_session, assetnum=assetnum) activity = await get(db_session=db_session, assetnum=assetnum)
if not activity: if not activity:
@ -55,10 +90,20 @@ async def update_scope(db_session: DbSession, scope_equipment_activity_in: Overh
detail="A data with this id does not exist.", detail="A data with this id does not exist.",
) )
return StandardResponse(data=await update(db_session=db_session, activity=activity, scope_equipment_activity_in=scope_equipment_activity_in), message="Data updated successfully") return StandardResponse(
data=await update(
db_session=db_session,
activity=activity,
scope_equipment_activity_in=scope_equipment_activity_in,
),
message="Data updated successfully",
)
@ router.delete("/{overhaul_session}/{assetnum}", response_model=StandardResponse[OverhaulActivityRead]) @router.delete(
"/{overhaul_session}/{assetnum}",
response_model=StandardResponse[OverhaulActivityRead],
)
async def delete_scope(db_session: DbSession, assetnum: str): async def delete_scope(db_session: DbSession, assetnum: str):
activity = await get(db_session=db_session, assetnum=assetnum) activity = await get(db_session=db_session, assetnum=assetnum)

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field from pydantic import Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
from src.scope_equipment.schema import MasterEquipmentRead from src.scope_equipment.schema import MasterEquipmentRead

@ -1,62 +1,81 @@
import asyncio import asyncio
from uuid import UUID
from sqlalchemy import Select, Delete, func, select, update as sqlUpdate
from sqlalchemy.orm import joinedload
from typing import List, Optional from typing import List, Optional
from uuid import UUID
from sqlalchemy import Delete, Select, func, select
from sqlalchemy import update as sqlUpdate
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.orm import joinedload
from src.auth.service import CurrentUser
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_activity.utils import get_material_cost, get_service_cost
from src.overhaul_scope.model import OverhaulScope from src.overhaul_scope.model import OverhaulScope
from src.overhaul_scope.service import get as get_session
from .model import OverhaulActivity from .model import OverhaulActivity
from .schema import OverhaulActivityCreate, OverhaulActivityUpdate, OverhaulActivityRead from .schema import (OverhaulActivityCreate, OverhaulActivityRead,
OverhaulActivityUpdate)
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.auth.service import CurrentUser
from src.overhaul_scope.service import get as get_session
async def get(*, db_session: DbSession, assetnum: str, overhaul_session_id: Optional[UUID] = None) -> Optional[OverhaulActivityRead]: async def get(
*, db_session: DbSession, assetnum: str, overhaul_session_id: Optional[UUID] = None
) -> Optional[OverhaulActivityRead]:
"""Returns a document based on the given document id.""" """Returns a document based on the given document id."""
query = Select(OverhaulActivity).where( query = (
OverhaulActivity.assetnum == assetnum).options(joinedload(OverhaulActivity.equipment)) Select(OverhaulActivity)
.where(OverhaulActivity.assetnum == assetnum)
.options(joinedload(OverhaulActivity.equipment))
)
if overhaul_session_id: if overhaul_session_id:
query = query.filter( query = query.filter(OverhaulActivity.overhaul_scope_id == overhaul_session_id)
OverhaulActivity.overhaul_scope_id == overhaul_session_id)
result = await db_session.execute(query) result = await db_session.execute(query)
return result.scalar() return result.scalar()
async def get_all(*, common: CommonParameters, overhaul_session_id: UUID, assetnum: Optional[str] = None, scope_name: Optional[str] = None): async def get_all(
query = Select(OverhaulActivity).where( *,
OverhaulActivity.overhaul_scope_id == overhaul_session_id).options(joinedload(OverhaulActivity.equipment)) common: CommonParameters,
overhaul_session_id: UUID,
assetnum: Optional[str] = None,
scope_name: Optional[str] = None
):
query = (
Select(OverhaulActivity)
.where(OverhaulActivity.overhaul_scope_id == overhaul_session_id)
.options(joinedload(OverhaulActivity.equipment))
)
if assetnum: if assetnum:
query = query.filter(OverhaulActivity.assetnum == assetnum).options( query = query.filter(OverhaulActivity.assetnum == assetnum).options(
joinedload(OverhaulActivity.overhaul_scope)) joinedload(OverhaulActivity.overhaul_scope)
)
if scope_name: if scope_name:
query = query.filter(OverhaulActivity.scope_name == scope_name).options( query = query.filter(OverhaulActivity.scope_name == scope_name).options(
joinedload(OverhaulActivity.overhaul_scope)) joinedload(OverhaulActivity.overhaul_scope)
)
results = await search_filter_sort_paginate(model=query, **common) results = await search_filter_sort_paginate(model=query, **common)
return results return results
async def get_all_by_session_id(*, db_session:DbSession, overhaul_session_id): async def get_all_by_session_id(*, db_session: DbSession, overhaul_session_id):
query = Select(OverhaulActivity).where( query = (
OverhaulActivity.overhaul_scope_id == overhaul_session_id).options(joinedload(OverhaulActivity.equipment)) Select(OverhaulActivity)
.where(OverhaulActivity.overhaul_scope_id == overhaul_session_id)
.options(joinedload(OverhaulActivity.equipment))
)
results = await db_session.execute(query) results = await db_session.execute(query)
return results.scalars().all() return results.scalars().all()
# async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID): # async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID):
# # Check if the combination of assetnum and activity_id already exists # # Check if the combination of assetnum and activity_id already exists
# existing_equipment_query = ( # existing_equipment_query = (
@ -90,14 +109,22 @@ async def get_all_by_session_id(*, db_session:DbSession, overhaul_session_id):
# return activity_with_relationship # return activity_with_relationship
async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID):
async def create(
*,
db_session: DbSession,
overhaul_activty_in: OverhaulActivityCreate,
overhaul_session_id: UUID
):
"""Creates a new document.""" """Creates a new document."""
assetnums = overhaul_activty_in.assetnums assetnums = overhaul_activty_in.assetnums
if not assetnums: if not assetnums:
return [] return []
# Get session and count in parallel # Get session and count in parallel
session = await get_session(db_session=db_session, overhaul_session_id=overhaul_session_id) session = await get_session(
db_session=db_session, overhaul_session_id=overhaul_session_id
)
equipment_count = await db_session.scalar( equipment_count = await db_session.scalar(
select(func.count()) select(func.count())
.select_from(OverhaulActivity) .select_from(OverhaulActivity)
@ -107,25 +134,25 @@ async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivity
# Calculate costs for all records # Calculate costs for all records
total_equipment = equipment_count + len(assetnums) total_equipment = equipment_count + len(assetnums)
material_cost = get_material_cost( material_cost = get_material_cost(
scope=session.type, total_equipment=total_equipment) scope=session.type, total_equipment=total_equipment
service_cost = get_service_cost( )
scope=session.type, total_equipment=total_equipment) service_cost = get_service_cost(scope=session.type, total_equipment=total_equipment)
# Create the insert statement # Create the insert statement
stmt = insert(OverhaulActivity).values([ stmt = insert(OverhaulActivity).values(
{ [
'assetnum': assetnum, {
'overhaul_scope_id': overhaul_session_id, "assetnum": assetnum,
'material_cost': material_cost, "overhaul_scope_id": overhaul_session_id,
'service_cost': service_cost "material_cost": material_cost,
} "service_cost": service_cost,
for assetnum in assetnums }
]) for assetnum in assetnums
]
)
# Add the ON CONFLICT DO NOTHING clause # Add the ON CONFLICT DO NOTHING clause
stmt = stmt.on_conflict_do_nothing( stmt = stmt.on_conflict_do_nothing(index_elements=["assetnum", "overhaul_scope_id"])
index_elements=["assetnum", "overhaul_scope_id"]
)
# Execute the statement # Execute the statement
await db_session.execute(stmt) await db_session.execute(stmt)
@ -139,7 +166,12 @@ async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivity
return assetnums return assetnums
async def update(*, db_session: DbSession, activity: OverhaulActivity, overhaul_activity_in: OverhaulActivityUpdate): async def update(
*,
db_session: DbSession,
activity: OverhaulActivity,
overhaul_activity_in: OverhaulActivityUpdate
):
"""Updates a document.""" """Updates a document."""
data = overhaul_activity_in.model_dump() data = overhaul_activity_in.model_dump()

@ -1,33 +1,35 @@
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
def get_material_cost(scope, total_equipment): def get_material_cost(scope, total_equipment):
# Set precision to 28 digits (maximum precision for Decimal) # Set precision to 28 digits (maximum precision for Decimal)
getcontext().prec = 28 getcontext().prec = 28
if not total_equipment: # Guard against division by zero if not total_equipment: # Guard against division by zero
return float(0) return float(0)
if scope == 'B': if scope == "B":
result = Decimal('365539731101') / Decimal(str(total_equipment)) result = Decimal("365539731101") / Decimal(str(total_equipment))
return float(result) return float(result)
else: else:
result = Decimal('8565468127') / Decimal(str(total_equipment)) result = Decimal("8565468127") / Decimal(str(total_equipment))
return float(result) return float(result)
return float(0) return float(0)
def get_service_cost(scope, total_equipment): def get_service_cost(scope, total_equipment):
# Set precision to 28 digits (maximum precision for Decimal) # Set precision to 28 digits (maximum precision for Decimal)
getcontext().prec = 28 getcontext().prec = 28
if not total_equipment: # Guard against division by zero if not total_equipment: # Guard against division by zero
return float(0) return float(0)
if scope == 'B': if scope == "B":
result = Decimal('36405830225') / Decimal(str(total_equipment)) result = Decimal("36405830225") / Decimal(str(total_equipment))
return float(result) return float(result)
else: else:
result = Decimal('36000000000') / Decimal(str(total_equipment)) result = Decimal("36000000000") / Decimal(str(total_equipment))
return float(result) return float(result)
return float(0) return float(0)

@ -1,28 +1,29 @@
from sqlalchemy import (UUID, Column, DateTime, Float, ForeignKey, Integer,
String)
from sqlalchemy.orm import relationship
from sqlalchemy import Column, DateTime, Float, Integer, String, UUID, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
class OverhaulJob(Base, DefaultMixin): class OverhaulJob(Base, DefaultMixin):
__tablename__ = "oh_tr_overhaul_job" __tablename__ = "oh_tr_overhaul_job"
overhaul_activity_id = Column(UUID(as_uuid=True), ForeignKey( overhaul_activity_id = Column(
"oh_tr_overhaul_activity.id"), nullable=False) UUID(as_uuid=True), ForeignKey("oh_tr_overhaul_activity.id"), nullable=False
)
scope_equipment_job_id = Column(UUID(as_uuid=True), ForeignKey(
"oh_ms_scope_equipment_job.id", ondelete="cascade"), nullable=False)
scope_equipment_job_id = Column(
UUID(as_uuid=True),
ForeignKey("oh_ms_scope_equipment_job.id", ondelete="cascade"),
nullable=False,
)
notes = Column(String, nullable=True) notes = Column(String, nullable=True)
status = Column(String, nullable=True, default="pending") status = Column(String, nullable=True, default="pending")
scope_equipment_job = relationship( scope_equipment_job = relationship(
"ScopeEquipmentJob", lazy="raise" "ScopeEquipmentJob", lazy="raise", back_populates="overhaul_jobs"
) )
overhaul_activity = relationship( overhaul_activity = relationship("OverhaulActivity", lazy="raise")
"OverhaulActivity", lazy="raise"
)

@ -1,19 +1,22 @@
from typing import List, Optional
from typing import Optional, List
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from .schema import OverhaulJobBase, OverhaulJobRead, OverhaulJobPagination, OverhaulJobCreate
from .service import get_all, create
from src.database.service import CommonParameters
from src.database.core import DbSession
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters
from src.models import StandardResponse from src.models import StandardResponse
from .schema import (OverhaulJobBase, OverhaulJobCreate, OverhaulJobPagination,
OverhaulJobRead)
from .service import create, get_all
router = APIRouter() router = APIRouter()
@router.get("/{overhaul_equipment_id}", response_model=StandardResponse[OverhaulJobPagination]) @router.get(
"/{overhaul_equipment_id}", response_model=StandardResponse[OverhaulJobPagination]
)
async def get_jobs(common: CommonParameters, overhaul_equipment_id: str): async def get_jobs(common: CommonParameters, overhaul_equipment_id: str):
"""Get all scope pagination.""" """Get all scope pagination."""
# return # return
@ -24,11 +27,18 @@ async def get_jobs(common: CommonParameters, overhaul_equipment_id: str):
message="Data retrieved successfully", message="Data retrieved successfully",
) )
@router.post("/{overhaul_equipment_id}", response_model=StandardResponse[None]) @router.post("/{overhaul_equipment_id}", response_model=StandardResponse[None])
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate): async def create_overhaul_equipment_jobs(
db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
):
"""Get all scope activity pagination.""" """Get all scope activity pagination."""
# return # return
await create(db_session=db_session, overhaul_equipment_id=overhaul_equipment_id, overhaul_job_in=overhaul_job_in) await create(
db_session=db_session,
overhaul_equipment_id=overhaul_equipment_id,
overhaul_job_in=overhaul_job_in,
)
return StandardResponse( return StandardResponse(
data=None, data=None,

@ -1,12 +1,14 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field from pydantic import Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
from src.overhaul_scope.schema import ScopeRead
from src.scope_equipment_job.schema import ScopeEquipmentJobRead from src.scope_equipment_job.schema import ScopeEquipmentJobRead
class OverhaulJobBase(DefultBase): class OverhaulJobBase(DefultBase):
pass pass
@ -18,9 +20,11 @@ class OverhaulJobCreate(OverhaulJobBase):
class OverhaulJobUpdate(OverhaulJobBase): class OverhaulJobUpdate(OverhaulJobBase):
pass pass
class OverhaulActivity(DefultBase): class OverhaulActivity(DefultBase):
id: UUID id: UUID
overhaul_scope_id: UUID overhaul_scope_id: UUID
overhaul_scope: ScopeRead
class OverhaulJobRead(OverhaulJobBase): class OverhaulJobRead(OverhaulJobBase):

@ -1,43 +1,48 @@
from typing import Optional
from sqlalchemy import Delete, Select, func
from sqlalchemy import Select, Delete, func
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import search_filter_sort_paginate from src.database.service import search_filter_sort_paginate
from .model import OverhaulJob from .model import OverhaulJob
from typing import Optional
from .schema import OverhaulJobCreate from .schema import OverhaulJobCreate
from src.database.core import DbSession
from src.auth.service import CurrentUser
async def get_all(*, common, overhaul_equipment_id: str): async def get_all(*, common, overhaul_equipment_id: str):
"""Returns all documents.""" """Returns all documents."""
query = Select(OverhaulJob).where(OverhaulJob.overhaul_activity_id == overhaul_equipment_id).options( query = (
selectinload(OverhaulJob.scope_equipment_job), selectinload(OverhaulJob.overhaul_activity) Select(OverhaulJob)
.where(OverhaulJob.overhaul_activity_id == overhaul_equipment_id)
.options(
selectinload(OverhaulJob.scope_equipment_job),
selectinload(OverhaulJob.overhaul_activity),
)
) )
results = await search_filter_sort_paginate(model=query, **common) results = await search_filter_sort_paginate(model=query, **common)
return results return results
async def create(*, db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate): async def create(
*, db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
):
overhaul_jobs = [] overhaul_jobs = []
if not overhaul_equipment_id: if not overhaul_equipment_id:
raise ValueError("assetnum parameter is required") raise ValueError("assetnum parameter is required")
equipment_stmt = Select(OverhaulJob).where( equipment_stmt = Select(OverhaulJob).where(
OverhaulJob.overhaul_activity_id == overhaul_equipment_id) OverhaulJob.overhaul_activity_id == overhaul_equipment_id
)
equipment = await db_session.scalar(equipment_stmt) equipment = await db_session.scalar(equipment_stmt)
for job_id in overhaul_job_in.job_ids: for job_id in overhaul_job_in.job_ids:
overhaul_equipment_job = OverhaulJob( overhaul_equipment_job = OverhaulJob(
overhaul_activity_id=overhaul_equipment_id, scope_equipment_job_id=job_id overhaul_activity_id=overhaul_equipment_id, scope_equipment_job_id=job_id
) )
overhaul_jobs.append(overhaul_equipment_job) overhaul_jobs.append(overhaul_equipment_job)

@ -1,8 +1,8 @@
from sqlalchemy import Column, DateTime, Float, Integer, String from sqlalchemy import Column, DateTime, Float, Integer, String
from sqlalchemy.orm import relationship
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
class OverhaulScope(Base, DefaultMixin): class OverhaulScope(Base, DefaultMixin):
@ -15,7 +15,4 @@ class OverhaulScope(Base, DefaultMixin):
crew_number = Column(Integer, nullable=True, default=1) crew_number = Column(Integer, nullable=True, default=1)
status = Column(String, nullable=False, default="upcoming") status = Column(String, nullable=False, default="upcoming")
activity_equipments = relationship( activity_equipments = relationship("OverhaulActivity", lazy="selectin")
"OverhaulActivity",
lazy="selectin"
)

@ -1,16 +1,16 @@
from typing import Optional from typing import Optional
from fastapi import APIRouter, HTTPException, status
from .model import OverhaulScope from fastapi import APIRouter, HTTPException, status
from .schema import ScopeCreate, ScopeRead, ScopeUpdate, ScopePagination
from .service import get, get_all, create, update, delete
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.database.core import DbSession
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.models import StandardResponse from src.models import StandardResponse
from .model import OverhaulScope
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
from .service import create, delete, get, get_all, update
router = APIRouter() router = APIRouter()
@ -46,7 +46,12 @@ async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
@router.put("/{scope_id}", response_model=StandardResponse[ScopeRead]) @router.put("/{scope_id}", response_model=StandardResponse[ScopeRead])
async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpdate, current_user: CurrentUser): async def update_scope(
db_session: DbSession,
scope_id: str,
scope_in: ScopeUpdate,
current_user: CurrentUser,
):
scope = await get(db_session=db_session, scope_id=scope_id) scope = await get(db_session=db_session, scope_id=scope_id)
if not scope: if not scope:
@ -55,7 +60,10 @@ async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpda
detail="A data with this id does not exist.", detail="A data with this id does not exist.",
) )
return StandardResponse(data=await update(db_session=db_session, scope=scope, scope_in=scope_in), message="Data updated successfully") return StandardResponse(
data=await update(db_session=db_session, scope=scope, scope_in=scope_in),
message="Data updated successfully",
)
@router.delete("/{scope_id}", response_model=StandardResponse[ScopeRead]) @router.delete("/{scope_id}", response_model=StandardResponse[ScopeRead])

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field from pydantic import Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination

@ -1,24 +1,24 @@
from typing import Optional
from sqlalchemy import Delete, Select, func
from sqlalchemy import Select, Delete, func from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import search_filter_sort_paginate from src.database.service import search_filter_sort_paginate
from src.overhaul_activity.model import OverhaulActivity from src.overhaul_activity.model import OverhaulActivity
from src.scope_equipment.service import get_by_scope_name from src.scope_equipment.service import get_by_scope_name
from src.utils import time_now from src.utils import time_now
from .model import OverhaulScope from .model import OverhaulScope
from .schema import ScopeCreate, ScopeUpdate from .schema import ScopeCreate, ScopeUpdate
from .utils import get_material_cost, get_service_cost from .utils import get_material_cost, get_service_cost
from typing import Optional
from src.database.core import DbSession
from src.auth.service import CurrentUser
async def get(*, db_session: DbSession, overhaul_session_id: str) -> Optional[OverhaulScope]: async def get(
*, db_session: DbSession, overhaul_session_id: str
) -> Optional[OverhaulScope]:
"""Returns a document based on the given document id.""" """Returns a document based on the given document id."""
query = Select(OverhaulScope).filter( query = Select(OverhaulScope).filter(OverhaulScope.id == overhaul_session_id)
OverhaulScope.id == overhaul_session_id)
result = await db_session.execute(query) result = await db_session.execute(query)
return result.scalars().one_or_none() return result.scalars().one_or_none()
@ -44,13 +44,14 @@ async def create(*, db_session: DbSession, scope_in: ScopeCreate):
scope_name = scope_in.type scope_name = scope_in.type
# Fix the function call - parameters were in wrong order # Fix the function call - parameters were in wrong order
equipments = await get_by_scope_name( equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name)
db_session=db_session,
scope_name=scope_name
)
material_cost = get_material_cost(scope=overhaul_session.type, total_equipment=len(equipments)) material_cost = get_material_cost(
service_cost = get_service_cost(scope=overhaul_session.type, total_equipment=len(equipments)) scope=overhaul_session.type, total_equipment=len(equipments)
)
service_cost = get_service_cost(
scope=overhaul_session.type, total_equipment=len(equipments)
)
scope_equipments = [ scope_equipments = [
OverhaulActivity( OverhaulActivity(
@ -95,24 +96,20 @@ async def get_overview_overhaul(*, db_session: DbSession):
current_date = time_now().date() current_date = time_now().date()
# For ongoing overhaul with count # For ongoing overhaul with count
ongoing_query = Select( ongoing_query = (
OverhaulScope, Select(OverhaulScope, func.count(OverhaulActivity.id).label("equipment_count"))
func.count(OverhaulActivity.id).label('equipment_count') .outerjoin(OverhaulScope.activity_equipments)
).outerjoin( .where(
OverhaulScope.activity_equipments OverhaulScope.start_date <= current_date,
).where( OverhaulScope.end_date >= current_date,
OverhaulScope.start_date <= current_date, )
OverhaulScope.end_date >= current_date, .group_by(OverhaulScope.id)
).group_by(
OverhaulScope.id
) )
ongoing_result = await db_session.execute(ongoing_query) ongoing_result = await db_session.execute(ongoing_query)
# Use first() instead of scalar_one_or_none() # Use first() instead of scalar_one_or_none()
ongoing_result = ongoing_result.first() ongoing_result = ongoing_result.first()
if ongoing_result: if ongoing_result:
ongoing_overhaul, equipment_count = ongoing_result # Unpack the result tuple ongoing_overhaul, equipment_count = ongoing_result # Unpack the result tuple
@ -126,22 +123,19 @@ async def get_overview_overhaul(*, db_session: DbSession):
"duration_oh": ongoing_overhaul.duration_oh, "duration_oh": ongoing_overhaul.duration_oh,
"crew_number": ongoing_overhaul.crew_number, "crew_number": ongoing_overhaul.crew_number,
"remaining_days": (ongoing_overhaul.end_date - current_date).days, "remaining_days": (ongoing_overhaul.end_date - current_date).days,
"equipment_count": equipment_count "equipment_count": equipment_count,
} },
} }
# For upcoming overhaul with count # For upcoming overhaul with count
upcoming_query = Select( upcoming_query = (
OverhaulScope, Select(OverhaulScope, func.count(OverhaulActivity.id).label("equipment_count"))
func.count(OverhaulActivity.id).label('equipment_count') .outerjoin(OverhaulScope.activity_equipments)
).outerjoin( .where(
OverhaulScope.activity_equipments OverhaulScope.start_date > current_date,
).where( )
OverhaulScope.start_date > current_date, .group_by(OverhaulScope.id)
).group_by( .order_by(OverhaulScope.start_date)
OverhaulScope.id
).order_by(
OverhaulScope.start_date
) )
upcoming_result = await db_session.execute(upcoming_query) upcoming_result = await db_session.execute(upcoming_query)
@ -161,14 +155,8 @@ async def get_overview_overhaul(*, db_session: DbSession):
"duration_oh": upcoming_overhaul.duration_oh, "duration_oh": upcoming_overhaul.duration_oh,
"crew_number": upcoming_overhaul.crew_number, "crew_number": upcoming_overhaul.crew_number,
"remaining_days": days_until, "remaining_days": days_until,
"equipment_count": equipment_count "equipment_count": equipment_count,
} },
} }
return { return {"status": "no_overhaul", "overhaul": None}
"status": "no_overhaul",
"overhaul": None
}

@ -1,33 +1,35 @@
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
def get_material_cost(scope, total_equipment): def get_material_cost(scope, total_equipment):
# Set precision to 28 digits (maximum precision for Decimal) # Set precision to 28 digits (maximum precision for Decimal)
getcontext().prec = 28 getcontext().prec = 28
if not total_equipment: # Guard against division by zero if not total_equipment: # Guard against division by zero
return float(0) return float(0)
if scope == 'B': if scope == "B":
result = Decimal('365539731101') / Decimal(str(total_equipment)) result = Decimal("365539731101") / Decimal(str(total_equipment))
return float(result) return float(result)
else: else:
result = Decimal('8565468127') / Decimal(str(total_equipment)) result = Decimal("8565468127") / Decimal(str(total_equipment))
return float(result) return float(result)
return float(0) return float(0)
def get_service_cost(scope, total_equipment): def get_service_cost(scope, total_equipment):
# Set precision to 28 digits (maximum precision for Decimal) # Set precision to 28 digits (maximum precision for Decimal)
getcontext().prec = 28 getcontext().prec = 28
if not total_equipment: # Guard against division by zero if not total_equipment: # Guard against division by zero
return float(0) return float(0)
if scope == 'B': if scope == "B":
result = Decimal('36405830225') / Decimal(str(total_equipment)) result = Decimal("36405830225") / Decimal(str(total_equipment))
return float(result) return float(result)
else: else:
result = Decimal('36000000000') / Decimal(str(total_equipment)) result = Decimal("36000000000") / Decimal(str(total_equipment))
return float(result) return float(result)
return float(0) return float(0)

@ -1,5 +1,4 @@
from slowapi import Limiter from slowapi import Limiter
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address) limiter = Limiter(key_func=get_remote_address)

@ -1,5 +1,3 @@
from src.enums import OptimumOHEnum from src.enums import OptimumOHEnum

@ -1,10 +1,10 @@
from sqlalchemy import UUID, Column, Date, Float, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy import UUID, Column, Date, Float, Integer, String, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from sqlalchemy.ext.hybrid import hybrid_property
class ScopeEquipment(Base, DefaultMixin): class ScopeEquipment(Base, DefaultMixin):
@ -20,7 +20,7 @@ class ScopeEquipment(Base, DefaultMixin):
"MasterEquipment", "MasterEquipment",
lazy="raise", lazy="raise",
primaryjoin="and_(ScopeEquipment.assetnum == foreign(MasterEquipment.assetnum))", primaryjoin="and_(ScopeEquipment.assetnum == foreign(MasterEquipment.assetnum))",
uselist=False # Add this if it's a one-to-one relationship uselist=False, # Add this if it's a one-to-one relationship
) )
@ -32,11 +32,11 @@ class MasterEquipment(Base, DefaultMixin):
system_tag = Column(String, nullable=True) system_tag = Column(String, nullable=True)
location_tag = Column(String, nullable=True) location_tag = Column(String, nullable=True)
name = Column(String, nullable=True) name = Column(String, nullable=True)
equipment_tree_id = Column(UUID(as_uuid=True), ForeignKey( equipment_tree_id = Column(
"ms_equipment_tree.id"), nullable=True) UUID(as_uuid=True), ForeignKey("ms_equipment_tree.id"), nullable=True
)
equipment_tree = relationship( equipment_tree = relationship("MasterEquipmentTree", backref="master_equipments")
"MasterEquipmentTree", backref="master_equipments")
class MasterEquipmentTree(Base, DefaultMixin): class MasterEquipmentTree(Base, DefaultMixin):

@ -1,17 +1,20 @@
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from fastapi.params import Query from fastapi.params import Query
from .model import ScopeEquipment
from .schema import ScopeEquipmentCreate, ScopeEquipmentPagination, ScopeEquipmentRead, ScopeEquipmentUpdate, MasterEquipmentPagination
from .service import get_all, create, update, delete, get_all_master_equipment, get_by_assetnum
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.database.core import DbSession
from src.auth.service import CurrentUser from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.models import StandardResponse from src.models import StandardResponse
from .model import ScopeEquipment
from .schema import (MasterEquipmentPagination, ScopeEquipmentCreate,
ScopeEquipmentPagination, ScopeEquipmentRead,
ScopeEquipmentUpdate)
from .service import (create, delete, get_all, get_all_master_equipment,
get_by_assetnum, update)
router = APIRouter() router = APIRouter()
@ -27,7 +30,10 @@ async def get_scope_equipments(common: CommonParameters, scope_name: str = Query
) )
@router.get("/available/{scope_name}", response_model=StandardResponse[MasterEquipmentPagination]) @router.get(
"/available/{scope_name}",
response_model=StandardResponse[MasterEquipmentPagination],
)
async def get_master_equipment(common: CommonParameters, scope_name: str): async def get_master_equipment(common: CommonParameters, scope_name: str):
results = await get_all_master_equipment(common=common, scope_name=scope_name) results = await get_all_master_equipment(common=common, scope_name=scope_name)
@ -35,14 +41,18 @@ async def get_master_equipment(common: CommonParameters, scope_name: str):
@router.post("", response_model=StandardResponse[List[str]]) @router.post("", response_model=StandardResponse[List[str]])
async def create_scope_equipment(db_session: DbSession, scope_equipment_in: ScopeEquipmentCreate): async def create_scope_equipment(
db_session: DbSession, scope_equipment_in: ScopeEquipmentCreate
):
scope = await create(db_session=db_session, scope_equipment_in=scope_equipment_in) scope = await create(db_session=db_session, scope_equipment_in=scope_equipment_in)
return StandardResponse(data=scope, message="Data created successfully") return StandardResponse(data=scope, message="Data created successfully")
@router.put("/{assetnum}", response_model=StandardResponse[ScopeEquipmentRead]) @router.put("/{assetnum}", response_model=StandardResponse[ScopeEquipmentRead])
async def update_scope_equipment(db_session: DbSession, assetnum: str, scope__equipment_in: ScopeEquipmentUpdate): async def update_scope_equipment(
db_session: DbSession, assetnum: str, scope__equipment_in: ScopeEquipmentUpdate
):
scope_equipment = await get_by_assetnum(db_session=db_session, assetnum=assetnum) scope_equipment = await get_by_assetnum(db_session=db_session, assetnum=assetnum)
if not scope_equipment: if not scope_equipment:
@ -51,7 +61,14 @@ async def update_scope_equipment(db_session: DbSession, assetnum: str, scope__eq
detail="A data with this id does not exist.", detail="A data with this id does not exist.",
) )
return StandardResponse(data=await update(db_session=db_session, scope_equipment=scope_equipment, scope__equipment_in=scope__equipment_in), message="Data updated successfully") return StandardResponse(
data=await update(
db_session=db_session,
scope_equipment=scope_equipment,
scope__equipment_in=scope__equipment_in,
),
message="Data updated successfully",
)
@router.delete("/{assetnum}", response_model=StandardResponse[None]) @router.delete("/{assetnum}", response_model=StandardResponse[None])

@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, computed_field, field_validator, validator from pydantic import Field, computed_field, field_validator, validator
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
from src.overhaul_scope.schema import ScopeRead from src.overhaul_scope.schema import ScopeRead
from .enum import ScopeEquipmentType from .enum import ScopeEquipmentType

@ -1,25 +1,28 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Union
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy import Select, Delete, and_, desc, func, not_, or_ from sqlalchemy import Delete, Select, and_, desc, func, not_, or_
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.orm import selectinload
from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.overhaul_scope.model import OverhaulScope from src.overhaul_scope.model import OverhaulScope
from src.scope_equipment.enum import ScopeEquipmentType from src.scope_equipment.enum import ScopeEquipmentType
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from .model import MasterEquipmentTree, ScopeEquipment, MasterEquipment
from .schema import ScopeEquipmentCreate, ScopeEquipmentUpdate
from typing import Optional, Union
from sqlalchemy.orm import selectinload
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.database.core import DbSession from .model import MasterEquipment, MasterEquipmentTree, ScopeEquipment
from src.auth.service import CurrentUser from .schema import ScopeEquipmentCreate, ScopeEquipmentUpdate
async def get_by_assetnum(*, db_session: DbSession, assetnum: str): async def get_by_assetnum(*, db_session: DbSession, assetnum: str):
query = Select(ScopeEquipment).filter(ScopeEquipment.assetnum == assetnum).options( query = (
selectinload(ScopeEquipment.master_equipment)) Select(ScopeEquipment)
.filter(ScopeEquipment.assetnum == assetnum)
.options(selectinload(ScopeEquipment.master_equipment))
)
result = await db_session.execute(query) result = await db_session.execute(query)
return result.unique().scalars().one_or_none() return result.unique().scalars().one_or_none()
@ -28,7 +31,8 @@ async def get_by_assetnum(*, db_session: DbSession, assetnum: str):
async def get_all(*, common, scope_name: str = None): async def get_all(*, common, scope_name: str = None):
"""Returns all documents.""" """Returns all documents."""
query = Select(ScopeEquipment).options( query = Select(ScopeEquipment).options(
selectinload(ScopeEquipment.master_equipment)) selectinload(ScopeEquipment.master_equipment)
)
query = query.order_by(desc(ScopeEquipment.created_at)) query = query.order_by(desc(ScopeEquipment.created_at))
@ -48,12 +52,17 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre
if scope_equipment_in.type == ScopeEquipmentType.TEMP: if scope_equipment_in.type == ScopeEquipmentType.TEMP:
# Search for the next or ongoing overhaul session for the given scope # Search for the next or ongoing overhaul session for the given scope
stmt = Select(OverhaulScope.end_date).where( stmt = (
OverhaulScope.type == scope_equipment_in.scope_name, Select(OverhaulScope.end_date)
(OverhaulScope.start_date <= datetime.now()) & ( .where(
OverhaulScope.end_date >= datetime.now()) # Ongoing OverhaulScope.type == scope_equipment_in.scope_name,
| (OverhaulScope.start_date > datetime.now()) # Upcoming (OverhaulScope.start_date <= datetime.now())
).order_by(OverhaulScope.start_date.asc()).limit(1) & (OverhaulScope.end_date >= datetime.now()) # Ongoing
| (OverhaulScope.start_date > datetime.now()), # Upcoming
)
.order_by(OverhaulScope.start_date.asc())
.limit(1)
)
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
removal_date = result.scalar_one_or_none() removal_date = result.scalar_one_or_none()
@ -61,14 +70,16 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre
# If no overhaul found, set a default removal date or handle the error # If no overhaul found, set a default removal date or handle the error
if removal_date is None: if removal_date is None:
# Handle if no overhaul session is found, set default or raise an error # Handle if no overhaul session is found, set default or raise an error
removal_date = datetime.now() + timedelta(days=30) # Example: 30 days from now removal_date = datetime.now() + timedelta(
days=30
) # Example: 30 days from now
for assetnum in assetnums: for assetnum in assetnums:
stmt = insert(ScopeEquipment).values( stmt = insert(ScopeEquipment).values(
assetnum=assetnum, assetnum=assetnum,
scope_overhaul=scope_equipment_in.scope_name, scope_overhaul=scope_equipment_in.scope_name,
type=scope_equipment_in.type, type=scope_equipment_in.type,
removal_date=removal_date removal_date=removal_date,
) )
stmt = stmt.on_conflict_do_nothing( stmt = stmt.on_conflict_do_nothing(
@ -82,7 +93,12 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre
return results return results
async def update(*, db_session: DbSession, scope_equipment: ScopeEquipment, scope_equipment_in: ScopeEquipmentUpdate): async def update(
*,
db_session: DbSession,
scope_equipment: ScopeEquipment,
scope_equipment_in: ScopeEquipmentUpdate
):
"""Updates a document.""" """Updates a document."""
data = scope_equipment_in.model_dump() data = scope_equipment_in.model_dump()
@ -99,11 +115,10 @@ async def update(*, db_session: DbSession, scope_equipment: ScopeEquipment, scop
async def delete(*, db_session: DbSession, assetnum: str): async def delete(*, db_session: DbSession, assetnum: str):
"""Deletes a document.""" """Deletes a document."""
query = Delete(ScopeEquipment).where( query = Delete(ScopeEquipment).where(ScopeEquipment.assetnum == assetnum)
ScopeEquipment.assetnum == assetnum)
await db_session.execute(query) await db_session.execute(query)
await db_session.commit() await db_session.commit()
return assetnum return assetnum
# query = Select(ScopeEquipment).filter( # query = Select(ScopeEquipment).filter(
@ -128,10 +143,13 @@ async def delete(*, db_session: DbSession, assetnum: str):
# await db_session.commit() # await db_session.commit()
async def get_by_scope_name(*, db_session: DbSession, scope_name: Optional[str]) -> Optional[ScopeEquipment]: async def get_by_scope_name(
*, db_session: DbSession, scope_name: Optional[str]
) -> Optional[ScopeEquipment]:
"""Returns a document based on the given document id.""" """Returns a document based on the given document id."""
query = Select(ScopeEquipment).options( query = Select(ScopeEquipment).options(
selectinload(ScopeEquipment.master_equipment)) selectinload(ScopeEquipment.master_equipment)
)
if scope_name: if scope_name:
query = query.filter(ScopeEquipment.scope_overhaul == scope_name) query = query.filter(ScopeEquipment.scope_overhaul == scope_name)
@ -156,11 +174,14 @@ async def get_by_scope_name(*, db_session: DbSession, scope_name: Optional[str])
async def get_all_master_equipment(*, common: CommonParameters, scope_name): async def get_all_master_equipment(*, common: CommonParameters, scope_name):
equipments_scope = [equip.assetnum for equip in await get_by_scope_name( equipments_scope = [
db_session=common.get("db_session"), scope_name=scope_name)] equip.assetnum
for equip in await get_by_scope_name(
db_session=common.get("db_session"), scope_name=scope_name
)
]
query = Select(MasterEquipment).filter( query = Select(MasterEquipment).filter(MasterEquipment.assetnum.is_not(None))
MasterEquipment.assetnum.is_not(None))
# Only add not_in filter if there are items in equipments_scope # Only add not_in filter if there are items in equipments_scope
if equipments_scope: if equipments_scope:
@ -171,9 +192,11 @@ async def get_all_master_equipment(*, common: CommonParameters, scope_name):
async def get_equipment_level_by_no(*, db_session: DbSession, level: int): async def get_equipment_level_by_no(*, db_session: DbSession, level: int):
query = Select(MasterEquipment).join(MasterEquipment.equipment_tree).where( query = (
MasterEquipmentTree.level_no == level) Select(MasterEquipment)
.join(MasterEquipment.equipment_tree)
.where(MasterEquipmentTree.level_no == level)
)
result = await db_session.execute(query) result = await db_session.execute(query)
return result.scalars().all() return result.scalars().all()

@ -1,22 +1,27 @@
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from sqlalchemy.ext.hybrid import hybrid_property
class ScopeEquipmentJob(Base, DefaultMixin): class ScopeEquipmentJob(Base, DefaultMixin):
__tablename__ = "oh_ms_scope_equipment_job" __tablename__ = "oh_ms_scope_equipment_job"
assetnum = Column(String, nullable=False) assetnum = Column(String, nullable=False)
job_id = Column(UUID(as_uuid=True), ForeignKey( job_id = Column(UUID(as_uuid=True), ForeignKey("oh_ms_job.id", ondelete="cascade"))
"oh_ms_job.id", ondelete="cascade"))
master_equipments = relationship( master_equipments = relationship(
"MasterEquipment", lazy="raise", primaryjoin="and_(ScopeEquipmentJob.assetnum == foreign(MasterEquipment.assetnum))", uselist=False) "MasterEquipment",
lazy="raise",
primaryjoin="and_(ScopeEquipmentJob.assetnum == foreign(MasterEquipment.assetnum))",
uselist=False,
)
job = relationship("MasterActivity", lazy="selectin")
job = relationship( overhaul_jobs = relationship(
"MasterActivity", lazy="selectin" "OverhaulJob", back_populates="scope_equipment_job", lazy="selectin"
) )

@ -1,18 +1,21 @@
from typing import Dict, List from typing import Dict, List
from fastapi import APIRouter, HTTPException, Query, status
from .service import get_all, delete, create from fastapi import APIRouter, HTTPException, Query, status
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
from src.database.service import (CommonParameters, DbSession,
search_filter_sort_paginate)
from src.models import StandardResponse from src.models import StandardResponse
from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
from .service import create, delete, get_all
router = APIRouter() router = APIRouter()
@router.get("/{assetnum}", response_model=StandardResponse[ScopeEquipmentJobPagination]) @router.get("/{assetnum}", response_model=StandardResponse[ScopeEquipmentJobPagination])
async def get_scope_equipment_jobs(db_session: DbSession, assetnum, common: CommonParameters): async def get_scope_equipment_jobs(
db_session: DbSession, assetnum, common: CommonParameters
):
"""Get all scope activity pagination.""" """Get all scope activity pagination."""
# return # return
data = await get_all(db_session=db_session, assetnum=assetnum, common=common) data = await get_all(db_session=db_session, assetnum=assetnum, common=common)
@ -24,7 +27,9 @@ async def get_scope_equipment_jobs(db_session: DbSession, assetnum, common: Comm
@router.post("/{assetnum}", response_model=StandardResponse[None]) @router.post("/{assetnum}", response_model=StandardResponse[None])
async def create_scope_equipment_jobs(db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate): async def create_scope_equipment_jobs(
db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate
):
"""Get all scope activity pagination.""" """Get all scope activity pagination."""
# return # return
await create(db_session=db_session, assetnum=assetnum, scope_job_in=scope_job_in) await create(db_session=db_session, assetnum=assetnum, scope_job_in=scope_job_in)

@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, BaseModel from pydantic import BaseModel, Field
from src.job.schema import ActivityMasterRead from src.job.schema import ActivityMasterRead
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination
from src.overhaul_scope.schema import ScopeRead
class ScopeEquipmentJobBase(DefultBase): class ScopeEquipmentJobBase(DefultBase):
@ -21,9 +22,20 @@ class ScopeEquipmentJobUpdate(ScopeEquipmentJobBase):
cost: Optional[str] = Field(0) cost: Optional[str] = Field(0)
class OverhaulActivity(DefultBase):
id: UUID
overhaul_scope: ScopeRead
class OverhaulJob(DefultBase):
id: UUID
overhaul_activity: OverhaulActivity
class ScopeEquipmentJobRead(ScopeEquipmentJobBase): class ScopeEquipmentJobRead(ScopeEquipmentJobBase):
id: UUID id: UUID
job: ActivityMasterRead job: ActivityMasterRead
overhaul_jobs: List[OverhaulJob] = []
class ScopeEquipmentJobPagination(Pagination): class ScopeEquipmentJobPagination(Pagination):

@ -1,20 +1,20 @@
import random import random
from sqlalchemy import Select, Delete, and_
from sqlalchemy.orm import selectinload
from typing import Optional from typing import Optional
from sqlalchemy import Delete, Select, and_
from sqlalchemy.orm import selectinload
from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree
from src.scope_equipment.service import get_equipment_level_by_no from src.scope_equipment.service import get_equipment_level_by_no
from .model import ScopeEquipmentJob from .model import ScopeEquipmentJob
from .schema import ScopeEquipmentJobCreate from .schema import ScopeEquipmentJobCreate
from src.database.core import DbSession from src.overhaul_job.model import OverhaulJob
from src.database.service import CommonParameters, search_filter_sort_paginate from src.overhaul_activity.model import OverhaulActivity
from src.auth.service import CurrentUser
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]: # async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
# """Returns a document based on the given document id.""" # """Returns a document based on the given document id."""
@ -28,8 +28,7 @@ async def get_all(db_session: DbSession, assetnum: Optional[str], common):
raise ValueError("assetnum parameter is required") raise ValueError("assetnum parameter is required")
# First get the parent equipment # First get the parent equipment
equipment_stmt = Select(MasterEquipment).where( equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum)
MasterEquipment.assetnum == assetnum)
equipment: MasterEquipment = await db_session.scalar(equipment_stmt) equipment: MasterEquipment = await db_session.scalar(equipment_stmt)
if not equipment: if not equipment:
@ -38,9 +37,13 @@ async def get_all(db_session: DbSession, assetnum: Optional[str], common):
# Build query for parts # Build query for parts
stmt = ( stmt = (
Select(ScopeEquipmentJob) Select(ScopeEquipmentJob)
.where( .where(ScopeEquipmentJob.assetnum == assetnum)
ScopeEquipmentJob.assetnum == assetnum .options(
).options(selectinload(ScopeEquipmentJob.job)) selectinload(ScopeEquipmentJob.job),
selectinload(ScopeEquipmentJob.overhaul_jobs)
.selectinload(OverhaulJob.overhaul_activity)
.selectinload(OverhaulActivity.overhaul_scope),
)
) )
results = await search_filter_sort_paginate(model=stmt, **common) results = await search_filter_sort_paginate(model=stmt, **common)
@ -48,23 +51,23 @@ async def get_all(db_session: DbSession, assetnum: Optional[str], common):
return results return results
async def create(*, db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate): async def create(
*, db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate
):
scope_jobs = [] scope_jobs = []
if not assetnum: if not assetnum:
raise ValueError("assetnum parameter is required") raise ValueError("assetnum parameter is required")
# First get the parent equipment # First get the parent equipment
equipment_stmt = Select(MasterEquipment).where( equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum)
MasterEquipment.assetnum == assetnum)
equipment: MasterEquipment = await db_session.scalar(equipment_stmt) equipment: MasterEquipment = await db_session.scalar(equipment_stmt)
if not equipment: if not equipment:
raise ValueError(f"No equipment found with assetnum: {assetnum}") raise ValueError(f"No equipment found with assetnum: {assetnum}")
for job_id in scope_job_in.job_ids: for job_id in scope_job_in.job_ids:
scope_equipment_job = ScopeEquipmentJob( scope_equipment_job = ScopeEquipmentJob(assetnum=assetnum, job_id=job_id)
assetnum=assetnum, job_id=job_id)
scope_jobs.append(scope_equipment_job) scope_jobs.append(scope_equipment_job)
db_session.add_all(scope_jobs) db_session.add_all(scope_jobs)

@ -1,10 +1,10 @@
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
from src.workorder.model import MasterWorkOrder from src.workorder.model import MasterWorkOrder
from sqlalchemy.ext.hybrid import hybrid_property
class ScopeEquipmentPart(Base, DefaultMixin): class ScopeEquipmentPart(Base, DefaultMixin):
@ -14,6 +14,8 @@ class ScopeEquipmentPart(Base, DefaultMixin):
stock = Column(Integer, nullable=False, default=0) stock = Column(Integer, nullable=False, default=0)
master_equipments = relationship( master_equipments = relationship(
"MasterEquipment", lazy="raise", primaryjoin="and_(ScopeEquipmentPart.assetnum == foreign(MasterEquipment.assetnum))", uselist=False) "MasterEquipment",
lazy="raise",
primaryjoin="and_(ScopeEquipmentPart.assetnum == foreign(MasterEquipment.assetnum))",
uselist=False,
)

@ -1,13 +1,15 @@
from typing import Dict, List from typing import Dict, List
from fastapi import APIRouter, HTTPException, Query, status from fastapi import APIRouter, HTTPException, Query, status
from src.database.service import (CommonParameters, DbSession,
search_filter_sort_paginate)
from src.models import StandardResponse
from .schema import (ScopeEquipmentActivityCreate,
ScopeEquipmentActivityPagination,
ScopeEquipmentActivityRead, ScopeEquipmentActivityUpdate)
from .service import get_all 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 = APIRouter()

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
from pydantic import Field, BaseModel from pydantic import BaseModel, Field
from src.models import DefultBase, Pagination from src.models import DefultBase, Pagination

@ -1,26 +1,24 @@
import random import random
from sqlalchemy import Select, Delete, and_
from sqlalchemy.orm import selectinload
from typing import Optional from typing import Optional
from sqlalchemy import Delete, Select, and_
from sqlalchemy.orm import selectinload
from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree
from src.scope_equipment.service import get_equipment_level_by_no from src.scope_equipment.service import get_equipment_level_by_no
from .model import ScopeEquipmentPart from .model import ScopeEquipmentPart
from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate 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]: # async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
# """Returns a document based on the given document id.""" # """Returns a document based on the given document id."""
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id) # result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
# return result # return result
def create_dummy_parts(assetnum: str, count: int = 5): def create_dummy_parts(assetnum: str, count: int = 5):
""" """
Create a list of dummy ScopeEquipmentPart objects with random stock values. Create a list of dummy ScopeEquipmentPart objects with random stock values.
@ -37,17 +35,14 @@ def create_dummy_parts(assetnum: str, count: int = 5):
# Generate a unique part asset number # Generate a unique part asset number
part_assetnum = f"{assetnum}_PART_{i}" part_assetnum = f"{assetnum}_PART_{i}"
stock = random.randint(1, 100) # Random stock value between 1 and 100 stock = random.randint(1, 100) # Random stock value between 1 and 100
parts.append({ parts.append({"assetnum": part_assetnum, "stock": stock})
"assetnum": part_assetnum,
"stock": stock
})
return parts return parts
async def get_all(db_session: DbSession, assetnum: Optional[str]): async def get_all(db_session: DbSession, assetnum: Optional[str]):
# Example usage # Example usage
dummy_parts = create_dummy_parts(assetnum, count=10) dummy_parts = create_dummy_parts(assetnum, count=10)
# if not assetnum: # if not assetnum:
# raise ValueError("assetnum parameter is required") # raise ValueError("assetnum parameter is required")

@ -1,10 +1,9 @@
from datetime import datetime, timedelta, timezone
import re import re
from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from dateutil.relativedelta import relativedelta
import pytz import pytz
from dateutil.relativedelta import relativedelta
from src.config import TIMEZONE from src.config import TIMEZONE
@ -61,7 +60,11 @@ def parse_date_string(date_str: str) -> Optional[datetime]:
# Parse the date and set it to start of day in UTC # Parse the date and set it to start of day in UTC
dt = datetime.strptime(date_str, fmt) dt = datetime.strptime(date_str, fmt)
dt = dt.replace( dt = dt.replace(
hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.tzname("Asia/Jakarta") hour=0,
minute=0,
second=0,
microsecond=0,
tzinfo=timezone.tzname("Asia/Jakarta"),
) )
return dt return dt
except ValueError: except ValueError:

@ -1,12 +1,8 @@
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey
from src.database.core import Base from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from sqlalchemy.orm import relationship
from src.models import DefaultMixin
class MasterWorkOrder(Base, DefaultMixin): class MasterWorkOrder(Base, DefaultMixin):
@ -17,5 +13,8 @@ class MasterWorkOrder(Base, DefaultMixin):
workgroup = Column(String, nullable=True) workgroup = Column(String, nullable=True)
total_cost_max = Column(Float, nullable=True) total_cost_max = Column(Float, nullable=True)
scope_equipments = relationship(
scope_equipments = relationship("ScopeEquipment", lazy="raise", primaryjoin="and_(MasterWorkOrder.assetnum == foreign(ScopeEquipment.assetnum))") "ScopeEquipment",
lazy="raise",
primaryjoin="and_(MasterWorkOrder.assetnum == foreign(ScopeEquipment.assetnum))",
)

@ -1,13 +1,12 @@
import asyncio import asyncio
from typing import AsyncGenerator, Generator from typing import AsyncGenerator, Generator
import pytest import pytest
from httpx import AsyncClient from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy_utils import database_exists, drop_database
import pytest
from sqlalchemy_utils import drop_database, database_exists
from starlette.config import environ from starlette.config import environ
from starlette.testclient import TestClient from starlette.testclient import TestClient
@ -66,4 +65,4 @@ async def setup_db() -> AsyncGenerator[None, None]:
@pytest.fixture @pytest.fixture
async def client() -> AsyncGenerator[AsyncClient, None]: async def client() -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient(app=app, base_url="http://test") as client: async with AsyncClient(app=app, base_url="http://test") as client:
yield client yield client

@ -1,22 +1,18 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from factory import ( from factory import (LazyAttribute, LazyFunction, SelfAttribute, Sequence,
LazyAttribute, SubFactory, post_generation)
LazyFunction,
Sequence,
SubFactory,
post_generation,
SelfAttribute,
)
from factory.alchemy import SQLAlchemyModelFactory from factory.alchemy import SQLAlchemyModelFactory
from factory.fuzzy import FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyText from factory.fuzzy import FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyText
from faker import Faker from faker import Faker
from faker.providers import misc from faker.providers import misc
from .database import Session
# from pytz import UTC # from pytz import UTC
from .database import Session
fake = Faker() fake = Faker()
fake.add_provider(misc) fake.add_provider(misc)

Loading…
Cancel
Save