Compare commits
10 Commits
d2ecf0849e
...
9322a89822
| Author | SHA1 | Date |
|---|---|---|
|
|
9322a89822 | 2 weeks ago |
|
|
2ba2c5750a | 4 weeks ago |
|
|
e67feed201 | 4 weeks ago |
|
|
044da9735a | 4 weeks ago |
|
|
0ecae96664 | 4 weeks ago |
|
|
95ae35c0fe | 1 month ago |
|
|
98b50b6532 | 1 month ago |
|
|
1029cd5ca9 | 1 month ago |
|
|
20ea1b580a | 1 month ago |
|
|
19066475c9 | 1 month ago |
@ -0,0 +1,2 @@
|
|||||||
|
tests/
|
||||||
|
.venv/
|
||||||
@ -1,3 +1,4 @@
|
|||||||
env/
|
env/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
venv/
|
venv/
|
||||||
|
tests/
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from src.config import HOST, PORT, ENV
|
from src.config import HOST, PORT, ENV
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
is_dev = str(ENV).lower() == 'development'
|
||||||
is_dev = str(ENV).lower() == "development"
|
uvicorn.run('src.main:app', host=HOST, port=PORT, reload=is_dev)
|
||||||
uvicorn.run("src.main:app", host=HOST, port=PORT, reload=is_dev)
|
|
||||||
|
|||||||
@ -1,183 +1,45 @@
|
|||||||
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 \
|
from src.calculation_budget_constrains.router import router as calculation_budget_constraint
|
||||||
router as calculation_budget_constraint
|
from src.calculation_target_reliability.router import router as calculation_target_reliability
|
||||||
from src.calculation_target_reliability.router import \
|
from src.calculation_time_constrains.router import router as calculation_time_constrains_router, get_calculation
|
||||||
router as calculation_target_reliability
|
|
||||||
from src.calculation_time_constrains.router import \
|
|
||||||
router as calculation_time_constrains_router, get_calculation
|
|
||||||
# from src.job.router import router as job_router
|
|
||||||
|
|
||||||
from src.overhaul.router import router as overhaul_router
|
from src.overhaul.router import router as overhaul_router
|
||||||
from src.standard_scope.router import router as standard_scope_router
|
from src.standard_scope.router import router as standard_scope_router
|
||||||
from src.overhaul_activity.router import router as overhaul_activity_router
|
from src.overhaul_activity.router import router as overhaul_activity_router
|
||||||
from src.workscope_group.router import router as workscope_group_router
|
from src.workscope_group.router import router as workscope_group_router
|
||||||
from src.equipment_workscope_group.router import router as equipment_workscope_group_router
|
from src.equipment_workscope_group.router import router as equipment_workscope_group_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_schedule.router import router as overhaul_schedule_router
|
|
||||||
from src.overhaul_gantt.router import router as gantt_router
|
from src.overhaul_gantt.router import router as gantt_router
|
||||||
from src.sparepart.router import router as sparepart_router
|
from src.sparepart.router import router as sparepart_router
|
||||||
from src.equipment_sparepart.router import router as equipment_sparepart_router
|
from src.equipment_sparepart.router import router as equipment_sparepart_router
|
||||||
|
|
||||||
# from src.overhaul_scope.router import router as scope_router
|
|
||||||
# from src.scope_equipment.router import router as scope_equipment_router
|
|
||||||
# from src.overhaul.router import router as overhaul_router
|
|
||||||
# from src.overhaul_history.router import router as overhaul_history_router
|
|
||||||
# from src.overhaul_activity.router import router as scope_equipment_activity_router
|
|
||||||
from src.overhaul_scope.router import router as ovehaul_schedule_router
|
from src.overhaul_scope.router import router as ovehaul_schedule_router
|
||||||
# from src.scope_equipment_part.router import router as scope_equipment_part_router
|
|
||||||
# from src.calculation_target_reliability.router import router as calculation_target_reliability
|
|
||||||
#
|
|
||||||
# from src.master_activity.router import router as activity_router
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorMessage(BaseModel):
|
class ErrorMessage(BaseModel):
|
||||||
msg: str
|
msg: str
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(BaseModel):
|
class ErrorResponse(BaseModel):
|
||||||
detail: Optional[List[ErrorMessage]]
|
detail: Optional[List[ErrorMessage]]
|
||||||
|
api_router = APIRouter(default_response_class=JSONResponse, responses={400: {'model': ErrorResponse}, 401: {'model': ErrorResponse}, 403: {'model': ErrorResponse}, 404: {'model': ErrorResponse}, 500: {'model': ErrorResponse}})
|
||||||
|
|
||||||
|
@api_router.get('/healthcheck', include_in_schema=False)
|
||||||
api_router = APIRouter(
|
|
||||||
default_response_class=JSONResponse,
|
|
||||||
responses={
|
|
||||||
400: {"model": ErrorResponse},
|
|
||||||
401: {"model": ErrorResponse},
|
|
||||||
403: {"model": ErrorResponse},
|
|
||||||
404: {"model": ErrorResponse},
|
|
||||||
500: {"model": ErrorResponse},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/healthcheck", include_in_schema=False)
|
|
||||||
def healthcheck():
|
def healthcheck():
|
||||||
return {"status": "ok"}
|
return {'status': 'ok'}
|
||||||
|
authenticated_api_router = APIRouter(dependencies=[Depends(JWTBearer())])
|
||||||
|
authenticated_api_router.include_router(overhaul_router, prefix='/overhauls', tags=['overhaul'])
|
||||||
authenticated_api_router = APIRouter(
|
authenticated_api_router.include_router(standard_scope_router, prefix='/scope-equipments', tags=['scope_equipment'])
|
||||||
dependencies=[Depends(JWTBearer())],
|
authenticated_api_router.include_router(overhaul_activity_router, prefix='/overhaul-activity', tags=['activity'])
|
||||||
)
|
authenticated_api_router.include_router(workscope_group_router, prefix='/workscopes', tags=['workscope_groups'])
|
||||||
# overhaul data
|
authenticated_api_router.include_router(sparepart_router, prefix='/spareparts', tags=['sparepart'])
|
||||||
authenticated_api_router.include_router(
|
authenticated_api_router.include_router(equipment_workscope_group_router, prefix='/equipment-workscopes', tags=['equipment_workscope_groups'])
|
||||||
overhaul_router, prefix="/overhauls", tags=["overhaul"]
|
authenticated_api_router.include_router(equipment_sparepart_router, prefix='/equipment-spareparts', tags=['equipment_workscope_sparepart_router'])
|
||||||
)
|
authenticated_api_router.include_router(gantt_router, prefix='/overhaul-gantt', tags=['gantt'])
|
||||||
|
authenticated_api_router.include_router(ovehaul_schedule_router, prefix='/overhaul-schedules', tags=['overhaul_schedules'])
|
||||||
|
calculation_router = APIRouter(prefix='/calculation', tags=['calculations'])
|
||||||
# authenticated_api_router.include_router(job_router, prefix="/jobs", tags=["job"])
|
calculation_router.include_router(calculation_time_constrains_router, prefix='/time-constraint', tags=['calculation', 'time_constraint'])
|
||||||
|
calculation_router.include_router(calculation_target_reliability, prefix='/target-reliability', tags=['calculation', 'target_reliability'])
|
||||||
# # # Overhaul session data
|
calculation_router.include_router(calculation_budget_constraint, prefix='/budget-constraint', tags=['calculation', 'budget_constraint'])
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# scope_router, prefix="/overhaul-session", tags=["overhaul-session"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
standard_scope_router, prefix="/scope-equipments", tags=["scope_equipment"]
|
|
||||||
)
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
overhaul_activity_router, prefix="/overhaul-activity", tags=["activity"]
|
|
||||||
)
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
workscope_group_router, prefix="/workscopes", tags=["workscope_groups"]
|
|
||||||
)
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
sparepart_router, prefix="/spareparts", tags=["sparepart"]
|
|
||||||
)
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
equipment_workscope_group_router,
|
|
||||||
prefix="/equipment-workscopes",
|
|
||||||
tags=["equipment_workscope_groups"],
|
|
||||||
)
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
equipment_sparepart_router,
|
|
||||||
prefix="/equipment-spareparts",
|
|
||||||
tags=["equipment_workscope_sparepart_router"]
|
|
||||||
)
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# scope_equipment_job_router,
|
|
||||||
# prefix="/scope-equipment-jobs",
|
|
||||||
# tags=["scope_equipment", "job"],
|
|
||||||
# )
|
|
||||||
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# overhaul_schedule_router,
|
|
||||||
# prefix="/overhaul-schedules",
|
|
||||||
# tags=["overhaul_schedule"],
|
|
||||||
# )
|
|
||||||
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# job_overhaul_router, prefix="/overhaul-jobs", tags=["job", "overhaul"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
gantt_router, prefix="/overhaul-gantt", tags=["gantt"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# overhaul_history_router, prefix="/overhaul-history", tags=["overhaul_history"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# scope_equipment_activity_router, prefix="/equipment-activities", tags=["scope_equipment_activities"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# activity_router, prefix="/activities", tags=["activities"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# authenticated_api_router.include_router(
|
|
||||||
# scope_equipment_part_router, prefix="/equipment-parts", tags=["scope_equipment_parts"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(
|
|
||||||
ovehaul_schedule_router, prefix="/overhaul-schedules", tags=["overhaul_schedules"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# calculation
|
|
||||||
calculation_router = APIRouter(prefix="/calculation", tags=["calculations"])
|
|
||||||
|
|
||||||
# Time constrains
|
|
||||||
calculation_router.include_router(
|
|
||||||
calculation_time_constrains_router,
|
|
||||||
prefix="/time-constraint",
|
|
||||||
tags=["calculation", "time_constraint"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Target reliability
|
|
||||||
calculation_router.include_router(
|
|
||||||
calculation_target_reliability,
|
|
||||||
prefix="/target-reliability",
|
|
||||||
tags=["calculation", "target_reliability"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# # Budget Constrain
|
|
||||||
calculation_router.include_router(
|
|
||||||
calculation_budget_constraint,
|
|
||||||
prefix="/budget-constraint",
|
|
||||||
tags=["calculation", "budget_constraint"],
|
|
||||||
)
|
|
||||||
|
|
||||||
authenticated_api_router.include_router(calculation_router)
|
authenticated_api_router.include_router(calculation_router)
|
||||||
|
authenticated_api_router.include_router(get_calculation, prefix='/calculation/time-constraint', tags=['calculation', 'time_constraint'])
|
||||||
authenticated_api_router.include_router(
|
|
||||||
get_calculation,
|
|
||||||
prefix="/calculation/time-constraint",
|
|
||||||
tags=["calculation", "time_constraint"],
|
|
||||||
)
|
|
||||||
|
|
||||||
api_router.include_router(authenticated_api_router)
|
api_router.include_router(authenticated_api_router)
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,10 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
# Some users from auth service may not have a display name.
|
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
role: str
|
role: str
|
||||||
user_id: str
|
user_id: str
|
||||||
|
|||||||
@ -1,247 +1,135 @@
|
|||||||
# app/auth/auth_bearer.py
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import Depends, HTTPException, Request
|
from fastapi import Depends, HTTPException, Request
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
from sqlalchemy.sql.expression import false
|
|
||||||
|
|
||||||
import src.config as config
|
import src.config as config
|
||||||
|
|
||||||
from .model import UserBase
|
from .model import UserBase
|
||||||
from .util import extract_template
|
from .util import extract_template
|
||||||
|
|
||||||
class JWTBearer(HTTPBearer):
|
class JWTBearer(HTTPBearer):
|
||||||
def __init__(self, auto_error: bool = True):
|
|
||||||
# Always pass auto_error=False so that HTTPBearer returns None (instead of
|
def __init__(self, auto_error: bool=True):
|
||||||
# raising HTTP 403) when the Authorization header is absent. We raise 401
|
|
||||||
# ourselves in the else-branch below, which is the correct status for a
|
|
||||||
# missing / unauthenticated credential.
|
|
||||||
super(JWTBearer, self).__init__(auto_error=False)
|
super(JWTBearer, self).__init__(auto_error=False)
|
||||||
|
|
||||||
async def __call__(self, request: Request):
|
async def __call__(self, request: Request):
|
||||||
credentials: HTTPAuthorizationCredentials = await super(
|
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
|
||||||
JWTBearer, self
|
|
||||||
).__call__(request)
|
|
||||||
if credentials:
|
if credentials:
|
||||||
if not credentials.scheme == "Bearer":
|
if not credentials.scheme == 'Bearer':
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail='Invalid authentication scheme.')
|
||||||
status_code=401, detail="Invalid authentication scheme."
|
|
||||||
)
|
|
||||||
method = request.method
|
method = request.method
|
||||||
|
if method == 'OPTIONS':
|
||||||
if method == "OPTIONS":
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
path = extract_template(request.url.path, request.path_params)
|
path = extract_template(request.url.path, request.path_params)
|
||||||
|
endpoint = f'/optimumoh{path}'
|
||||||
endpoint = f"/optimumoh{path}"
|
|
||||||
|
|
||||||
user_info, message = self.verify_jwt(credentials.credentials, method, endpoint)
|
user_info, message = self.verify_jwt(credentials.credentials, method, endpoint)
|
||||||
|
|
||||||
if not user_info:
|
if not user_info:
|
||||||
if isinstance(message, dict):
|
if isinstance(message, dict):
|
||||||
message = message.get("message") or message.get("detail") or "Invalid token or expired token."
|
message = message.get('message') or message.get('detail') or 'Invalid token or expired token.'
|
||||||
else:
|
else:
|
||||||
message = str(message) if message else "Invalid token or expired token."
|
message = str(message) if message else 'Invalid token or expired token.'
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail=message)
|
||||||
status_code=401, detail=message
|
|
||||||
)
|
|
||||||
|
|
||||||
request.state.user = message
|
request.state.user = message
|
||||||
|
|
||||||
from src.context import set_user_id, set_username, set_role
|
from src.context import set_user_id, set_username, set_role
|
||||||
if hasattr(message, "user_id"):
|
if hasattr(message, 'user_id'):
|
||||||
set_user_id(str(message.user_id))
|
set_user_id(str(message.user_id))
|
||||||
username = getattr(message, "username", None) or getattr(message, "name", None)
|
username = getattr(message, 'username', None) or getattr(message, 'name', None)
|
||||||
if username:
|
if username:
|
||||||
set_username(username)
|
set_username(username)
|
||||||
if hasattr(message, "role"):
|
if hasattr(message, 'role'):
|
||||||
set_role(message.role)
|
set_role(message.role)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
else:
|
raise HTTPException(status_code=401, detail='Invalid authorization code.')
|
||||||
raise HTTPException(status_code=401, detail="Invalid authorization code.")
|
|
||||||
|
|
||||||
def verify_jwt(self, jwtoken: str, method: str, endpoint: str):
|
def verify_jwt(self, jwtoken: str, method: str, endpoint: str):
|
||||||
try:
|
try:
|
||||||
url_to_verify = f"{config.AUTH_SERVICE_API}/verify-token"
|
url_to_verify = f'{config.AUTH_SERVICE_API}/verify-token'
|
||||||
response = requests.get(
|
response = requests.get(url_to_verify, headers={'Authorization': f'Bearer {jwtoken}'})
|
||||||
url_to_verify,
|
|
||||||
headers={"Authorization": f"Bearer {jwtoken}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
return False, response.json()
|
return (False, response.json())
|
||||||
|
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
return True, UserBase(**user_data["data"])
|
return (True, 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)}')
|
||||||
return False, str(e)
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
# Create dependency to get current user from request state
|
|
||||||
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')
|
||||||
if token:
|
if token:
|
||||||
return token.replace("Bearer ", "") # Menghapus prefix "Bearer "
|
return token.replace('Bearer ', '')
|
||||||
else:
|
return request.cookies.get('access_token')
|
||||||
return request.cookies.get("access_token") # Fallback ke cookie
|
return ''
|
||||||
|
|
||||||
return "" # Mengembalikan token atau None jika tidak ada
|
|
||||||
|
|
||||||
|
|
||||||
async def internal_key(request: Request):
|
async def internal_key(request: Request):
|
||||||
token = request.headers.get("Authorization")
|
token = request.headers.get('Authorization')
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
api_key = request.headers.get("X-Internal-Key")
|
api_key = request.headers.get('X-Internal-Key')
|
||||||
|
|
||||||
if api_key != config.API_KEY:
|
if api_key != config.API_KEY:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail='Invalid Key.')
|
||||||
status_code=401, detail="Invalid Key."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers = {'Content-Type': 'application/json'}
|
||||||
'Content-Type': 'application/json'
|
response = requests.post(f'{config.AUTH_SERVICE_API}/sign-in', headers=headers, data=json.dumps({'username': 'ohuser', 'password': '123456789'}))
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{config.AUTH_SERVICE_API}/sign-in",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps({
|
|
||||||
"username": "ohuser",
|
|
||||||
"password": "123456789"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
print(str(response.json()))
|
print(str(response.json()))
|
||||||
raise Exception("error auth")
|
raise Exception('error auth')
|
||||||
|
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
return user_data['data']['access_token']
|
return user_data['data']['access_token']
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(str(e))
|
raise Exception(str(e))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
verify_url = f"{config.AUTH_SERVICE_API}/verify-token"
|
verify_url = f'{config.AUTH_SERVICE_API}/verify-token'
|
||||||
response = requests.get(
|
response = requests.get(verify_url, headers={'Authorization': f'{token}'})
|
||||||
verify_url,
|
|
||||||
headers={"Authorization": f"{token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail='Invalid token.')
|
||||||
status_code=401, detail="Invalid token."
|
return token.split(' ')[1]
|
||||||
)
|
|
||||||
|
|
||||||
return token.split(" ")[1]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Token verification error: {str(e)}")
|
print(f'Token verification error: {str(e)}')
|
||||||
return False, str(e)
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import src.config as config
|
import src.config as config
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
AUTH_NOTIFY_ENDPOINT = f'{config.AUTH_SERVICE_API}/admin/notify-limit'
|
||||||
|
|
||||||
|
async def notify_admin_on_rate_limit(endpoint_name: str, ip_address: str, request: Request, method: str='POST', cooldown: int=900, timeout: int=5) -> Dict[str, Any]:
|
||||||
AUTH_NOTIFY_ENDPOINT = f"{config.AUTH_SERVICE_API}/admin/notify-limit"
|
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', ''), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
||||||
|
token = request.headers.get('Authorization')
|
||||||
|
headers = {'Authorization': token} if token else {}
|
||||||
async def notify_admin_on_rate_limit(
|
|
||||||
endpoint_name: str,
|
|
||||||
ip_address: str,
|
|
||||||
request: Request,
|
|
||||||
method: str = "POST",
|
|
||||||
cooldown: int = 900,
|
|
||||||
timeout: int = 5
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Kirim notifikasi ke admin via be-auth service ketika rate limit terlampaui.
|
|
||||||
|
|
||||||
Async version - gunakan di async context.
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"endpoint_name": f"oh/{endpoint_name}".replace("//", ""),
|
|
||||||
"ip_address": ip_address,
|
|
||||||
"method": method,
|
|
||||||
"cooldown": cooldown,
|
|
||||||
}
|
|
||||||
token = request.headers.get("Authorization")
|
|
||||||
headers = {"Authorization": token} if token else {}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.to_thread(
|
response = await asyncio.to_thread(requests.post, AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
||||||
requests.post, AUTH_NOTIFY_ENDPOINT,
|
|
||||||
json=payload, headers=headers, timeout=timeout
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
log.info(f"Notifikasi admin sent | Endpoint: {endpoint_name}")
|
log.info(f'Notifikasi admin sent | Endpoint: {endpoint_name}')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error notifying admin: {str(e)}")
|
log.error(f'Error notifying admin: {str(e)}')
|
||||||
return {"status": False, "message": str(e), "data": payload}
|
return {'status': False, 'message': str(e), 'data': payload}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def notify_admin_on_rate_limit_sync(
|
|
||||||
endpoint_name: str,
|
|
||||||
ip_address: str,
|
|
||||||
request: Request,
|
|
||||||
method: str = "POST",
|
|
||||||
cooldown: int = 900,
|
|
||||||
timeout: int = 5
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Kirim notifikasi ke admin via be-auth service.
|
|
||||||
|
|
||||||
Sync version - gunakan di exception handler atau sync context.
|
|
||||||
RECOMMENDED untuk use case ini.
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"endpoint_name": f"oh/{endpoint_name}".replace("//", "/"),
|
|
||||||
"ip_address": ip_address,
|
|
||||||
"method": method,
|
|
||||||
"cooldown": cooldown,
|
|
||||||
}
|
|
||||||
token = request.headers.get("Authorization")
|
|
||||||
headers = {"Authorization": token} if token else {}
|
|
||||||
|
|
||||||
|
def notify_admin_on_rate_limit_sync(endpoint_name: str, ip_address: str, request: Request, method: str='POST', cooldown: int=900, timeout: int=5) -> Dict[str, Any]:
|
||||||
|
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', '/'), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
||||||
|
token = request.headers.get('Authorization')
|
||||||
|
headers = {'Authorization': token} if token else {}
|
||||||
try:
|
try:
|
||||||
response = requests.post(AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
response = requests.post(AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
log.info(f"Notifikasi admin sent | Endpoint: {endpoint_name}")
|
log.info(f'Notifikasi admin sent | Endpoint: {endpoint_name}')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error notifying admin: {str(e)}")
|
log.error(f'Error notifying admin: {str(e)}')
|
||||||
return {"status": False, "message": str(e), "data": payload}
|
return {'status': False, 'message': str(e), 'data': payload}
|
||||||
|
|
||||||
|
async def admin_required(request: Request):
|
||||||
|
user = await get_current_user(request)
|
||||||
|
if user.role != 'Admin' or user.role != 'Superadmin':
|
||||||
|
raise HTTPException(status_code=403, detail='Invalid authorization code.')
|
||||||
|
return user
|
||||||
|
Admin = Annotated[UserBase, Depends(admin_required)]
|
||||||
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)]
|
||||||
InternalKey = Annotated[str, Depends(internal_key)]
|
InternalKey = Annotated[str, Depends(internal_key)]
|
||||||
@ -1,9 +1,6 @@
|
|||||||
def extract_template(path_string, value_dict):
|
def extract_template(path_string, value_dict):
|
||||||
template = path_string
|
template = path_string
|
||||||
|
|
||||||
# Replace each value in the dict with its corresponding key placeholder
|
|
||||||
for key, value in value_dict.items():
|
for key, value in value_dict.items():
|
||||||
if str(value) in template:
|
if str(value) in template:
|
||||||
template = template.replace(str(value), f'<{key}>')
|
template = template.replace(str(value), f'<{key}>')
|
||||||
|
|
||||||
return template
|
return template
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,46 +1,21 @@
|
|||||||
from typing import Annotated, Dict, List, Optional
|
from typing import Annotated, Dict
|
||||||
|
from fastapi import APIRouter
|
||||||
from fastapi import APIRouter, HTTPException, status
|
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.auth.service import Token
|
from src.auth.service import Token
|
||||||
from src.calculation_budget_constrains.schema import BudgetContraintQuery
|
from src.calculation_budget_constrains.schema import BudgetContraintQuery
|
||||||
from src.calculation_target_reliability.service import get_simulation_results
|
from src.calculation_target_reliability.service import get_simulation_results
|
||||||
from src.config import TC_RBD_ID
|
from src.config import TC_RBD_ID
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
|
||||||
from .service import get_all_budget_constrains
|
from .service import get_all_budget_constrains
|
||||||
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('/{session_id}', response_model=StandardResponse[Dict], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("/{session_id}", response_model=StandardResponse[Dict], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
async def get_target_reliability(db_session: DbSession, token: Token, session_id: str, collector_db: CollectorDbSession, params: Annotated[BudgetContraintQuery, Query()]):
|
||||||
async def get_target_reliability(
|
|
||||||
db_session: DbSession,
|
|
||||||
token: Token,
|
|
||||||
session_id: str,
|
|
||||||
collector_db: CollectorDbSession,
|
|
||||||
params: Annotated[BudgetContraintQuery, Query()],
|
|
||||||
):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
cost_threshold = params.cost_threshold
|
cost_threshold = params.cost_threshold
|
||||||
results = await get_simulation_results(
|
results = await get_simulation_results(simulation_id=TC_RBD_ID, token=token)
|
||||||
simulation_id = TC_RBD_ID,
|
results, consequence = await get_all_budget_constrains(db_session=db_session, session_id=session_id, cost_threshold=cost_threshold, simulation_result=results, collector_db=collector_db)
|
||||||
token=token
|
return StandardResponse(data={'results': results, 'consequence': consequence}, message='Data retrieved successfully')
|
||||||
)
|
|
||||||
|
|
||||||
results, consequence = await get_all_budget_constrains(
|
|
||||||
db_session=db_session, session_id=session_id, cost_threshold=cost_threshold, simulation_result=results, collector_db=collector_db
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data={
|
|
||||||
"results": results,
|
|
||||||
"consequence": consequence
|
|
||||||
},
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,178 +1,108 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import random
|
import math
|
||||||
from typing import Optional
|
from typing import Optional, List, Dict, Tuple
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import Delete, Select
|
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.contribution_util import calculate_contribution_accurate
|
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
# from src.scope_equipment.model import ScopeEquipment
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||||
# from src.scope_equipment.service import get_by_scope_name
|
|
||||||
from src.overhaul_activity.service import get_all_by_session_id, get_standard_scope_by_session_id
|
|
||||||
|
|
||||||
# async def get_all_budget_constrains(
|
|
||||||
# *, db_session: DbSession, session_id: str, cost_threshold: float = 100000000
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# At the module level, add this dictionary to store persistent EAF values
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from typing import List, Dict, Tuple
|
from typing import List, Dict, Tuple
|
||||||
|
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||||
from src.contribution_util import calculate_contribution_accurate
|
|
||||||
|
|
||||||
|
|
||||||
async def get_all_budget_constrains(
|
|
||||||
*,
|
|
||||||
db_session: DbSession,
|
|
||||||
collector_db: CollectorDbSession,
|
|
||||||
session_id: UUID,
|
|
||||||
simulation_result: Dict,
|
|
||||||
cost_threshold: float = 100_000_000,
|
|
||||||
use_optimal: bool = True, # default = optimal (knapsack)
|
|
||||||
) -> Tuple[List[Dict], List[Dict]]:
|
|
||||||
"""
|
|
||||||
Select equipment under budget constraint using contribution + cost efficiency.
|
|
||||||
Returns (priority_list, consequence_list).
|
|
||||||
"""
|
|
||||||
|
|
||||||
calc_result = simulation_result["calc_result"]
|
|
||||||
plant_result = simulation_result["plant_result"]
|
|
||||||
|
|
||||||
equipments = await get_standard_scope_by_session_id(
|
async def get_all_budget_constrains(*, db_session: DbSession, collector_db: CollectorDbSession, session_id: UUID, simulation_result: Dict, cost_threshold: float=100000000, use_optimal: bool=True) -> Tuple[List[Dict], List[Dict]]:
|
||||||
db_session=db_session,
|
calc_result = simulation_result['calc_result']
|
||||||
overhaul_session_id=session_id,
|
plant_result = simulation_result['plant_result']
|
||||||
collector_db=collector_db,
|
equipments = await get_standard_scope_by_session_id(db_session=db_session, overhaul_session_id=session_id, collector_db=collector_db)
|
||||||
)
|
|
||||||
if not equipments:
|
if not equipments:
|
||||||
return [], []
|
return ([], [])
|
||||||
|
|
||||||
# Flatten results
|
|
||||||
eq_results = calc_result if isinstance(calc_result, list) else [calc_result]
|
eq_results = calc_result if isinstance(calc_result, list) else [calc_result]
|
||||||
|
equipments_eaf_contribution = calculate_asset_eaf_contributions(plant_result=plant_result, eq_results=eq_results)
|
||||||
# Calculate contribution map (node_name → contribution)
|
|
||||||
equipments_eaf_contribution = calculate_asset_eaf_contributions(
|
|
||||||
plant_result=plant_result, eq_results=eq_results
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build base result list
|
|
||||||
result = []
|
result = []
|
||||||
for equipment in equipments:
|
for equipment in equipments:
|
||||||
contribution = equipments_eaf_contribution.get(equipment.location_tag, 0.0)
|
contribution = equipments_eaf_contribution.get(equipment.location_tag, 0.0)
|
||||||
total_cost = (equipment.overhaul_cost or 0) + (equipment.service_cost or 0)
|
total_cost = (equipment.overhaul_cost or 0) + (equipment.service_cost or 0)
|
||||||
|
result.append({'id': equipment.id, 'location_tag': equipment.location_tag, 'name': equipment.equipment_name, 'total_cost': total_cost, 'eaf_contribution': contribution})
|
||||||
result.append(
|
total_contrib = sum((item['eaf_contribution'] for item in result)) or 1.0
|
||||||
{
|
|
||||||
"id": equipment.id,
|
|
||||||
"location_tag": equipment.location_tag,
|
|
||||||
"name": equipment.equipment_name,
|
|
||||||
"total_cost": total_cost,
|
|
||||||
"eaf_contribution": contribution,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Normalize contribution so sum = 1.0
|
|
||||||
total_contrib = sum(item["eaf_contribution"] for item in result) or 1.0
|
|
||||||
for item in result:
|
for item in result:
|
||||||
item["contribution_norm"] = item["eaf_contribution"] / total_contrib
|
item['contribution_norm'] = item['eaf_contribution'] / total_contrib
|
||||||
|
|
||||||
# Calculate efficiency and composite score
|
|
||||||
for item in result:
|
for item in result:
|
||||||
cost = item["total_cost"] or 1.0
|
cost = item['total_cost'] or 1.0
|
||||||
efficiency = item["contribution_norm"] / cost
|
item['contribution_norm'] / cost
|
||||||
item["priority_score"] = item["contribution_norm"]
|
item['priority_score'] = item['contribution_norm']
|
||||||
|
|
||||||
# Choose method
|
|
||||||
if use_optimal:
|
if use_optimal:
|
||||||
priority_list, consequence_list = knapsack_selection(result, cost_threshold)
|
priority_list, consequence_list = knapsack_selection(result, cost_threshold)
|
||||||
else:
|
else:
|
||||||
priority_list, consequence_list = greedy_selection(result, cost_threshold)
|
priority_list, consequence_list = greedy_selection(result, cost_threshold)
|
||||||
|
return (priority_list, consequence_list)
|
||||||
return priority_list, consequence_list
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_asset_eaf_contributions(plant_result, eq_results):
|
def calculate_asset_eaf_contributions(plant_result, eq_results):
|
||||||
"""
|
|
||||||
Calculate each asset's negative contribution to plant EAF.
|
|
||||||
Key assumption: eq_results have aeros_node.node_name matching equipment.location_tag.
|
|
||||||
"""
|
|
||||||
results = defaultdict(float)
|
results = defaultdict(float)
|
||||||
for asset in eq_results:
|
for asset in eq_results:
|
||||||
node_name = asset.get("aeros_node", {}).get("node_name")
|
node_name = asset.get('aeros_node', {}).get('node_name')
|
||||||
if node_name:
|
if node_name:
|
||||||
results[node_name] = asset.get("contribution_factor", 0.0)
|
results[node_name] = asset.get('contribution_factor', 0.0)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def greedy_selection(equipments: List[dict], budget: float) -> Tuple[List[dict], List[dict]]:
|
def greedy_selection(equipments: List[dict], budget: float) -> Tuple[List[dict], List[dict]]:
|
||||||
"""Greedy fallback: select items by score until budget is used."""
|
equipments_sorted = sorted(equipments, key=lambda x: x['priority_score'], reverse=True)
|
||||||
# Sort by priority_score descending
|
current_cost = 0.0
|
||||||
equipments_sorted = sorted(equipments, key=lambda x: x["priority_score"], reverse=True)
|
selected, excluded = ([], [])
|
||||||
total_cost = 0
|
|
||||||
selected, excluded = [], []
|
|
||||||
|
|
||||||
for eq in equipments_sorted:
|
for eq in equipments_sorted:
|
||||||
if total_cost + eq["cost"] <= budget:
|
cost = eq.get('total_cost', 0.0)
|
||||||
|
if current_cost + cost <= budget:
|
||||||
selected.append(eq)
|
selected.append(eq)
|
||||||
total_cost += eq["cost"]
|
current_cost += cost
|
||||||
else:
|
else:
|
||||||
excluded.append(eq)
|
excluded.append(eq)
|
||||||
|
return (selected, excluded)
|
||||||
|
|
||||||
return selected, excluded
|
def knapsack_selection(equipments: List[dict], budget: float, scale: Optional[float]=None) -> Tuple[List[dict], List[dict]]:
|
||||||
|
if not equipments:
|
||||||
|
return ([], [])
|
||||||
def knapsack_selection(equipments: List[dict], budget: float, scale: int = 10_000_000) -> Tuple[List[dict], List[dict]]:
|
eligible_items = []
|
||||||
"""
|
strictly_excluded = []
|
||||||
Select equipment optimally within budget using 0/1 knapsack DP.
|
for eq in equipments:
|
||||||
Uses scaling + 1D DP optimization to avoid MemoryError.
|
if eq['total_cost'] > budget:
|
||||||
Falls back to greedy if W is too large.
|
strictly_excluded.append(eq)
|
||||||
"""
|
else:
|
||||||
n = len(equipments)
|
eligible_items.append(eq)
|
||||||
|
if not eligible_items:
|
||||||
# Scale costs + budget
|
return ([], strictly_excluded)
|
||||||
costs = [int(eq["total_cost"] // scale) for eq in equipments]
|
n = len(eligible_items)
|
||||||
values = [eq["priority_score"] for eq in equipments]
|
if scale is None:
|
||||||
|
target_W = 2000
|
||||||
|
scale = max(1.0, budget / target_W)
|
||||||
|
costs = [int(math.ceil(eq['total_cost'] / scale)) for eq in eligible_items]
|
||||||
|
values = [eq['priority_score'] for eq in eligible_items]
|
||||||
W = int(budget // scale)
|
W = int(budget // scale)
|
||||||
|
if W > 1000000:
|
||||||
# Fallback if W is still too large
|
|
||||||
if W > 1_000_000:
|
|
||||||
print("too big")
|
|
||||||
return greedy_selection(equipments, budget)
|
return greedy_selection(equipments, budget)
|
||||||
|
|
||||||
# 1D DP array
|
|
||||||
dp = [0.0] * (W + 1)
|
dp = [0.0] * (W + 1)
|
||||||
keep = [[False] * (W + 1) for _ in range(n)] # track selection choices
|
keep = [[False] * (W + 1) for _ in range(n)]
|
||||||
|
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
cost, value = costs[i], values[i]
|
cost, value = (costs[i], values[i])
|
||||||
for w in range(W, cost - 1, -1):
|
for w in range(W, cost - 1, -1):
|
||||||
if dp[w - cost] + value >= dp[w]: # <= FIXED HERE
|
if dp[w - cost] + value >= dp[w]:
|
||||||
dp[w] = dp[w - cost] + value
|
dp[w] = dp[w - cost] + value
|
||||||
keep[i][w] = True
|
keep[i][w] = True
|
||||||
|
selected = []
|
||||||
# Backtrack to find selected items
|
backtrack_excluded = []
|
||||||
selected, excluded = [], []
|
|
||||||
w = W
|
w = W
|
||||||
for i in range(n - 1, -1, -1):
|
for i in range(n - 1, -1, -1):
|
||||||
if keep[i][w]:
|
if keep[i][w]:
|
||||||
selected.append(equipments[i])
|
selected.append(eligible_items[i])
|
||||||
w -= costs[i]
|
w -= costs[i]
|
||||||
else:
|
else:
|
||||||
excluded.append(equipments[i])
|
backtrack_excluded.append(eligible_items[i])
|
||||||
|
excluded = backtrack_excluded + strictly_excluded
|
||||||
# Optional: fill leftover budget with zero-priority items
|
current_total_cost = sum((eq['total_cost'] for eq in selected))
|
||||||
remaining_budget = budget - sum(eq["total_cost"] for eq in selected)
|
remaining_budget = budget - current_total_cost
|
||||||
if remaining_budget > 0:
|
if remaining_budget > 0:
|
||||||
|
excluded.sort(key=lambda x: x['priority_score'], reverse=True)
|
||||||
for eq in excluded[:]:
|
for eq in excluded[:]:
|
||||||
if eq["total_cost"] <= remaining_budget:
|
if eq['total_cost'] <= remaining_budget:
|
||||||
selected.append(eq)
|
selected.append(eq)
|
||||||
excluded.remove(eq)
|
excluded.remove(eq)
|
||||||
remaining_budget -= eq["total_cost"]
|
remaining_budget -= eq['total_cost']
|
||||||
|
return (selected, excluded)
|
||||||
return selected, excluded
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,127 +1,45 @@
|
|||||||
import asyncio
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.calculation_target_reliability.utils import wait_for_workflow
|
|
||||||
from src.config import TEMPORAL_URL, TR_RBD_ID
|
from src.config import TEMPORAL_URL, TR_RBD_ID
|
||||||
from src.database.core import DbSession, CollectorDbSession
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
from src.auth.service import Token
|
from src.auth.service import Token
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
from .service import get_simulation_results, identify_worst_eaf_contributors
|
||||||
from .service import run_rbd_simulation, get_simulation_results, identify_worst_eaf_contributors
|
|
||||||
from .schema import OptimizationResult, TargetReliabiltiyQuery
|
from .schema import OptimizationResult, TargetReliabiltiyQuery
|
||||||
from src.auth.access_control import required_permission, OHPermission
|
from src.auth.access_control import required_permission, OHPermission
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[OptimizationResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
# @router.get("", response_model=StandardResponse[List[Dict]])
|
async def get_target_reliability(db_session: DbSession, token: Token, collector_db: CollectorDbSession, params: Annotated[TargetReliabiltiyQuery, Query()]):
|
||||||
# async def get_target_reliability(
|
|
||||||
# db_session: DbSession,
|
|
||||||
# scope_name: Optional[str] = Query(None),
|
|
||||||
# eaf_threshold: float = Query(100),
|
|
||||||
# ):
|
|
||||||
# """Get all scope pagination."""
|
|
||||||
# results = await get_all_target_reliability(
|
|
||||||
# db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold
|
|
||||||
# )
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# data=results,
|
|
||||||
# message="Data retrieved successfully",
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=StandardResponse[OptimizationResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_target_reliability(
|
|
||||||
db_session: DbSession,
|
|
||||||
token: Token,
|
|
||||||
collector_db: CollectorDbSession,
|
|
||||||
params: Annotated[TargetReliabiltiyQuery, Query()],
|
|
||||||
# oh_session_id: Optional[str] = Query(None),
|
|
||||||
# eaf_input: float = Query(99.8),
|
|
||||||
# duration: int = Query(17520),
|
|
||||||
# simulation_id: Optional[str] = Query(None),
|
|
||||||
# cut_hours = Query(0)
|
|
||||||
):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
oh_session_id = params.oh_session_id
|
oh_session_id = params.oh_session_id
|
||||||
eaf_input = params.eaf_input
|
eaf_input = params.eaf_input
|
||||||
duration = params.duration
|
duration = params.duration
|
||||||
simulation_id = params.simulation_id
|
simulation_id = params.simulation_id
|
||||||
cut_hours = params.cut_hours
|
cut_hours = params.cut_hours
|
||||||
|
|
||||||
if not oh_session_id:
|
if not oh_session_id:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='oh_session_id is required')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="oh_session_id is required",
|
|
||||||
)
|
|
||||||
|
|
||||||
# results = await get_eaf_timeline(
|
|
||||||
# db_session=db_session,
|
|
||||||
# oh_session_id=oh_session_id,
|
|
||||||
# eaf_input=eaf_input,
|
|
||||||
# oh_duration=duration
|
|
||||||
# )
|
|
||||||
|
|
||||||
if duration != 17520:
|
if duration != 17520:
|
||||||
if not simulation_id:
|
if not simulation_id:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Simulation ID is required for non-default duration. Please run simulation first.')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
try:
|
||||||
detail="Simulation ID is required for non-default duration. Please run simulation first.",
|
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||||
)
|
handle = temporal_client.get_workflow_handle(f'simulation-{simulation_id}')
|
||||||
else:
|
desc = await handle.describe()
|
||||||
try:
|
status_name = desc.status.name
|
||||||
temporal_client = await Client.connect(TEMPORAL_URL)
|
if status_name in ['RUNNING', 'CONTINUED_AS_NEW']:
|
||||||
handle = temporal_client.get_workflow_handle(f"simulation-{simulation_id}")
|
raise HTTPException(status_code=status.HTTP_425_TOO_EARLY, detail='Simulation is still running.')
|
||||||
desc = await handle.describe()
|
if status_name != 'COMPLETED':
|
||||||
status_name = desc.status.name
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f'Simulation failed with status: {status_name}')
|
||||||
|
except HTTPException:
|
||||||
if status_name in ["RUNNING", "CONTINUED_AS_NEW"]:
|
raise
|
||||||
raise HTTPException(
|
except Exception as e:
|
||||||
status_code=status.HTTP_425_TOO_EARLY,
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'Simulation not found or error checking status: {str(e)}')
|
||||||
detail="Simulation is still running.",
|
|
||||||
)
|
|
||||||
elif status_name != "COMPLETED":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=f"Simulation failed with status: {status_name}",
|
|
||||||
)
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
# Handle connection errors or invalid workflow IDs
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Simulation not found or error checking status: {str(e)}",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
simulation_id = TR_RBD_ID
|
simulation_id = TR_RBD_ID
|
||||||
|
results = await get_simulation_results(simulation_id=simulation_id, token=token)
|
||||||
|
optimize_result = await identify_worst_eaf_contributors(simulation_result=results, target_eaf=eaf_input, db_session=db_session, oh_session_id=oh_session_id, collector_db=collector_db, simulation_id=simulation_id, duration=duration, po_duration=1200, cut_hours=float(cut_hours))
|
||||||
results = await get_simulation_results(
|
return StandardResponse(data=optimize_result, message='Data retrieved successfully')
|
||||||
simulation_id=simulation_id,
|
|
||||||
token=token
|
|
||||||
)
|
|
||||||
|
|
||||||
optimize_result = await identify_worst_eaf_contributors(
|
|
||||||
simulation_result=results,
|
|
||||||
target_eaf=eaf_input,
|
|
||||||
db_session=db_session,
|
|
||||||
oh_session_id=oh_session_id,
|
|
||||||
collector_db=collector_db,
|
|
||||||
simulation_id=simulation_id,
|
|
||||||
duration=duration,
|
|
||||||
po_duration=1200,
|
|
||||||
cut_hours=float(cut_hours)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=optimize_result,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,91 +1,49 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import random
|
import random
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
|
from src.config import TEMPORAL_URL
|
||||||
|
|
||||||
from src.config import TEMPORAL_URL, TR_RBD_ID
|
def generate_down_periods(start_date: datetime, end_date: datetime, num_periods: Optional[int]=None, min_duration: int=3, max_duration: int=7) -> list[tuple[datetime, datetime]]:
|
||||||
|
|
||||||
def generate_down_periods(start_date: datetime, end_date: datetime,
|
|
||||||
num_periods: Optional[int] = None, min_duration: int = 3,
|
|
||||||
max_duration: int = 7) -> list[tuple[datetime, datetime]]:
|
|
||||||
"""
|
|
||||||
Generate random system down periods within a date range.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_date (datetime): Start date of the overall period
|
|
||||||
end_date (datetime): End date of the overall period
|
|
||||||
num_periods (int, optional): Number of down periods to generate.
|
|
||||||
If None, generates 1-3 periods randomly
|
|
||||||
min_duration (int): Minimum duration of each down period in days
|
|
||||||
max_duration (int): Maximum duration of each down period in days
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[tuple[datetime, datetime]]: List of (start_date, end_date) tuples
|
|
||||||
for each down period
|
|
||||||
"""
|
|
||||||
if num_periods is None:
|
if num_periods is None:
|
||||||
num_periods = random.randint(1, 3)
|
num_periods = random.randint(1, 3)
|
||||||
|
|
||||||
total_days = (end_date - start_date).days
|
total_days = (end_date - start_date).days
|
||||||
down_periods = []
|
down_periods = []
|
||||||
|
|
||||||
# Generate random down periods
|
|
||||||
for _ in range(num_periods):
|
for _ in range(num_periods):
|
||||||
# Random duration for this period
|
|
||||||
duration = random.randint(min_duration, max_duration)
|
duration = random.randint(min_duration, max_duration)
|
||||||
|
|
||||||
# Ensure we don't exceed the total date range
|
|
||||||
latest_possible_start = total_days - duration
|
latest_possible_start = total_days - duration
|
||||||
|
|
||||||
if latest_possible_start < 0:
|
if latest_possible_start < 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Random start day within available range
|
|
||||||
start_day = random.randint(0, latest_possible_start)
|
start_day = random.randint(0, latest_possible_start)
|
||||||
period_start = start_date + timedelta(days=start_day)
|
period_start = start_date + timedelta(days=start_day)
|
||||||
period_end = period_start + timedelta(days=duration)
|
period_end = period_start + timedelta(days=duration)
|
||||||
|
overlaps = any((p_start <= period_end and period_start <= p_end for p_start, p_end in down_periods))
|
||||||
# Check for overlaps with existing periods
|
|
||||||
overlaps = any(
|
|
||||||
(p_start <= period_end and period_start <= p_end)
|
|
||||||
for p_start, p_end in down_periods
|
|
||||||
)
|
|
||||||
|
|
||||||
if not overlaps:
|
if not overlaps:
|
||||||
down_periods.append((period_start, period_end))
|
down_periods.append((period_start, period_end))
|
||||||
|
|
||||||
return sorted(down_periods)
|
return sorted(down_periods)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_workflow(simulation_id, max_retries=3):
|
async def wait_for_workflow(simulation_id, max_retries=3):
|
||||||
workflow_id = f"simulation-{simulation_id}" # use returned ID
|
workflow_id = f'simulation-{simulation_id}'
|
||||||
retries = 0
|
retries = 0
|
||||||
temporal_client = await Client.connect(TEMPORAL_URL)
|
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
handle = temporal_client.get_workflow_handle(workflow_id=workflow_id)
|
handle = temporal_client.get_workflow_handle(workflow_id=workflow_id)
|
||||||
desc = await handle.describe()
|
desc = await handle.describe()
|
||||||
status = desc.status.name
|
status = desc.status.name
|
||||||
|
if status not in ['RUNNING', 'CONTINUED_AS_NEW']:
|
||||||
if status not in ["RUNNING", "CONTINUED_AS_NEW"]:
|
print(f'✅ Workflow {workflow_id} finished with status: {status}')
|
||||||
print(f"✅ Workflow {workflow_id} finished with status: {status}")
|
|
||||||
break
|
break
|
||||||
|
print(f'⏳ Workflow {workflow_id} still {status}, checking again in 10s...')
|
||||||
print(f"⏳ Workflow {workflow_id} still {status}, checking again in 10s...")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
retries += 1
|
retries += 1
|
||||||
if retries > max_retries:
|
if retries > max_retries:
|
||||||
print(f"⚠️ Workflow {workflow_id} not found after {max_retries} retries, treating as done. Error: {e}")
|
print(f'⚠️ Workflow {workflow_id} not found after {max_retries} retries, treating as done. Error: {e}')
|
||||||
break
|
break
|
||||||
else:
|
print(f'⚠️ Workflow {workflow_id} not found (retry {retries}/{max_retries}), waiting 10s before retry...')
|
||||||
print(f"⚠️ Workflow {workflow_id} not found (retry {retries}/{max_retries}), waiting 10s before retry...")
|
await asyncio.sleep(10)
|
||||||
await asyncio.sleep(10)
|
continue
|
||||||
continue
|
retries = 0
|
||||||
|
|
||||||
retries = 0 # reset retries if describe() worked
|
|
||||||
await asyncio.sleep(30)
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
return simulation_id
|
return simulation_id
|
||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,198 +1,61 @@
|
|||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, List, Optional, Union
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
import requests
|
|
||||||
|
|
||||||
from src import config
|
|
||||||
from src.auth.service import CurrentUser, InternalKey, Token
|
from src.auth.service import CurrentUser, InternalKey, Token
|
||||||
from src.config import DEFAULT_TC_ID
|
from src.config import DEFAULT_TC_ID
|
||||||
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, get_create_calculation_parameters, get_or_create_scope_equipment_calculation
|
||||||
from .flows import (create_calculation, get_create_calculation_parameters,
|
from .schema import CalculationResultsRead, CalculationSelectedEquipmentUpdate, CalculationTimeConstrainsCreate, CalculationTimeConstrainsParametersCreate, CalculationTimeConstrainsParametersRead, CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult
|
||||||
get_or_create_scope_equipment_calculation)
|
from .service import bulk_update_equipment, get_calculation_result, get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations
|
||||||
from .schema import (CalculationResultsRead,
|
|
||||||
CalculationSelectedEquipmentUpdate,
|
|
||||||
CalculationTimeConstrainsCreate,
|
|
||||||
CalculationTimeConstrainsParametersCreate,
|
|
||||||
CalculationTimeConstrainsParametersRead,
|
|
||||||
CalculationTimeConstrainsParametersRetrive,
|
|
||||||
CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult)
|
|
||||||
from .service import (bulk_update_equipment, get_calculation_result,
|
|
||||||
get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations)
|
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.auth.access_control import required_permission, OHPermission
|
from src.auth.access_control import required_permission, OHPermission
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
get_calculation = APIRouter()
|
get_calculation = APIRouter()
|
||||||
|
|
||||||
|
@router.post('', response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
||||||
@router.post(
|
async def create_calculation_time_constrains(token: Token, db_session: DbSession, collector_db_session: CollectorDbSession, current_user: CurrentUser, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, params: Annotated[CreateCalculationQuery, Query()]):
|
||||||
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def create_calculation_time_constrains(
|
|
||||||
token: Token,
|
|
||||||
db_session: DbSession,
|
|
||||||
collector_db_session: CollectorDbSession,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
|
||||||
params: Annotated[CreateCalculationQuery, Query()],
|
|
||||||
# scope_calculation_id: Optional[str] = Query(None),
|
|
||||||
# with_results: Optional[int] = Query(0),
|
|
||||||
# simulation_id = Query(None)
|
|
||||||
):
|
|
||||||
"""Save calculation time constrains Here"""
|
|
||||||
scope_calculation_id = params.scope_calculation_id
|
scope_calculation_id = params.scope_calculation_id
|
||||||
with_results = params.with_results
|
|
||||||
simulation_id = params.simulation_id
|
simulation_id = params.simulation_id
|
||||||
|
|
||||||
if scope_calculation_id:
|
if scope_calculation_id:
|
||||||
results = await get_or_create_scope_equipment_calculation(
|
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)
|
||||||
db_session=db_session,
|
|
||||||
scope_calculation_id=scope_calculation_id,
|
|
||||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
results = await create_calculation(
|
results = await create_calculation(token=token, db_session=db_session, collector_db_session=collector_db_session, calculation_time_constrains_in=calculation_time_constrains_in, created_by=current_user.name, simulation_id=simulation_id)
|
||||||
token=token,
|
return StandardResponse(data=results, message='Data created successfully')
|
||||||
db_session=db_session,
|
|
||||||
collector_db_session=collector_db_session,
|
|
||||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
|
||||||
created_by=current_user.name,
|
|
||||||
simulation_id=simulation_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=results, message="Data created successfully")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_all_simulation_calculations(
|
|
||||||
db_session: DbSession,
|
|
||||||
token: Token,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
):
|
|
||||||
"""Get all calculation time constrains Here"""
|
|
||||||
|
|
||||||
calculations = await get_all_calculations(
|
|
||||||
db_session=db_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=calculations,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('', response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
"/parameters",
|
async def get_all_simulation_calculations(db_session: DbSession, token: Token, current_user: CurrentUser):
|
||||||
response_model=StandardResponse[
|
calculations = await get_all_calculations(db_session=db_session)
|
||||||
Union[
|
return StandardResponse(data=calculations, message='Data retrieved successfully')
|
||||||
CalculationTimeConstrainsParametersRetrive,
|
|
||||||
CalculationTimeConstrainsParametersRead,
|
|
||||||
]
|
|
||||||
],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_calculation_parameters(
|
|
||||||
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
|
|
||||||
):
|
|
||||||
"""Get all calculation parameter."""
|
|
||||||
|
|
||||||
parameters = await get_create_calculation_parameters(
|
@router.get('/parameters', response_model=StandardResponse[Union[CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsParametersRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
db_session=db_session, calculation_id=calculation_id
|
async def get_calculation_parameters(db_session: DbSession, calculation_id: Optional[str]=Query(default=None)):
|
||||||
)
|
parameters = await get_create_calculation_parameters(db_session=db_session, calculation_id=calculation_id)
|
||||||
|
return StandardResponse(data=parameters, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@get_calculation.get('/{calculation_id}', response_model=StandardResponse[CalculationTimeConstrainsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
data=parameters,
|
async def get_calculation_results(db_session: DbSession, calculation_id, token: InternalKey, include_risk_cost: int=Query(1, alias='risk_cost')):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@get_calculation.get(
|
|
||||||
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_calculation_results(db_session: DbSession, calculation_id, token:InternalKey, include_risk_cost:int = Query(1, alias="risk_cost")):
|
|
||||||
if calculation_id == 'default':
|
if calculation_id == 'default':
|
||||||
calculation_id = DEFAULT_TC_ID
|
calculation_id = DEFAULT_TC_ID
|
||||||
|
results = await get_calculation_result(db_session=db_session, calculation_id=calculation_id, token=token, include_risk_cost=include_risk_cost)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
results = await get_calculation_result(
|
@router.get('/{calculation_id}/{assetnum}', response_model=StandardResponse[EquipmentResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
db_session=db_session, calculation_id=calculation_id, token=token, include_risk_cost=include_risk_cost
|
|
||||||
)
|
|
||||||
|
|
||||||
# requests.post(f"{config.AUTH_SERVICE_API}/sign-out", headers={
|
|
||||||
# "Authorization": f"Bearer {token}"
|
|
||||||
# })
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
||||||
|
results = await get_calculation_by_assetnum(db_session=db_session, assetnum=assetnum, calculation_id=calculation_id)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
results = await get_calculation_by_assetnum(
|
@router.post('/{calculation_id}/simulation', response_model=StandardResponse[CalculationResultsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
db_session=db_session, assetnum=assetnum, calculation_id=calculation_id
|
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=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/update/{calculation_id}', response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope)), Depends(csrf_protect)])
|
||||||
@router.post(
|
async def update_selected_equipment(db_session: DbSession, calculation_id, calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate]):
|
||||||
"/{calculation_id}/simulation",
|
|
||||||
response_model=StandardResponse[CalculationResultsRead],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope)), Depends(csrf_protect)])
|
|
||||||
async def update_selected_equipment(
|
|
||||||
db_session: DbSession,
|
|
||||||
calculation_id,
|
|
||||||
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
|
|
||||||
):
|
|
||||||
if calculation_id == 'default':
|
if calculation_id == 'default':
|
||||||
calculation_id = "3b9a73a2-bde6-418c-9e2f-19046f501a05"
|
calculation_id = '3b9a73a2-bde6-418c-9e2f-19046f501a05'
|
||||||
|
results = await bulk_update_equipment(db=db_session, selected_equipments=calculation_time_constrains_in, calculation_data_id=calculation_id)
|
||||||
results = await bulk_update_equipment(
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
db=db_session,
|
|
||||||
selected_equipments=calculation_time_constrains_in,
|
|
||||||
calculation_data_id=calculation_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,302 +1,134 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from src.config import RBD_SERVICE_API
|
from src.config import RBD_SERVICE_API
|
||||||
|
|
||||||
def get_months_between(start_date: datetime.datetime, end_date: datetime.datetime) -> int:
|
def get_months_between(start_date: datetime.datetime, end_date: datetime.datetime) -> int:
|
||||||
"""
|
return (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
||||||
Calculate number of months between two dates.
|
|
||||||
"""
|
|
||||||
months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
|
||||||
# Add 1 to include both start and end months
|
|
||||||
return months
|
|
||||||
|
|
||||||
|
|
||||||
def create_time_series_data(chart_data, max_hours=None):
|
def create_time_series_data(chart_data, max_hours=None):
|
||||||
# Filter out ON_OH
|
filtered_data = [d for d in chart_data if d['currentEvent'] != 'ON_OH']
|
||||||
filtered_data = [d for d in chart_data if d["currentEvent"] != "ON_OH"]
|
sorted_data = sorted(filtered_data, key=lambda x: x['cumulativeTime'])
|
||||||
sorted_data = sorted(filtered_data, key=lambda x: x["cumulativeTime"])
|
|
||||||
|
|
||||||
if not sorted_data:
|
if not sorted_data:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
hourly_data = []
|
hourly_data = []
|
||||||
current_state_index = 0
|
current_state_index = 0
|
||||||
current_flow_rate = sorted_data[0]["flowRate"]
|
current_flow_rate = sorted_data[0]['flowRate']
|
||||||
current_eq_status = sorted_data[0]["currentEQStatus"]
|
current_eq_status = sorted_data[0]['currentEQStatus']
|
||||||
|
last_time = int(sorted_data[-1]['cumulativeTime'])
|
||||||
# Determine maximum bound (either given or from data)
|
|
||||||
last_time = int(sorted_data[-1]["cumulativeTime"])
|
|
||||||
if max_hours is None:
|
if max_hours is None:
|
||||||
max_hours = last_time
|
max_hours = last_time
|
||||||
|
for hour in range(0, max_hours + 1):
|
||||||
for hour in range(0, max_hours + 1): # start from 0
|
while current_state_index < len(sorted_data) - 1 and hour >= sorted_data[current_state_index + 1]['cumulativeTime']:
|
||||||
# Advance state if needed
|
|
||||||
while (current_state_index < len(sorted_data) - 1 and
|
|
||||||
hour >= sorted_data[current_state_index + 1]["cumulativeTime"]):
|
|
||||||
current_state_index += 1
|
current_state_index += 1
|
||||||
current_flow_rate = sorted_data[current_state_index]["flowRate"]
|
current_flow_rate = sorted_data[current_state_index]['flowRate']
|
||||||
current_eq_status = sorted_data[current_state_index]["currentEQStatus"]
|
current_eq_status = sorted_data[current_state_index]['currentEQStatus']
|
||||||
|
hourly_data.append({'cumulativeTime': hour, 'flowRate': current_flow_rate, 'currentEQStatus': current_eq_status})
|
||||||
hourly_data.append({
|
|
||||||
"cumulativeTime": hour,
|
|
||||||
"flowRate": current_flow_rate,
|
|
||||||
"currentEQStatus": current_eq_status
|
|
||||||
})
|
|
||||||
|
|
||||||
return hourly_data
|
return hourly_data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_failures_per_month(hourly_data):
|
def calculate_failures_per_month(hourly_data):
|
||||||
"""
|
|
||||||
Calculate the cumulative number of failures up to each month from hourly data.
|
|
||||||
A failure is defined as when currentEQStatus = "OoS".
|
|
||||||
Only counts the start of each failure period (transition from "Svc" to "OoS").
|
|
||||||
|
|
||||||
Args:
|
|
||||||
hourly_data: List of dicts with 'hour', 'flowrate', and 'currentEQStatus' keys
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of dicts with 'month' and 'failures' keys (cumulative count)
|
|
||||||
"""
|
|
||||||
total_failures = 0
|
total_failures = 0
|
||||||
previous_eq_status = None
|
previous_eq_status = None
|
||||||
monthly_data = {}
|
monthly_data = {}
|
||||||
|
|
||||||
for data_point in hourly_data:
|
for data_point in hourly_data:
|
||||||
hour = data_point['hour']
|
hour = data_point['hour']
|
||||||
current_eq_status = data_point['currentEQStatus']
|
current_eq_status = data_point['currentEQStatus']
|
||||||
|
month = (hour - 1) // 720 + 1
|
||||||
# Calculate which month this hour belongs to (1-based)
|
if current_eq_status == 'OoS' and previous_eq_status is not None and (previous_eq_status != 'OoS'):
|
||||||
# Assuming 30 days per month = 720 hours per month
|
|
||||||
month = ((hour - 1) // 720) + 1
|
|
||||||
|
|
||||||
# Check if this is the start of a failure (transition to "OoS")
|
|
||||||
if current_eq_status == "OoS" and previous_eq_status is not None and previous_eq_status != "OoS":
|
|
||||||
total_failures += 1
|
total_failures += 1
|
||||||
# Special case: if the very first data point is a failure
|
elif current_eq_status == 'OoS' and previous_eq_status is None:
|
||||||
elif current_eq_status == "OoS" and previous_eq_status is None:
|
|
||||||
total_failures += 1
|
total_failures += 1
|
||||||
|
|
||||||
# Store the current cumulative count for this month
|
|
||||||
monthly_data[month] = total_failures
|
monthly_data[month] = total_failures
|
||||||
previous_eq_status = current_eq_status
|
previous_eq_status = current_eq_status
|
||||||
|
|
||||||
# Convert to list format
|
|
||||||
result = []
|
result = []
|
||||||
if monthly_data:
|
if monthly_data:
|
||||||
max_month = max(monthly_data.keys())
|
max_month = max(monthly_data.keys())
|
||||||
for month in range(1, max_month + 1):
|
for month in range(1, max_month + 1):
|
||||||
result.append({
|
result.append({'month': month, 'failures': monthly_data.get(month, monthly_data.get(month - 1, 0))})
|
||||||
'month': month,
|
|
||||||
'failures': monthly_data.get(month, monthly_data.get(month-1, 0))
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
async def plant_simulation_metrics(simulation_id: str, location_tag: str, max_interval, token, last_oh_date, use_location_tag: int = 1):
|
async def plant_simulation_metrics(simulation_id: str, location_tag: str, max_interval, token, last_oh_date, use_location_tag: int=1):
|
||||||
"""Get failure predictions for equipment from simulation service"""
|
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}'
|
||||||
calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}"
|
try:
|
||||||
|
response = requests.get(calc_result_url, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'}, timeout=30)
|
||||||
try:
|
response.raise_for_status()
|
||||||
response = requests.get(
|
prediction_data = response.json()['data']
|
||||||
calc_result_url,
|
except (requests.RequestException, ValueError) as e:
|
||||||
headers={
|
raise Exception(str(e))
|
||||||
"Content-Type": "application/json",
|
return prediction_data
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
},
|
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float=550):
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
prediction_data = response.json()['data']
|
|
||||||
except (requests.RequestException, ValueError) as e:
|
|
||||||
raise Exception(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
return prediction_data
|
|
||||||
|
|
||||||
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float = 550):
|
|
||||||
if not timestamp_outs:
|
if not timestamp_outs:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
df = pd.DataFrame(timestamp_outs)
|
df = pd.DataFrame(timestamp_outs)
|
||||||
required_columns = ['cumulativeTime', 'currentEQStatus', 'flowRate']
|
required_columns = ['cumulativeTime', 'currentEQStatus', 'flowRate']
|
||||||
if not all(col in df.columns for col in required_columns):
|
if not all((col in df.columns for col in required_columns)):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
start_oh = datetime.datetime(start_date.year, start_date.month, start_date.day)
|
start_oh = datetime.datetime(start_date.year, start_date.month, start_date.day)
|
||||||
|
|
||||||
# Actual datetime from cumulative hours
|
|
||||||
df['datetime'] = df['cumulativeTime'].apply(lambda x: start_oh + datetime.timedelta(hours=x))
|
df['datetime'] = df['cumulativeTime'].apply(lambda x: start_oh + datetime.timedelta(hours=x))
|
||||||
df['month_year'] = df['datetime'].dt.to_period('M')
|
df['month_year'] = df['datetime'].dt.to_period('M')
|
||||||
|
|
||||||
# Duration until next timestamp
|
|
||||||
df['duration_hours'] = df['cumulativeTime'].shift(-1) - df['cumulativeTime']
|
df['duration_hours'] = df['cumulativeTime'].shift(-1) - df['cumulativeTime']
|
||||||
df['duration_hours'] = df['duration_hours'].fillna(0)
|
df['duration_hours'] = df['duration_hours'].fillna(0)
|
||||||
|
|
||||||
# Failure detection
|
|
||||||
df['status_change'] = df['currentEQStatus'].shift() != df['currentEQStatus']
|
df['status_change'] = df['currentEQStatus'].shift() != df['currentEQStatus']
|
||||||
df['failure'] = (df['currentEQStatus'] == 'OoS') & df['status_change']
|
df['failure'] = (df['currentEQStatus'] == 'OoS') & df['status_change']
|
||||||
|
|
||||||
# Cumulative tracking
|
|
||||||
df['cumulative_failures'] = df['failure'].cumsum()
|
df['cumulative_failures'] = df['failure'].cumsum()
|
||||||
df['cumulative_oos'] = (df['duration_hours'] * (df['currentEQStatus'] == 'OoS')).cumsum()
|
df['cumulative_oos'] = (df['duration_hours'] * (df['currentEQStatus'] == 'OoS')).cumsum()
|
||||||
|
|
||||||
# Derating calculation
|
|
||||||
# Derating = capacity reduction below max but not outage
|
|
||||||
df['derating'] = (max_flow_rate - df['flowRate']).clip(lower=0)
|
df['derating'] = (max_flow_rate - df['flowRate']).clip(lower=0)
|
||||||
df['is_derated'] = (df['currentEQStatus'] == 'Svc') & (df['derating'] > 0)
|
df['is_derated'] = (df['currentEQStatus'] == 'Svc') & (df['derating'] > 0)
|
||||||
|
|
||||||
# Equivalent Derated Hours (EFDH) → sum of derating * hours, then normalized by max capacity
|
|
||||||
df['derated_mwh'] = df['derating'] * df['duration_hours']
|
df['derated_mwh'] = df['derating'] * df['duration_hours']
|
||||||
df['derated_hours_equivalent'] = df['derated_mwh'] / max_flow_rate
|
df['derated_hours_equivalent'] = df['derated_mwh'] / max_flow_rate
|
||||||
|
|
||||||
monthly_results = {}
|
monthly_results = {}
|
||||||
|
|
||||||
for month_period, group in df.groupby('month_year', sort=True):
|
for month_period, group in df.groupby('month_year', sort=True):
|
||||||
month_str = str(month_period)
|
month_str = str(month_period)
|
||||||
monthly_results[month_str] = {}
|
monthly_results[month_str] = {}
|
||||||
|
|
||||||
# Failures
|
|
||||||
monthly_results[month_str]['failures_count'] = int(group['failure'].sum())
|
monthly_results[month_str]['failures_count'] = int(group['failure'].sum())
|
||||||
monthly_results[month_str]['cumulative_failures'] = int(group['cumulative_failures'].max())
|
monthly_results[month_str]['cumulative_failures'] = int(group['cumulative_failures'].max())
|
||||||
|
|
||||||
# OOS hours
|
|
||||||
oos_time = group.loc[group['currentEQStatus'] == 'OoS', 'duration_hours'].sum()
|
oos_time = group.loc[group['currentEQStatus'] == 'OoS', 'duration_hours'].sum()
|
||||||
monthly_results[month_str]['total_oos_hours'] = float(oos_time)
|
monthly_results[month_str]['total_oos_hours'] = float(oos_time)
|
||||||
monthly_results[month_str]['cummulative_oos'] = float(group['cumulative_oos'].max())
|
monthly_results[month_str]['cummulative_oos'] = float(group['cumulative_oos'].max())
|
||||||
|
|
||||||
# Flow rate (weighted average)
|
|
||||||
total_flow_time = (group['flowRate'] * group['duration_hours']).sum()
|
total_flow_time = (group['flowRate'] * group['duration_hours']).sum()
|
||||||
total_time = group['duration_hours'].sum()
|
total_time = group['duration_hours'].sum()
|
||||||
avg_flow_rate = total_flow_time / total_time if total_time > 0 else 0
|
avg_flow_rate = total_flow_time / total_time if total_time > 0 else 0
|
||||||
monthly_results[month_str]['avg_flow_rate'] = float(avg_flow_rate)
|
monthly_results[month_str]['avg_flow_rate'] = float(avg_flow_rate)
|
||||||
|
|
||||||
# Extra metrics
|
|
||||||
monthly_results[month_str]['total_hours'] = float(total_time)
|
monthly_results[month_str]['total_hours'] = float(total_time)
|
||||||
service_hours = group.loc[group['currentEQStatus'] == 'Svc', 'duration_hours'].sum()
|
service_hours = group.loc[group['currentEQStatus'] == 'Svc', 'duration_hours'].sum()
|
||||||
monthly_results[month_str]['service_hours'] = float(service_hours)
|
monthly_results[month_str]['service_hours'] = float(service_hours)
|
||||||
monthly_results[month_str]['availability_percentage'] = float(
|
monthly_results[month_str]['availability_percentage'] = float(service_hours / total_time * 100 if total_time > 0 else 0)
|
||||||
(service_hours / total_time * 100) if total_time > 0 else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Derating metrics
|
|
||||||
derating_hours = group.loc[group['is_derated'], 'duration_hours'].sum()
|
derating_hours = group.loc[group['is_derated'], 'duration_hours'].sum()
|
||||||
derated_mwh = group['derated_mwh'].sum()
|
derated_mwh = group['derated_mwh'].sum()
|
||||||
equivalent_derated_hours = group['derated_hours_equivalent'].sum()
|
equivalent_derated_hours = group['derated_hours_equivalent'].sum()
|
||||||
|
|
||||||
monthly_results[month_str]['derating_hours'] = float(derating_hours)
|
monthly_results[month_str]['derating_hours'] = float(derating_hours)
|
||||||
monthly_results[month_str]['derated_mwh'] = float(derated_mwh)
|
monthly_results[month_str]['derated_mwh'] = float(derated_mwh)
|
||||||
monthly_results[month_str]['equivalent_derated_hours'] = float(equivalent_derated_hours)
|
monthly_results[month_str]['equivalent_derated_hours'] = float(equivalent_derated_hours)
|
||||||
|
|
||||||
return monthly_results
|
return monthly_results
|
||||||
|
|
||||||
|
|
||||||
def calculate_risk_cost_per_failure(monthly_results, birnbaum_importance, energy_price):
|
def calculate_risk_cost_per_failure(monthly_results, birnbaum_importance, energy_price):
|
||||||
"""
|
|
||||||
Calculate risk cost per failure for each month based on:
|
|
||||||
1. Equipment capacity contribution to system (flowrate * birnbaum_importance * availability)
|
|
||||||
2. Capacity lost to downtime per month
|
|
||||||
3. Energy price
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- monthly_results: Output from analyze_monthly_metrics()
|
|
||||||
- birnbaum_importance: Birnbaum importance factor for this equipment
|
|
||||||
- energy_price: Price per unit of energy/flow
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- Dictionary with monthly risk costs and array of risk costs per failure
|
|
||||||
"""
|
|
||||||
|
|
||||||
risk_costs = {}
|
risk_costs = {}
|
||||||
risk_cost_array = []
|
risk_cost_array = []
|
||||||
|
|
||||||
for month, data in monthly_results.items():
|
for month, data in monthly_results.items():
|
||||||
# Extract monthly data
|
|
||||||
avg_flow_rate = data['avg_flow_rate']
|
avg_flow_rate = data['avg_flow_rate']
|
||||||
availability = data['availability_percentage'] / 100 # Convert to decimal
|
availability = data['availability_percentage'] / 100
|
||||||
total_oos_hours = data['total_oos_hours']
|
total_oos_hours = data['total_oos_hours']
|
||||||
failures_count = data['failures_count']
|
failures_count = data['failures_count']
|
||||||
|
|
||||||
# 1. Calculate equipment capacity contribution to system
|
|
||||||
# Capacity = avg_flowrate * birnbaum_importance * availability
|
|
||||||
equipment_capacity = avg_flow_rate * birnbaum_importance * availability
|
equipment_capacity = avg_flow_rate * birnbaum_importance * availability
|
||||||
|
|
||||||
# 2. Calculate capacity lost to downtime per month
|
|
||||||
# Lost capacity = avg_flowrate * birnbaum_importance * downtime_hours
|
|
||||||
capacity_lost_to_downtime = avg_flow_rate * birnbaum_importance * total_oos_hours
|
capacity_lost_to_downtime = avg_flow_rate * birnbaum_importance * total_oos_hours
|
||||||
|
|
||||||
# 3. Calculate total risk cost for the month
|
|
||||||
# Risk cost = capacity_lost * energy_price
|
|
||||||
monthly_risk_cost = capacity_lost_to_downtime * energy_price
|
monthly_risk_cost = capacity_lost_to_downtime * energy_price
|
||||||
|
|
||||||
# 4. Calculate risk cost per failure for this month
|
|
||||||
if failures_count > 0:
|
if failures_count > 0:
|
||||||
risk_cost_per_failure = monthly_risk_cost / failures_count
|
risk_cost_per_failure = monthly_risk_cost / failures_count
|
||||||
else:
|
else:
|
||||||
# If no failures, set to 0 or use alternative approach
|
|
||||||
risk_cost_per_failure = 0
|
risk_cost_per_failure = 0
|
||||||
|
risk_costs[month] = {'equipment_capacity': equipment_capacity, 'capacity_lost_to_downtime': capacity_lost_to_downtime, 'monthly_risk_cost': monthly_risk_cost, 'failures_count': failures_count, 'risk_cost_per_failure': risk_cost_per_failure}
|
||||||
# Store results
|
|
||||||
risk_costs[month] = {
|
|
||||||
'equipment_capacity': equipment_capacity,
|
|
||||||
'capacity_lost_to_downtime': capacity_lost_to_downtime,
|
|
||||||
'monthly_risk_cost': monthly_risk_cost,
|
|
||||||
'failures_count': failures_count,
|
|
||||||
'risk_cost_per_failure': risk_cost_per_failure
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add to array
|
|
||||||
risk_cost_array.append(risk_cost_per_failure)
|
risk_cost_array.append(risk_cost_per_failure)
|
||||||
|
return {'monthly_details': risk_costs, 'risk_cost_per_failure_array': risk_cost_array}
|
||||||
|
|
||||||
return {
|
|
||||||
'monthly_details': risk_costs,
|
|
||||||
'risk_cost_per_failure_array': risk_cost_array
|
|
||||||
}
|
|
||||||
|
|
||||||
# Example usage:
|
|
||||||
def get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price):
|
def get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price):
|
||||||
"""
|
|
||||||
Complete analysis combining monthly metrics with risk cost calculation
|
|
||||||
"""
|
|
||||||
# Get monthly metrics
|
|
||||||
monthly_metrics = analyze_monthly_metrics(timestamp_outs)
|
monthly_metrics = analyze_monthly_metrics(timestamp_outs)
|
||||||
|
risk_analysis = calculate_risk_cost_per_failure(monthly_metrics, birnbaum_importance, energy_price)
|
||||||
# Calculate risk costs
|
|
||||||
risk_analysis = calculate_risk_cost_per_failure(
|
|
||||||
monthly_metrics,
|
|
||||||
birnbaum_importance,
|
|
||||||
energy_price
|
|
||||||
)
|
|
||||||
|
|
||||||
# Combine results for comprehensive view
|
|
||||||
combined_results = {}
|
combined_results = {}
|
||||||
for month in monthly_metrics.keys():
|
for month in monthly_metrics.keys():
|
||||||
combined_results[month] = {
|
combined_results[month] = {**monthly_metrics[month], **risk_analysis['monthly_details'][month]}
|
||||||
**monthly_metrics[month],
|
return {'monthly_data': combined_results, 'risk_cost_array': risk_analysis['risk_cost_per_failure_array']}
|
||||||
**risk_analysis['monthly_details'][month]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'monthly_data': combined_results,
|
|
||||||
'risk_cost_array': risk_analysis['risk_cost_per_failure_array']
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage example:
|
|
||||||
# birnbaum_importance = 0.85 # Example value
|
|
||||||
# energy_price = 100 # Example: $100 per unit
|
|
||||||
#
|
|
||||||
# results = get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price)
|
|
||||||
# risk_cost_array = results['risk_cost_array']
|
|
||||||
# print("Risk cost per failure each month:", risk_cost_array)
|
|
||||||
|
|||||||
@ -1,100 +1,64 @@
|
|||||||
import base64
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from starlette.config import Config
|
from starlette.config import Config
|
||||||
from starlette.datastructures import CommaSeparatedStrings
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseConfigurationModel(BaseModel):
|
class BaseConfigurationModel(BaseModel):
|
||||||
"""Base configuration model used by all config options."""
|
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_env_tags(tag_list: List[str]) -> dict:
|
def get_env_tags(tag_list: List[str]) -> dict:
|
||||||
"""Create dictionary of available env tags."""
|
|
||||||
tags = {}
|
tags = {}
|
||||||
for t in tag_list:
|
for t in tag_list:
|
||||||
tag_key, env_key = t.split(":")
|
tag_key, env_key = t.split(':')
|
||||||
|
|
||||||
env_value = os.environ.get(env_key)
|
env_value = os.environ.get(env_key)
|
||||||
|
|
||||||
if env_value:
|
if env_value:
|
||||||
tags.update({tag_key: env_value})
|
tags.update({tag_key: env_value})
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
|
||||||
def get_config():
|
def get_config():
|
||||||
try:
|
try:
|
||||||
# Try to load from .env file first
|
config = Config('.env')
|
||||||
config = Config(".env")
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# If .env doesn't exist, use environment variables
|
|
||||||
config = Config(environ=os.environ)
|
config = Config(environ=os.environ)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
LOG_LEVEL = config('LOG_LEVEL', default='INFO')
|
||||||
|
SERVICE_NAME = config('SERVICE_NAME', default='Be-OptimumOH')
|
||||||
LOG_LEVEL = config("LOG_LEVEL", default="INFO")
|
ENV = config('ENV')
|
||||||
SERVICE_NAME = config("SERVICE_NAME", default="Be-OptimumOH")
|
PORT = config('PORT', cast=int)
|
||||||
ENV = config("ENV")
|
HOST = config('HOST')
|
||||||
PORT = config("PORT", cast=int)
|
DATABASE_HOSTNAME = config('DATABASE_HOSTNAME')
|
||||||
HOST = config("HOST")
|
_DATABASE_CREDENTIAL_USER = config('DATABASE_CREDENTIAL_USER')
|
||||||
|
_DATABASE_CREDENTIAL_PASSWORD = config('DATABASE_CREDENTIAL_PASSWORD')
|
||||||
|
|
||||||
# database
|
|
||||||
DATABASE_HOSTNAME = config("DATABASE_HOSTNAME")
|
|
||||||
_DATABASE_CREDENTIAL_USER = config("DATABASE_CREDENTIAL_USER")
|
|
||||||
_DATABASE_CREDENTIAL_PASSWORD = config("DATABASE_CREDENTIAL_PASSWORD")
|
|
||||||
_QUOTED_DATABASE_PASSWORD = parse.quote(str(_DATABASE_CREDENTIAL_PASSWORD))
|
_QUOTED_DATABASE_PASSWORD = parse.quote(str(_DATABASE_CREDENTIAL_PASSWORD))
|
||||||
DATABASE_NAME = config("DATABASE_NAME")
|
DATABASE_NAME = config('DATABASE_NAME')
|
||||||
DATABASE_PORT = config("DATABASE_PORT")
|
DATABASE_PORT = config('DATABASE_PORT')
|
||||||
|
DATABASE_ENGINE_POOL_SIZE = config('DATABASE_ENGINE_POOL_SIZE', cast=int, default=20)
|
||||||
DATABASE_ENGINE_POOL_SIZE = config("DATABASE_ENGINE_POOL_SIZE", cast=int, default=20)
|
DATABASE_ENGINE_MAX_OVERFLOW = config('DATABASE_ENGINE_MAX_OVERFLOW', cast=int, default=0)
|
||||||
DATABASE_ENGINE_MAX_OVERFLOW = config(
|
COLLECTOR_HOSTNAME = config('COLLECTOR_HOSTNAME')
|
||||||
"DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0
|
COLLECTOR_PORT = config('COLLECTOR_PORT', cast=int)
|
||||||
)
|
COLLECTOR_CREDENTIAL_USER = config('COLLECTOR_CREDENTIAL_USER')
|
||||||
|
COLLECTOR_CREDENTIAL_PASSWORD = config('COLLECTOR_CREDENTIAL_PASSWORD')
|
||||||
COLLECTOR_HOSTNAME = config("COLLECTOR_HOSTNAME")
|
|
||||||
COLLECTOR_PORT = config("COLLECTOR_PORT", cast=int)
|
|
||||||
COLLECTOR_CREDENTIAL_USER = config("COLLECTOR_CREDENTIAL_USER")
|
|
||||||
COLLECTOR_CREDENTIAL_PASSWORD = config("COLLECTOR_CREDENTIAL_PASSWORD")
|
|
||||||
QUOTED_COLLECTOR_CREDENTIAL_PASSWORD = parse.quote(str(COLLECTOR_CREDENTIAL_PASSWORD))
|
QUOTED_COLLECTOR_CREDENTIAL_PASSWORD = parse.quote(str(COLLECTOR_CREDENTIAL_PASSWORD))
|
||||||
COLLECTOR_NAME = config("COLLECTOR_NAME")
|
COLLECTOR_NAME = config('COLLECTOR_NAME')
|
||||||
|
SQLALCHEMY_DATABASE_URI = f'postgresql+asyncpg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}'
|
||||||
# Deal w
|
SQLALCHEMY_COLLECTOR_URI = f'postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}'
|
||||||
SQLALCHEMY_DATABASE_URI = f"postgresql+asyncpg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}"
|
TIMEZONE = config('TIMEZONE')
|
||||||
SQLALCHEMY_COLLECTOR_URI = f"postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}"
|
MAXIMO_BASE_URL = config('MAXIMO_BASE_URL')
|
||||||
|
MAXIMO_API_KEY = config('MAXIMO_API_KEY')
|
||||||
|
AUTH_SERVICE_API = config('AUTH_SERVICE_API')
|
||||||
TIMEZONE = config("TIMEZONE")
|
REALIBILITY_SERVICE_API = config('REALIBILITY_SERVICE_API')
|
||||||
|
RBD_SERVICE_API = config('RBD_SERVICE_API')
|
||||||
MAXIMO_BASE_URL = config("MAXIMO_BASE_URL")
|
TEMPORAL_URL = config('TEMPORAL_URL')
|
||||||
MAXIMO_API_KEY = config("MAXIMO_API_KEY")
|
BASE_URL = config('BASE_URL')
|
||||||
|
API_KEY = config('API_KEY')
|
||||||
AUTH_SERVICE_API = config("AUTH_SERVICE_API")
|
TR_RBD_ID = config('TR_RBD_ID')
|
||||||
REALIBILITY_SERVICE_API = config("REALIBILITY_SERVICE_API")
|
TC_RBD_ID = config('TC_RBD_ID')
|
||||||
RBD_SERVICE_API = config("RBD_SERVICE_API")
|
DEFAULT_TC_ID = config('DEFAULT_TC_ID')
|
||||||
TEMPORAL_URL = config("TEMPORAL_URL")
|
REDIS_HOST = config('REDIS_HOST')
|
||||||
BASE_URL = config("BASE_URL")
|
REDIS_PORT = config('REDIS_PORT', cast=int)
|
||||||
|
RATELIMIT_STORAGE_URI = f'redis://{REDIS_HOST}:{REDIS_PORT}'
|
||||||
|
REDIS_AUTH_DB = config('REDIS_AUTH_DB', cast=int)
|
||||||
API_KEY = config("API_KEY")
|
|
||||||
|
|
||||||
TR_RBD_ID = config("TR_RBD_ID")
|
|
||||||
TC_RBD_ID = config("TC_RBD_ID")
|
|
||||||
DEFAULT_TC_ID = config("DEFAULT_TC_ID")
|
|
||||||
REDIS_HOST = config("REDIS_HOST")
|
|
||||||
REDIS_PORT = config("REDIS_PORT", cast=int)
|
|
||||||
RATELIMIT_STORAGE_URI = f"redis://{REDIS_HOST}:{REDIS_PORT}"
|
|
||||||
REDIS_AUTH_DB = config("REDIS_AUTH_DB", cast=int)
|
|
||||||
|
|||||||
@ -1,125 +1,36 @@
|
|||||||
"""
|
|
||||||
CSRF Token Verification Dependency for be-optimumoh (FastAPI).
|
|
||||||
|
|
||||||
Menggantikan CSRFProtectMiddleware (Double Submit Cookie) dengan verifikasi
|
|
||||||
berbasis Redis. Token CSRF di-generate oleh be-auth (CSRFTokenResource) dan
|
|
||||||
disimpan di Redis dalam bentuk SHA256 hash.
|
|
||||||
|
|
||||||
Token Flow (generation di be-auth, verification di sini):
|
|
||||||
────────────────────────────────────────
|
|
||||||
GENERATION (be-auth / CSRFTokenResource):
|
|
||||||
1. token = secrets.token_urlsafe(32)
|
|
||||||
2. token_hash = SHA256(token)
|
|
||||||
3. redis[csrf:{user_id}:{path_hash}] = token_hash (dengan TTL)
|
|
||||||
4. return plain token ke client
|
|
||||||
|
|
||||||
VERIFICATION (dependency ini):
|
|
||||||
1. Baca X-CSRF-Token header dari request
|
|
||||||
2. Ambil user_id dari request.state.user (di-set oleh JWTBearer)
|
|
||||||
3. incoming_hash = SHA256(nilai X-CSRF-Token)
|
|
||||||
4. stored_hash = redis_auth.get("csrf:{user_id}:{path_hash}")
|
|
||||||
5. secrets.compare_digest(incoming_hash, stored_hash)
|
|
||||||
6. redis_auth.delete(key) ← one-time use
|
|
||||||
────────────────────────────────────────
|
|
||||||
|
|
||||||
NOTE: Dependency ini harus selalu dikombinasikan dengan JWTBearer() agar
|
|
||||||
request.state.user sudah tersedia saat dijalankan.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from fastapi import Depends
|
|
||||||
from src.csrf_protect import csrf_protect
|
|
||||||
|
|
||||||
@router.post("/some-endpoint", dependencies=[Depends(csrf_protect)])
|
|
||||||
async def some_endpoint(...):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import redis
|
import redis
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
|
||||||
import src.config as config
|
import src.config as config
|
||||||
from src.context import get_user_id
|
from src.context import get_user_id
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
_redis_auth = redis.Redis(host=config.REDIS_HOST, port=int(config.REDIS_PORT), db=config.REDIS_AUTH_DB)
|
||||||
# Koneksi Redis untuk CSRF — Redis DB 0 tempat be-auth menyimpan CSRF token
|
|
||||||
_redis_auth = redis.Redis(
|
|
||||||
host=config.REDIS_HOST,
|
|
||||||
port=int(config.REDIS_PORT),
|
|
||||||
db=config.REDIS_AUTH_DB,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def csrf_protect(request: Request) -> None:
|
async def csrf_protect(request: Request) -> None:
|
||||||
"""
|
user = getattr(request.state, 'user', None)
|
||||||
FastAPI dependency untuk verifikasi CSRF token berbasis Redis.
|
|
||||||
Harus dikombinasikan dengan JWTBearer() dependency.
|
|
||||||
"""
|
|
||||||
# ── Step 1: ambil user_id dari request.state (di-set oleh JWTBearer) ────
|
|
||||||
user = getattr(request.state, "user", None)
|
|
||||||
user_id = str(user.user_id) if user else get_user_id()
|
user_id = str(user.user_id) if user else get_user_id()
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise HTTPException(status_code=401, detail="Authentication required")
|
raise HTTPException(status_code=401, detail='Authentication required')
|
||||||
|
csrf_token_header = request.headers.get('x-csrf-token') or request.headers.get('x-xsrf-token') or request.headers.get('x-csrf-token')
|
||||||
# ── Step 2: baca CSRF token dari request header ───────────────────────────
|
|
||||||
# Starlette lowercases all header names
|
|
||||||
csrf_token_header = (
|
|
||||||
request.headers.get("x-csrf-token")
|
|
||||||
or request.headers.get("x-xsrf-token")
|
|
||||||
or request.headers.get("x-csrf-token")
|
|
||||||
)
|
|
||||||
|
|
||||||
if not csrf_token_header:
|
if not csrf_token_header:
|
||||||
log.warning(
|
log.warning('[CSRF] Missing X-CSRF-Token header | user_id=%s | IP: %s', user_id, request.client.host if request.client else 'unknown')
|
||||||
"[CSRF] Missing X-CSRF-Token header | user_id=%s | IP: %s",
|
raise HTTPException(status_code=403, detail='CSRF token missing in header')
|
||||||
user_id,
|
|
||||||
request.client.host if request.client else "unknown",
|
|
||||||
)
|
|
||||||
raise HTTPException(status_code=403, detail="CSRF token missing in header")
|
|
||||||
|
|
||||||
csrf_token_header = csrf_token_header.strip()
|
csrf_token_header = csrf_token_header.strip()
|
||||||
|
|
||||||
# ── Step 3: bangun Redis key (harus sama dengan format di be-auth) ─────────
|
|
||||||
current_path = request.url.path
|
current_path = request.url.path
|
||||||
path_hash = hashlib.sha256(current_path.encode()).hexdigest()[:16]
|
path_hash = hashlib.sha256(current_path.encode()).hexdigest()[:16]
|
||||||
redis_key = f"csrf:{user_id}:{path_hash}"
|
redis_key = f'csrf:{user_id}:{path_hash}'
|
||||||
|
|
||||||
# ── Step 4: ambil stored token hash dari Redis (DB 0) ────────────────────
|
|
||||||
stored_token_hash = _redis_auth.get(redis_key)
|
stored_token_hash = _redis_auth.get(redis_key)
|
||||||
if not stored_token_hash:
|
if not stored_token_hash:
|
||||||
log.warning(
|
log.warning('[CSRF] Token not found in Redis | user_id=%s | path=%s | IP: %s', user_id, current_path, request.client.host if request.client else 'unknown')
|
||||||
"[CSRF] Token not found in Redis | user_id=%s | path=%s | IP: %s",
|
raise HTTPException(status_code=403, detail='CSRF token expired or invalid')
|
||||||
user_id,
|
|
||||||
current_path,
|
|
||||||
request.client.host if request.client else "unknown",
|
|
||||||
)
|
|
||||||
raise HTTPException(status_code=403, detail="CSRF token expired or invalid")
|
|
||||||
|
|
||||||
if isinstance(stored_token_hash, bytes):
|
if isinstance(stored_token_hash, bytes):
|
||||||
stored_token_hash = stored_token_hash.decode("utf-8")
|
stored_token_hash = stored_token_hash.decode('utf-8')
|
||||||
|
|
||||||
# ── Step 5: bandingkan hash dengan constant-time digest ───────────────────
|
|
||||||
incoming_hash = hashlib.sha256(csrf_token_header.encode()).hexdigest()
|
incoming_hash = hashlib.sha256(csrf_token_header.encode()).hexdigest()
|
||||||
if not secrets.compare_digest(incoming_hash, stored_token_hash):
|
if not secrets.compare_digest(incoming_hash, stored_token_hash):
|
||||||
log.warning(
|
log.warning('[CSRF] Token mismatch | user_id=%s | path=%s | IP: %s', user_id, current_path, request.client.host if request.client else 'unknown')
|
||||||
"[CSRF] Token mismatch | user_id=%s | path=%s | IP: %s",
|
|
||||||
user_id,
|
|
||||||
current_path,
|
|
||||||
request.client.host if request.client else "unknown",
|
|
||||||
)
|
|
||||||
_redis_auth.delete(redis_key)
|
_redis_auth.delete(redis_key)
|
||||||
raise HTTPException(status_code=403, detail="CSRF token verification failed")
|
raise HTTPException(status_code=403, detail='CSRF token verification failed')
|
||||||
|
|
||||||
# ── Step 6: hapus token (one-time use) dan lanjutkan ─────────────────────
|
|
||||||
_redis_auth.delete(redis_key)
|
_redis_auth.delete(redis_key)
|
||||||
log.info(
|
log.info('[CSRF] Verified & deleted | user_id=%s | path=%s', user_id, current_path)
|
||||||
"[CSRF] Verified & deleted | user_id=%s | path=%s",
|
|
||||||
user_id,
|
|
||||||
current_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,22 +1,18 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from src.models import DefultBase
|
from src.models import DefultBase
|
||||||
|
|
||||||
|
|
||||||
class CommonParams(DefultBase):
|
class CommonParams(DefultBase):
|
||||||
# This ensures no extra query params are allowed
|
current_user: Optional[str] = Field(None, alias='currentUser')
|
||||||
current_user: Optional[str] = Field(None, alias="currentUser")
|
page: int = Field(1, gt=0, le=1000000)
|
||||||
page: int = Field(1, gt=0, lt=2147483647)
|
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias='itemsPerPage')
|
||||||
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias="itemsPerPage")
|
query_str: Optional[str] = Field(None, alias='q')
|
||||||
query_str: Optional[str] = Field(None, alias="q")
|
filter_spec: Optional[str] = Field(None, alias='filter')
|
||||||
filter_spec: Optional[str] = Field(None, alias="filter")
|
sort_by: List[str] = Field(default_factory=list, alias='sortBy[]')
|
||||||
sort_by: List[str] = Field(default_factory=list, alias="sortBy[]")
|
descending: List[bool] = Field(default_factory=list, alias='descending[]')
|
||||||
descending: List[bool] = Field(default_factory=list, alias="descending[]")
|
exclude: List[str] = Field(default_factory=list, alias='exclude[]')
|
||||||
exclude: List[str] = Field(default_factory=list, alias="exclude[]")
|
all_params: int = Field(0, alias='all', ge=0, le=1000000)
|
||||||
all_params: int = Field(0, alias="all")
|
|
||||||
|
|
||||||
# Property to mirror your original return dict's bool conversion
|
|
||||||
@property
|
@property
|
||||||
def is_all(self) -> bool:
|
def is_all(self) -> bool:
|
||||||
return bool(self.all_params)
|
return bool(self.all_params)
|
||||||
@ -1,149 +1,58 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Annotated, List, Type, TypeVar
|
from typing import Annotated, List, Type, TypeVar
|
||||||
|
|
||||||
from fastapi import Depends, Query
|
from fastapi import Depends, Query
|
||||||
from pydantic.types import Json, constr
|
from pydantic.types import Json, constr
|
||||||
from sqlalchemy import Select, desc, func, or_
|
from sqlalchemy import Select, desc, func, or_
|
||||||
from sqlalchemy.exc import ProgrammingError
|
|
||||||
from sqlalchemy_filters import apply_pagination
|
|
||||||
|
|
||||||
from src.database.schema import CommonParams
|
from src.database.schema import CommonParams
|
||||||
|
|
||||||
from .core import DbSession
|
from .core import DbSession
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
QueryStr = constr(pattern='^[ -~]+$', min_length=1)
|
||||||
|
|
||||||
# allows only printable characters
|
def common_parameters(db_session: DbSession, current_user: QueryStr=Query(None, alias='currentUser'), page: int=Query(1, gt=0, le=1000000), items_per_page: int=Query(5, alias='itemsPerPage', gt=-2, le=1000000), query_str: QueryStr=Query(None, alias='q'), filter_spec: QueryStr=Query(None, alias='filter'), sort_by: List[str]=Query([], alias='sortBy[]'), descending: List[bool]=Query([], alias='descending[]'), exclude: List[str]=Query([], alias='exclude[]'), all: int=Query(0, ge=0, le=1000000)):
|
||||||
QueryStr = constr(pattern=r"^[ -~]+$", min_length=1)
|
return {'db_session': db_session, 'page': page, 'items_per_page': items_per_page, 'query_str': query_str, 'filter_spec': filter_spec, 'sort_by': sort_by, 'descending': descending, 'current_user': current_user, 'all': bool(all)}
|
||||||
|
CommonParameters = Annotated[dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool, Depends(common_parameters)]
|
||||||
|
T = TypeVar('T', bound=CommonParams)
|
||||||
def common_parameters(
|
|
||||||
db_session: DbSession, # type: ignore
|
|
||||||
current_user: QueryStr = Query(None, alias="currentUser"), # type: ignore
|
|
||||||
page: int = Query(1, gt=0, lt=2147483647),
|
|
||||||
items_per_page: int = Query(5, alias="itemsPerPage", gt=-2, lt=2147483647),
|
|
||||||
query_str: QueryStr = Query(None, alias="q"), # type: ignore
|
|
||||||
filter_spec: QueryStr = Query(None, alias="filter"), # type: ignore
|
|
||||||
sort_by: List[str] = Query([], alias="sortBy[]"),
|
|
||||||
descending: List[bool] = Query([], alias="descending[]"),
|
|
||||||
exclude: List[str] = Query([], alias="exclude[]"),
|
|
||||||
all: int = Query(0),
|
|
||||||
# role: QueryStr = Depends(get_current_role),
|
|
||||||
):
|
|
||||||
return {
|
|
||||||
"db_session": db_session,
|
|
||||||
"page": page,
|
|
||||||
"items_per_page": items_per_page,
|
|
||||||
"query_str": query_str,
|
|
||||||
"filter_spec": filter_spec,
|
|
||||||
"sort_by": sort_by,
|
|
||||||
"descending": descending,
|
|
||||||
"current_user": current_user,
|
|
||||||
"all": bool(all),
|
|
||||||
# "role": role,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CommonParameters = Annotated[
|
|
||||||
dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool,
|
|
||||||
Depends(common_parameters),
|
|
||||||
]
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=CommonParams)
|
|
||||||
|
|
||||||
def get_params_factory(model_type: Type[T]):
|
def get_params_factory(model_type: Type[T]):
|
||||||
async def wrapper(
|
|
||||||
db_session: DbSession,
|
async def wrapper(db_session: DbSession, params: Annotated[model_type, Query()]):
|
||||||
params: Annotated[model_type, Query()] # type: ignore
|
|
||||||
):
|
|
||||||
res = params.model_dump()
|
res = params.model_dump()
|
||||||
return {
|
return {'db_session': db_session, 'all': params.is_all, **res}
|
||||||
"db_session": db_session,
|
|
||||||
"all": params.is_all,
|
|
||||||
**res
|
|
||||||
}
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def search(*, query_str: str, query: Query, model, sort=False):
|
def search(*, query_str: str, query: Query, model, sort=False):
|
||||||
"""Perform a search based on the query."""
|
|
||||||
search_model = model
|
search_model = model
|
||||||
|
|
||||||
if not query_str.strip():
|
if not query_str.strip():
|
||||||
return query
|
return query
|
||||||
|
|
||||||
search = []
|
search = []
|
||||||
if hasattr(search_model, "search_vector"):
|
if hasattr(search_model, 'search_vector'):
|
||||||
vector = search_model.search_vector
|
vector = search_model.search_vector
|
||||||
search.append(vector.op("@@")(func.tsq_parse(query_str)))
|
search.append(vector.op('@@')(func.tsq_parse(query_str)))
|
||||||
|
if hasattr(search_model, 'name'):
|
||||||
if hasattr(search_model, "name"):
|
search.append(search_model.name.ilike(f'%{query_str}%'))
|
||||||
search.append(
|
|
||||||
search_model.name.ilike(f"%{query_str}%"),
|
|
||||||
)
|
|
||||||
search.append(search_model.name == query_str)
|
search.append(search_model.name == query_str)
|
||||||
|
|
||||||
if not search:
|
if not search:
|
||||||
raise Exception(f"Search not supported for model: {model}")
|
raise Exception(f'Search not supported for model: {model}')
|
||||||
|
|
||||||
query = query.filter(or_(*search))
|
query = query.filter(or_(*search))
|
||||||
|
|
||||||
if sort:
|
if sort:
|
||||||
query = query.order_by(desc(func.ts_rank_cd(vector, func.tsq_parse(query_str))))
|
query = query.order_by(desc(func.ts_rank_cd(vector, func.tsq_parse(query_str))))
|
||||||
|
|
||||||
return query.params(term=query_str)
|
return query.params(term=query_str)
|
||||||
|
|
||||||
|
async def search_filter_sort_paginate(db_session: DbSession, model, query_str: str=None, filter_spec: str | dict | None=None, page: int=1, items_per_page: int=5, sort_by: List[str]=None, descending: List[bool]=None, current_user: str=None, exclude: List[str]=None, all: bool=False):
|
||||||
async def search_filter_sort_paginate(
|
|
||||||
db_session: DbSession,
|
|
||||||
model,
|
|
||||||
query_str: str = None,
|
|
||||||
filter_spec: str | dict | None = None,
|
|
||||||
page: int = 1,
|
|
||||||
items_per_page: int = 5,
|
|
||||||
sort_by: List[str] = None,
|
|
||||||
descending: List[bool] = None,
|
|
||||||
current_user: str = None,
|
|
||||||
exclude: List[str] = None,
|
|
||||||
all: bool = False,
|
|
||||||
):
|
|
||||||
"""Common functionality for searching, filtering, sorting, and pagination."""
|
|
||||||
# try:
|
|
||||||
# Check if model is Select
|
|
||||||
if not isinstance(model, Select):
|
if not isinstance(model, Select):
|
||||||
query = Select(model)
|
query = Select(model)
|
||||||
else:
|
else:
|
||||||
query = model
|
query = model
|
||||||
|
|
||||||
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, model=model, sort=sort)
|
query = search(query_str=query_str, query=query, model=model, sort=sort)
|
||||||
|
|
||||||
# Get total count
|
|
||||||
count_query = Select(func.count()).select_from(query.subquery())
|
count_query = Select(func.count()).select_from(query.subquery())
|
||||||
total = await db_session.scalar(count_query)
|
total = await db_session.scalar(count_query)
|
||||||
|
|
||||||
if all:
|
if all:
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
items = result.scalars().all()
|
items = result.scalars().all()
|
||||||
|
return {'items': items, 'itemsPerPage': total, 'page': 1, 'total': total, 'totalPages': 1}
|
||||||
return {
|
|
||||||
"items": items,
|
|
||||||
"itemsPerPage": total,
|
|
||||||
"page": 1,
|
|
||||||
"total": total,
|
|
||||||
"totalPages": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
query = query.offset((page - 1) * items_per_page).limit(items_per_page)
|
query = 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()
|
||||||
|
return {'items': items, 'itemsPerPage': items_per_page, 'page': page, 'total': total, 'totalPages': (total + items_per_page - 1) // items_per_page}
|
||||||
return {
|
|
||||||
"items": items,
|
|
||||||
"itemsPerPage": items_per_page,
|
|
||||||
"page": page,
|
|
||||||
"total": total,
|
|
||||||
"totalPages": (total + items_per_page - 1) // items_per_page,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,29 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
else:
|
else:
|
||||||
from strenum import StrEnum
|
from strenum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class OptimumOHEnum(StrEnum):
|
class OptimumOHEnum(StrEnum):
|
||||||
"""
|
pass
|
||||||
A custom Enum class that extends StrEnum.
|
|
||||||
|
|
||||||
This class inherits all functionality from StrEnum, including
|
|
||||||
string representation and automatic value conversion to strings.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
class Visibility(DispatchEnum):
|
|
||||||
OPEN = "Open"
|
|
||||||
RESTRICTED = "Restricted"
|
|
||||||
|
|
||||||
assert str(Visibility.OPEN) == "Open"
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass # No additional implementation needed
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseStatus(OptimumOHEnum):
|
class ResponseStatus(OptimumOHEnum):
|
||||||
SUCCESS = "success"
|
SUCCESS = 'success'
|
||||||
ERROR = "error"
|
ERROR = 'error'
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,21 +1,12 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
from sqlalchemy.orm import relationship
|
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
|
||||||
from src.workorder.model import MasterWorkOrder
|
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentPart(Base, DefaultMixin):
|
class ScopeEquipmentPart(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_scope_equipment_part"
|
__tablename__ = 'oh_ms_scope_equipment_part'
|
||||||
|
|
||||||
required_stock = Column(Float, nullable=False, default=0)
|
required_stock = Column(Float, nullable=False, default=0)
|
||||||
sparepart_id = Column(UUID(as_uuid=True), ForeignKey("oh_ms_sparepart.id"), nullable=False)
|
sparepart_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_sparepart.id'), nullable=False)
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
|
part = relationship('MasterSparePart', lazy='selectin')
|
||||||
part = relationship(
|
|
||||||
"MasterSparePart",
|
|
||||||
lazy="selectin",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,28 +1,12 @@
|
|||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
||||||
|
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.database.service import (CommonParameters, DbSession,
|
|
||||||
search_filter_sort_paginate)
|
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
|
|
||||||
from .schema import (ScopeEquipmentActivityCreate,
|
|
||||||
ScopeEquipmentActivityPagination,
|
|
||||||
ScopeEquipmentActivityRead, ScopeEquipmentActivityUpdate)
|
|
||||||
from .service import get_all
|
from .service import get_all
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('/{location_tag}', response_model=StandardResponse[List[Dict]])
|
||||||
@router.get("/{location_tag}", response_model=StandardResponse[List[Dict]])
|
|
||||||
async def get_scope_equipment_parts(collector_db_session: CollectorDbSession, location_tag):
|
async def get_scope_equipment_parts(collector_db_session: CollectorDbSession, location_tag):
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
# return
|
|
||||||
data = await get_all(db_session=collector_db_session, location_tag=location_tag)
|
data = await get_all(db_session=collector_db_session, location_tag=location_tag)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=data,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,218 +1,29 @@
|
|||||||
import random
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy import Delete, Select, and_, text
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
|
||||||
from src.database.service import CommonParameters, search_filter_sort_paginate
|
|
||||||
|
|
||||||
from .model import ScopeEquipmentPart
|
|
||||||
from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate
|
|
||||||
|
|
||||||
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
|
||||||
# """Returns a document based on the given document id."""
|
|
||||||
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
|
|
||||||
# return result
|
|
||||||
|
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession as DbSession
|
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# async def get_all(
|
|
||||||
# db_session: CollectorDbSession,
|
|
||||||
# location_tag: Optional[str] = None,
|
|
||||||
# start_year: int = 2023,
|
|
||||||
# end_year: Optional[int] = None,
|
|
||||||
# parent_wonum: Optional[str] = None
|
|
||||||
# ) -> List[Dict[str, Any]]:
|
|
||||||
# """
|
|
||||||
# Retrieve overhaul spare parts consumption data.
|
|
||||||
# Handles missing data, null parent WO, and query safety.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# db_session: Async SQLAlchemy session
|
|
||||||
# location_tag: Optional location filter
|
|
||||||
# start_year: Year to start analysis (default 2023)
|
|
||||||
# end_year: Optional year to end analysis (default start_year + 1)
|
|
||||||
# parent_wonum: Parent work order number (required for context)
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# List of dictionaries with spare part usage per overhaul WO.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# # --- 1. Basic validation ---
|
|
||||||
# if not parent_wonum:
|
|
||||||
# logger.warning("Parent WO number not provided. Returning empty result.")
|
|
||||||
# return []
|
|
||||||
|
|
||||||
# if start_year < 1900 or (end_year and end_year < start_year):
|
|
||||||
# raise ValueError("Invalid year range provided.")
|
|
||||||
|
|
||||||
# if end_year is None:
|
|
||||||
# end_year = start_year + 1
|
|
||||||
|
|
||||||
# # --- 2. Build SQL safely ---
|
|
||||||
# base_query = """
|
|
||||||
# WITH filtered_wo AS (
|
|
||||||
# SELECT wonum, location_tag
|
|
||||||
# FROM public.wo_max
|
|
||||||
# WHERE worktype = 'OH'
|
|
||||||
# AND xx_parent = :parent_wonum
|
|
||||||
# """
|
|
||||||
|
|
||||||
# params = {
|
|
||||||
# "parent_wonum": parent_wonum,
|
|
||||||
# }
|
|
||||||
|
|
||||||
# if location_tag:
|
|
||||||
# base_query += " AND location_tag = :location_tag"
|
|
||||||
# params["location_tag"] = location_tag
|
|
||||||
|
|
||||||
# base_query += """
|
|
||||||
# ),
|
|
||||||
# filtered_materials AS (
|
|
||||||
# SELECT wonum, itemnum, itemqty, inv_curbaltotal, inv_avgcost
|
|
||||||
# FROM public.wo_max_material
|
|
||||||
# WHERE wonum IN (SELECT wonum FROM filtered_wo)
|
|
||||||
# )
|
|
||||||
# SELECT
|
|
||||||
# fwo.location_tag AS location_tag,
|
|
||||||
# fm.itemnum,
|
|
||||||
# spl.description AS sparepart_name,
|
|
||||||
# COALESCE(SUM(fm.itemqty), 0) AS parts_consumed_in_oh,
|
|
||||||
# COALESCE(AVG(fm.inv_avgcost), 0) AS avgcost,
|
|
||||||
# COALESCE(AVG(fm.inv_curbaltotal), 0) AS inv_curbaltotal
|
|
||||||
# FROM filtered_wo fwo
|
|
||||||
# INNER JOIN filtered_materials fm ON fwo.wonum = fm.wonum
|
|
||||||
# LEFT JOIN public.maximo_sparepart_pr_po_line spl ON fm.itemnum = spl.item_num
|
|
||||||
# GROUP BY fwo.location_tag, fm.itemnum, spl.description
|
|
||||||
# ORDER BY fwo.location_tag, fm.itemnum;
|
|
||||||
# """
|
|
||||||
|
|
||||||
# # --- 3. Execute query ---
|
|
||||||
# try:
|
|
||||||
# result = await db_session.execute(text(base_query), params)
|
|
||||||
# rows = result.fetchall()
|
|
||||||
|
|
||||||
# # Handle "no data found"
|
|
||||||
# if not rows:
|
|
||||||
# logger.info(f"No spare part data found for parent WO {parent_wonum}.")
|
|
||||||
# return []
|
|
||||||
|
|
||||||
# # --- 4. Map results cleanly ---
|
|
||||||
# equipment_parts = []
|
|
||||||
# for row in rows:
|
|
||||||
# try:
|
|
||||||
# equipment_parts.append({
|
|
||||||
# "location_tag": row.location_tag,
|
|
||||||
# "itemnum": row.itemnum,
|
|
||||||
# "sparepart_name": row.sparepart_name or "-",
|
|
||||||
# "parts_consumed_in_oh": float(row.parts_consumed_in_oh or 0),
|
|
||||||
# "avgcost": float(row.avgcost or 0),
|
|
||||||
# "inv_curbaltotal": float(row.inv_curbaltotal or 0)
|
|
||||||
# })
|
|
||||||
# except Exception as parse_err:
|
|
||||||
# logger.error(f"Failed to parse row {row}: {parse_err}")
|
|
||||||
# continue # Skip malformed rows
|
|
||||||
|
|
||||||
# return equipment_parts
|
|
||||||
|
|
||||||
# except Exception as e:
|
|
||||||
# logger.exception(f"Database query failed: {e}")
|
|
||||||
# raise RuntimeError("Failed to fetch overhaul spare parts data.") from e
|
|
||||||
|
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
|
async def get_all(db_session: AsyncSession, location_tag: Optional[str]=None, start_year: int=2023, end_year: Optional[int]=None) -> List[Dict[str, Any]]:
|
||||||
async def get_all(
|
|
||||||
db_session: AsyncSession,
|
|
||||||
location_tag: Optional[str] = None,
|
|
||||||
start_year: int = 2023,
|
|
||||||
end_year: Optional[int] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Get overhaul spare parts consumption data with optimized query.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_session: SQLAlchemy async database session
|
|
||||||
location_tag: Optional filter for location (asset_location)
|
|
||||||
start_year: Starting year (default: 2023)
|
|
||||||
end_year: Ending year (default: start_year + 1)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of dictionaries with spare parts consumption data
|
|
||||||
"""
|
|
||||||
# Set default end year
|
|
||||||
if end_year is None:
|
if end_year is None:
|
||||||
end_year = start_year + 1
|
end_year = start_year + 1
|
||||||
|
query_str = "\n WITH filtered_wo AS (\n SELECT DISTINCT wonum, asset_location, asset_unit\n FROM public.wo_maximo ma\n WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')\n "
|
||||||
# Build query dynamically
|
|
||||||
query_str = """
|
|
||||||
WITH filtered_wo AS (
|
|
||||||
SELECT DISTINCT wonum, asset_location, asset_unit
|
|
||||||
FROM public.wo_maximo ma
|
|
||||||
WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')
|
|
||||||
"""
|
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
# Optional filter for location
|
|
||||||
if location_tag:
|
if location_tag:
|
||||||
query_str += " AND asset_location = :location_tag"
|
query_str += ' AND asset_location = :location_tag'
|
||||||
params["location_tag"] = location_tag
|
params['location_tag'] = location_tag
|
||||||
|
query_str += "\n ),\n filtered_materials AS (\n SELECT \n mat.wonum,\n mat.itemnum,\n mat.itemqty,\n mat.inv_curbaltotal AS inv_curbaltotal,\n mat.inv_avgcost AS inv_avgcost\n FROM public.wo_maximo_material AS mat\n WHERE mat.wonum IN (SELECT wonum FROM filtered_wo)\n )\n SELECT \n fwo.asset_location AS location_tag,\n ft.itemnum,\n COALESCE(spl.description, 'Unknown') AS sparepart_name,\n AVG(ft.itemqty) AS total_parts_used,\n COALESCE(AVG(ft.inv_avgcost), 0) AS avg_cost,\n COALESCE(AVG(ft.inv_curbaltotal), 0) AS avg_inventory_balance\n FROM filtered_wo AS fwo\n INNER JOIN filtered_materials AS ft \n ON fwo.wonum = ft.wonum\n LEFT JOIN public.maximo_sparepart_pr_po_line AS spl \n ON ft.itemnum = spl.item_num\n GROUP BY fwo.asset_location, ft.itemnum, spl.description\n ORDER BY fwo.asset_location, ft.itemnum;\n "
|
||||||
query_str += """
|
|
||||||
),
|
|
||||||
filtered_materials AS (
|
|
||||||
SELECT
|
|
||||||
mat.wonum,
|
|
||||||
mat.itemnum,
|
|
||||||
mat.itemqty,
|
|
||||||
mat.inv_curbaltotal AS inv_curbaltotal,
|
|
||||||
mat.inv_avgcost AS inv_avgcost
|
|
||||||
FROM public.wo_maximo_material AS mat
|
|
||||||
WHERE mat.wonum IN (SELECT wonum FROM filtered_wo)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
fwo.asset_location AS location_tag,
|
|
||||||
ft.itemnum,
|
|
||||||
COALESCE(spl.description, 'Unknown') AS sparepart_name,
|
|
||||||
AVG(ft.itemqty) AS total_parts_used,
|
|
||||||
COALESCE(AVG(ft.inv_avgcost), 0) AS avg_cost,
|
|
||||||
COALESCE(AVG(ft.inv_curbaltotal), 0) AS avg_inventory_balance
|
|
||||||
FROM filtered_wo AS fwo
|
|
||||||
INNER JOIN filtered_materials AS ft
|
|
||||||
ON fwo.wonum = ft.wonum
|
|
||||||
LEFT JOIN public.maximo_sparepart_pr_po_line AS spl
|
|
||||||
ON ft.itemnum = spl.item_num
|
|
||||||
GROUP BY fwo.asset_location, ft.itemnum, spl.description
|
|
||||||
ORDER BY fwo.asset_location, ft.itemnum;
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await db_session.execute(text(query_str), params)
|
result = await db_session.execute(text(query_str), params)
|
||||||
rows = result.fetchall()
|
rows = result.fetchall()
|
||||||
|
|
||||||
equipment_parts = []
|
equipment_parts = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
equipment_parts.append({
|
equipment_parts.append({'location_tag': row.location_tag, 'itemnum': row.itemnum, 'sparepart_name': row.sparepart_name, 'parts_consumed_in_oh': float(row.total_parts_used or 0), 'avg_cost': float(row.avg_cost or 0), 'inv_curbaltotal': float(row.avg_inventory_balance or 0)})
|
||||||
"location_tag": row.location_tag,
|
|
||||||
"itemnum": row.itemnum,
|
|
||||||
"sparepart_name": row.sparepart_name,
|
|
||||||
"parts_consumed_in_oh": float(row.total_parts_used or 0),
|
|
||||||
"avg_cost": float(row.avg_cost or 0),
|
|
||||||
"inv_curbaltotal": float(row.avg_inventory_balance or 0),
|
|
||||||
})
|
|
||||||
|
|
||||||
return equipment_parts
|
return equipment_parts
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[get_all] Database query error: {e}")
|
print(f'[get_all] Database query error: {e}')
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,22 +1,11 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
|
from sqlalchemy import UUID, Column, ForeignKey, String
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
from sqlalchemy.orm import relationship
|
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
|
||||||
from src.workorder.model import MasterWorkOrder
|
|
||||||
|
|
||||||
|
|
||||||
class EquipmentWorkscopeGroup(Base, DefaultMixin):
|
class EquipmentWorkscopeGroup(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_tr_equipment_workscope_group"
|
__tablename__ = 'oh_tr_equipment_workscope_group'
|
||||||
|
|
||||||
workscope_group_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_workscope_group.id'))
|
workscope_group_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_workscope_group.id'))
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
|
workscope_group = relationship('MasterActivity', lazy='selectin', back_populates='equipment_workscope_groups')
|
||||||
workscope_group = relationship("MasterActivity", lazy="selectin", back_populates="equipment_workscope_groups")
|
equipment = relationship('StandardScope', lazy='raise', primaryjoin='and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))', uselist=False)
|
||||||
equipment = relationship(
|
|
||||||
"StandardScope",
|
|
||||||
lazy="raise",
|
|
||||||
primaryjoin="and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))",
|
|
||||||
uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,51 +1,22 @@
|
|||||||
from typing import Dict, List, Optional
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Query
|
||||||
from fastapi import APIRouter, HTTPException, Query, status
|
from src.database.service import CommonParameters, DbSession
|
||||||
|
|
||||||
from src.database.service import (CommonParameters, DbSession,
|
|
||||||
search_filter_sort_paginate)
|
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
|
||||||
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
|
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
|
||||||
from .service import create, delete, get_all
|
from .service import create, delete, get_all
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('/{location_tag}', response_model=StandardResponse[ScopeEquipmentJobPagination])
|
||||||
@router.get(
|
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
||||||
"/{location_tag}", response_model=StandardResponse[ScopeEquipmentJobPagination]
|
|
||||||
)
|
|
||||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str] = Query(None)):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/{assetnum}', response_model=StandardResponse[None])
|
||||||
data=results,
|
async def create_scope_equipment_jobs(db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{assetnum}", response_model=StandardResponse[None])
|
|
||||||
async def create_scope_equipment_jobs(
|
|
||||||
db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate
|
|
||||||
):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
# 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)
|
||||||
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/delete/{scope_job_id}', response_model=StandardResponse[None])
|
||||||
data=None,
|
|
||||||
message="Data created successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{scope_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def delete_scope_equipment_job(db_session: DbSession, scope_job_id):
|
async def delete_scope_equipment_job(db_session: DbSession, scope_job_id):
|
||||||
|
|
||||||
await delete(db_session=db_session, scope_job_id=scope_job_id)
|
await delete(db_session=db_session, scope_job_id=scope_job_id)
|
||||||
|
return StandardResponse(data=None, message='Data deleted successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data deleted successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,153 +1,46 @@
|
|||||||
import random
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import Delete, Select, and_
|
from sqlalchemy import Select
|
||||||
from sqlalchemy.orm import 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 search_filter_sort_paginate
|
||||||
from src.overhaul_activity.model import OverhaulActivity
|
|
||||||
from src.standard_scope.model import MasterEquipment
|
from src.standard_scope.model import MasterEquipment
|
||||||
from src.standard_scope.service import get_equipment_level_by_no
|
|
||||||
from src.workscope_group.model import MasterActivity
|
from src.workscope_group.model import MasterActivity
|
||||||
from src.workscope_group_maintenance_type.model import WorkscopeOHType
|
from src.workscope_group_maintenance_type.model import WorkscopeOHType
|
||||||
from src.overhaul_scope.model import MaintenanceType
|
from src.overhaul_scope.model import MaintenanceType
|
||||||
|
|
||||||
from .model import EquipmentWorkscopeGroup
|
from .model import EquipmentWorkscopeGroup
|
||||||
from .schema import ScopeEquipmentJobCreate
|
from .schema import ScopeEquipmentJobCreate
|
||||||
from src.soft_delete import apply_not_deleted_filter
|
from src.soft_delete import apply_not_deleted_filter
|
||||||
|
|
||||||
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
async def get_all(*, common, location_tag: str, scope: Optional[str]=None):
|
||||||
# """Returns a document based on the given document id."""
|
query = Select(EquipmentWorkscopeGroup).where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
||||||
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
|
|
||||||
# return result
|
|
||||||
|
|
||||||
|
|
||||||
# async def get_all(db_session: DbSession, assetnum: Optional[str], common):
|
|
||||||
# # Example usage
|
|
||||||
# if not assetnum:
|
|
||||||
# raise ValueError("assetnum parameter is required")
|
|
||||||
|
|
||||||
# # First get the parent equipment
|
|
||||||
# equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum)
|
|
||||||
# equipment: MasterEquipment = await db_session.scalar(equipment_stmt)
|
|
||||||
|
|
||||||
# if not equipment:
|
|
||||||
# raise ValueError(f"No equipment found with assetnum: {assetnum}")
|
|
||||||
|
|
||||||
# # Build query for parts
|
|
||||||
# stmt = (
|
|
||||||
# Select(ScopeEquipmentJob)
|
|
||||||
# .where(ScopeEquipmentJob.assetnum == assetnum)
|
|
||||||
# .options(
|
|
||||||
# selectinload(ScopeEquipmentJob.job),
|
|
||||||
# selectinload(ScopeEquipmentJob.overhaul_jobs)
|
|
||||||
# .selectinload(OverhaulJob.overhaul_activity)
|
|
||||||
# .selectinload(OverhaulActivity.overhaul_scope),
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
|
|
||||||
# results = await search_filter_sort_paginate(model=stmt, **common)
|
|
||||||
|
|
||||||
# return results
|
|
||||||
|
|
||||||
async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
|
|
||||||
"""Returns all documents."""
|
|
||||||
query = (
|
|
||||||
Select(EquipmentWorkscopeGroup)
|
|
||||||
.where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
|
||||||
)
|
|
||||||
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
||||||
|
|
||||||
if scope:
|
if scope:
|
||||||
query = (
|
query = query.join(EquipmentWorkscopeGroup.workscope_group).join(MasterActivity.oh_types).join(WorkscopeOHType.oh_type).filter(MaintenanceType.name == scope)
|
||||||
query
|
return await search_filter_sort_paginate(model=query, **common)
|
||||||
.join(EquipmentWorkscopeGroup.workscope_group)
|
|
||||||
.join(MasterActivity.oh_types)
|
|
||||||
.join(WorkscopeOHType.oh_type)
|
|
||||||
.filter(MaintenanceType.name == scope)
|
|
||||||
)
|
|
||||||
|
|
||||||
results = await search_filter_sort_paginate(model=query, **common)
|
async def create(*, db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum)
|
equipment_stmt = Select(MasterEquipment).where(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(assetnum=assetnum, job_id=job_id)
|
scope_equipment_job = ScopeEquipmentJob(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)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
async def delete(*, db_session: DbSession, scope_job_id: int) -> bool:
|
||||||
# async def update(*, db_session: DbSession, activity: ScopeEquipmentActivity, scope_equipment_activty_in: ScopeEquipmentActivityUpdate):
|
|
||||||
# """Updates a document."""
|
|
||||||
# data = scope_equipment_activty_in.model_dump()
|
|
||||||
|
|
||||||
# update_data = scope_equipment_activty_in.model_dump(exclude_defaults=True)
|
|
||||||
|
|
||||||
# for field in data:
|
|
||||||
# if field in update_data:
|
|
||||||
# setattr(activity, field, update_data[field])
|
|
||||||
|
|
||||||
# await db_session.commit()
|
|
||||||
|
|
||||||
# return activity
|
|
||||||
|
|
||||||
|
|
||||||
async def delete(
|
|
||||||
*,
|
|
||||||
db_session: DbSession,
|
|
||||||
scope_job_id: int,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Deletes a scope job and returns success status.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_session: Database session
|
|
||||||
scope_job_id: ID of the scope job to delete
|
|
||||||
user_id: ID of user performing the deletion
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if deletion was successful, False otherwise
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: If scope job doesn't exist
|
|
||||||
AuthorizationError: If user lacks delete permission
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Check if job exists
|
|
||||||
scope_job = await db_session.get(ScopeEquipmentJob, scope_job_id)
|
scope_job = await db_session.get(ScopeEquipmentJob, scope_job_id)
|
||||||
if not scope_job:
|
if not scope_job:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="A data with this id does not exist.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Perform deletion
|
|
||||||
await db_session.delete(scope_job)
|
await db_session.delete(scope_job)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
except Exception:
|
||||||
except Exception as e:
|
|
||||||
await db_session.rollback()
|
await db_session.rollback()
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,332 +1,37 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from sqlalchemy import select, func, cast, Numeric, text
|
from sqlalchemy import select, text
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import and_
|
|
||||||
from sqlalchemy.sql import not_
|
|
||||||
from src.maximo.model import WorkOrderData # Assuming this is where your model is
|
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
|
|
||||||
async def get_cm_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
|
async def get_cm_cost_summary(collector_db: CollectorDbSession, last_oh_date: datetime, upcoming_oh_date: datetime):
|
||||||
query = text("""WITH part_costs AS (
|
query = text("WITH part_costs AS (\n SELECT \n mu.wonum,\n SUM(mu.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost\n FROM maximo_workorder_materials mu\n LEFT JOIN maximo_inventory inv\n ON mu.itemnum = inv.itemnum\n LEFT JOIN (\n SELECT item_num, AVG(unit_cost) AS unit_cost\n FROM maximo_sparepart_pr_po_line\n GROUP BY item_num\n ) po\n ON mu.itemnum = po.item_num\n GROUP BY mu.wonum\n),\nwo_costs AS (\n SELECT \n w.wonum,\n w.asset_location,\n (COALESCE(w.mat_cost_max, 0) + COALESCE(pc.parts_total_cost, 0)) AS total_wo_cost\n FROM wo_staging_maximo_2 w\n LEFT JOIN part_costs pc\n ON w.wonum = pc.wonum\n WHERE \n w.worktype IN ('CM', 'EM', 'PROACTIVE')\n AND w.asset_system IN (\n 'HPB','AH','APC','SCR','CL','DM','CRH','ASH','BAD','DS','WTP',\n 'MT','SUP','DCS','FF','EG','AI','SPS','EVM','SCW','KLH','CH',\n 'TUR','LOT','HRH','ESP','CAE','GMC','BFT','LSH','CHB','BSS',\n 'LOS','LPB','SAC','CP','EHS','RO','GG','MS','CW','SO','ATT',\n 'AFG','EHB','RP','FO','PC','APE','AF','DMW','BRS','GEN','ABS',\n 'CHA','TR','H2','BDW','LOM','ACR','AL','FW','COND','CCCW','IA',\n 'GSS','BOL','SSB','CO','OA','CTH-UPD','AS','DP'\n )\n AND w.reportdate IS NOT NULL\n AND w.actstart IS NOT NULL\n AND w.actfinish IS NOT NULL\n AND w.asset_unit IN ('3','00')\n AND w.reportdate >= '2015-01-01'\n AND w.wonum NOT LIKE 'T%'\n),\n-- find max cost per location\nlocation_max AS (\n SELECT asset_location, MAX(total_wo_cost) AS max_cost\n FROM wo_costs\n WHERE total_wo_cost > 0\n GROUP BY asset_location\n),\n-- filter WO costs to only reasonable range (e.g. >0 and >=10% of max)\nfiltered_wo AS (\n SELECT w.*\n FROM wo_costs w\n JOIN location_max lm ON w.asset_location = lm.asset_location\n WHERE w.total_wo_cost > 0\n)\nSELECT \n asset_location,\n SUM(total_wo_cost)::numeric / COUNT(wonum) AS avg_cost\nFROM filtered_wo\nGROUP BY asset_location\nORDER BY avg_cost DESC;\n ")
|
||||||
SELECT
|
|
||||||
mu.wonum,
|
|
||||||
SUM(mu.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost
|
|
||||||
FROM maximo_workorder_materials mu
|
|
||||||
LEFT JOIN maximo_inventory inv
|
|
||||||
ON mu.itemnum = inv.itemnum
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT item_num, AVG(unit_cost) AS unit_cost
|
|
||||||
FROM maximo_sparepart_pr_po_line
|
|
||||||
GROUP BY item_num
|
|
||||||
) po
|
|
||||||
ON mu.itemnum = po.item_num
|
|
||||||
GROUP BY mu.wonum
|
|
||||||
),
|
|
||||||
wo_costs AS (
|
|
||||||
SELECT
|
|
||||||
w.wonum,
|
|
||||||
w.asset_location,
|
|
||||||
(COALESCE(w.mat_cost_max, 0) + COALESCE(pc.parts_total_cost, 0)) AS total_wo_cost
|
|
||||||
FROM wo_staging_maximo_2 w
|
|
||||||
LEFT JOIN part_costs pc
|
|
||||||
ON w.wonum = pc.wonum
|
|
||||||
WHERE
|
|
||||||
w.worktype IN ('CM', 'EM', 'PROACTIVE')
|
|
||||||
AND w.asset_system IN (
|
|
||||||
'HPB','AH','APC','SCR','CL','DM','CRH','ASH','BAD','DS','WTP',
|
|
||||||
'MT','SUP','DCS','FF','EG','AI','SPS','EVM','SCW','KLH','CH',
|
|
||||||
'TUR','LOT','HRH','ESP','CAE','GMC','BFT','LSH','CHB','BSS',
|
|
||||||
'LOS','LPB','SAC','CP','EHS','RO','GG','MS','CW','SO','ATT',
|
|
||||||
'AFG','EHB','RP','FO','PC','APE','AF','DMW','BRS','GEN','ABS',
|
|
||||||
'CHA','TR','H2','BDW','LOM','ACR','AL','FW','COND','CCCW','IA',
|
|
||||||
'GSS','BOL','SSB','CO','OA','CTH-UPD','AS','DP'
|
|
||||||
)
|
|
||||||
AND w.reportdate IS NOT NULL
|
|
||||||
AND w.actstart IS NOT NULL
|
|
||||||
AND w.actfinish IS NOT NULL
|
|
||||||
AND w.asset_unit IN ('3','00')
|
|
||||||
AND w.reportdate >= '2015-01-01'
|
|
||||||
AND w.wonum NOT LIKE 'T%'
|
|
||||||
),
|
|
||||||
-- find max cost per location
|
|
||||||
location_max AS (
|
|
||||||
SELECT asset_location, MAX(total_wo_cost) AS max_cost
|
|
||||||
FROM wo_costs
|
|
||||||
WHERE total_wo_cost > 0
|
|
||||||
GROUP BY asset_location
|
|
||||||
),
|
|
||||||
-- filter WO costs to only reasonable range (e.g. >0 and >=10% of max)
|
|
||||||
filtered_wo AS (
|
|
||||||
SELECT w.*
|
|
||||||
FROM wo_costs w
|
|
||||||
JOIN location_max lm ON w.asset_location = lm.asset_location
|
|
||||||
WHERE w.total_wo_cost > 0
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
asset_location,
|
|
||||||
SUM(total_wo_cost)::numeric / COUNT(wonum) AS avg_cost
|
|
||||||
FROM filtered_wo
|
|
||||||
GROUP BY asset_location
|
|
||||||
ORDER BY avg_cost DESC;
|
|
||||||
""")
|
|
||||||
results = await collector_db.execute(query)
|
results = await collector_db.execute(query)
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
data.append({
|
data.append({'location_tag': row.asset_location, 'avg_cost': row.avg_cost})
|
||||||
"location_tag": row.asset_location,
|
return {item['location_tag']: item['avg_cost'] for item in data}
|
||||||
"avg_cost": row.avg_cost
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
item["location_tag"]: item["avg_cost"] for item in data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
|
|
||||||
# query = text("""
|
|
||||||
# WITH target_wo AS (
|
|
||||||
# -- Get work orders under a specific parent(s)
|
|
||||||
# SELECT
|
|
||||||
# wonum,
|
|
||||||
# xx_parent,
|
|
||||||
# assetnum,
|
|
||||||
# location_tag AS asset_location,
|
|
||||||
# actmatcost,
|
|
||||||
# actservcost,
|
|
||||||
# reportdate
|
|
||||||
# FROM public.wo_maxim
|
|
||||||
# WHERE xx_parent = ANY(:parent_nums)
|
|
||||||
# ),
|
|
||||||
# part_costs AS (
|
|
||||||
# -- Calculate parts cost per WO if actmatcost = 0
|
|
||||||
# SELECT
|
|
||||||
# wm.wonum,
|
|
||||||
# SUM(
|
|
||||||
# wm.itemqty *
|
|
||||||
# COALESCE(wm.inv_avgcost, po.unit_cost, 0)
|
|
||||||
# ) AS parts_total_cost
|
|
||||||
# FROM public.wo_maxim_material wm
|
|
||||||
# LEFT JOIN (
|
|
||||||
# SELECT item_num, AVG(unit_cost) AS unit_cost
|
|
||||||
# FROM public.maximo_sparepart_pr_po_line
|
|
||||||
# GROUP BY item_num
|
|
||||||
# ) po ON wm.itemnum = po.item_num
|
|
||||||
# WHERE wm.itemnum IS NOT NULL
|
|
||||||
# GROUP BY wm.wonum
|
|
||||||
# ),
|
|
||||||
# wo_costs AS (
|
|
||||||
# SELECT
|
|
||||||
# w.wonum,
|
|
||||||
# w.asset_location,
|
|
||||||
# CASE
|
|
||||||
# WHEN COALESCE(w.actmatcost, 0) > 0 THEN COALESCE(w.actmatcost, 0)
|
|
||||||
# ELSE COALESCE(pc.parts_total_cost, 0)
|
|
||||||
# END AS material_cost,
|
|
||||||
# COALESCE(w.actservcost, 0) AS service_cost
|
|
||||||
# FROM target_wo w
|
|
||||||
# LEFT JOIN part_costs pc ON w.wonum = pc.wonum
|
|
||||||
# )
|
|
||||||
# SELECT
|
|
||||||
# asset_location,
|
|
||||||
# ROUND(SUM(material_cost + service_cost)::numeric / COUNT(wonum), 2) AS avg_cost,
|
|
||||||
# COUNT(wonum) AS total_wo_count
|
|
||||||
# FROM wo_costs
|
|
||||||
# GROUP BY asset_location
|
|
||||||
# ORDER BY total_wo_count DESC;
|
|
||||||
# """)
|
|
||||||
|
|
||||||
# parent_nums = []
|
|
||||||
|
|
||||||
# result = await collector_db.execute(query, {"parent_nums": parent_nums})
|
|
||||||
# data = []
|
|
||||||
|
|
||||||
# for row in result:
|
|
||||||
# data.append({
|
|
||||||
# "location_tag": row.asset_location,
|
|
||||||
# "avg_cost": float(row.avg_cost or 0.0),
|
|
||||||
# "total_wo_count": row.total_wo_count,
|
|
||||||
# })
|
|
||||||
|
|
||||||
# return {item["location_tag"]: item["avg_cost"] for item in data}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
|
|
||||||
# query = text("""
|
|
||||||
# WITH part_costs AS (
|
|
||||||
# SELECT
|
|
||||||
# wm.wonum,
|
|
||||||
# SUM(wm.itemqty * COALESCE(wm.inv_avgcost, po.unit_cost, 0)) AS parts_total_cost
|
|
||||||
# FROM public.wo_maxim_material wm
|
|
||||||
# LEFT JOIN (
|
|
||||||
# SELECT item_num, AVG(unit_cost) AS unit_cost
|
|
||||||
# FROM public.maximo_sparepart_pr_po_line
|
|
||||||
# GROUP BY item_num
|
|
||||||
# ) po ON wm.itemnum = po.item_num
|
|
||||||
# WHERE wm.itemnum IS NOT NULL
|
|
||||||
# GROUP BY wm.wonum
|
|
||||||
# ),
|
|
||||||
# wo_costs AS (
|
|
||||||
# SELECT
|
|
||||||
# w.wonum,
|
|
||||||
# w.asset_location,
|
|
||||||
# -- Use mat_cost_max if parts_total_cost = 0
|
|
||||||
# CASE
|
|
||||||
# WHEN COALESCE(pc.parts_total_cost, 0) = 0 THEN COALESCE(w.mat_cost_max , 0)
|
|
||||||
# ELSE COALESCE(pc.parts_total_cost, 0)
|
|
||||||
# END AS total_wo_cost
|
|
||||||
# FROM wo_staging_maximo_2 w
|
|
||||||
# LEFT JOIN part_costs pc
|
|
||||||
# ON w.wonum = pc.wonum
|
|
||||||
# WHERE
|
|
||||||
# w.worktype = 'OH'
|
|
||||||
# AND w.reportdate IS NOT NULL
|
|
||||||
# AND w.actstart IS NOT NULL
|
|
||||||
# AND w.actfinish IS NOT NULL
|
|
||||||
# AND w.asset_unit IN ('3', '00')
|
|
||||||
# AND w.wonum NOT LIKE 'T%'
|
|
||||||
# )
|
|
||||||
# SELECT
|
|
||||||
# asset_location,
|
|
||||||
# AVG(total_wo_cost) AS avg_cost
|
|
||||||
# FROM wo_costs
|
|
||||||
# GROUP BY asset_location
|
|
||||||
# ORDER BY COUNT(wonum) DESC;
|
|
||||||
# """)
|
|
||||||
|
|
||||||
query = text("""
|
|
||||||
WITH part_costs AS (
|
|
||||||
SELECT
|
|
||||||
wm.wonum,
|
|
||||||
SUM(wm.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost
|
|
||||||
FROM public.maximo_workorder_materials wm
|
|
||||||
JOIN public.maximo_inventory AS inv on inv.itemnum = wm.itemnum
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT item_num, AVG(unit_cost) AS unit_cost
|
|
||||||
FROM public.maximo_sparepart_pr_po_line
|
|
||||||
GROUP BY item_num
|
|
||||||
) po ON wm.itemnum = po.item_num
|
|
||||||
WHERE wm.itemnum IS NOT NULL
|
|
||||||
GROUP BY wm.wonum
|
|
||||||
),
|
|
||||||
wo_costs AS (
|
|
||||||
SELECT
|
|
||||||
w.wonum,
|
|
||||||
w.asset_location,
|
|
||||||
-- Use mat_cost_max if parts_total_cost = 0
|
|
||||||
CASE
|
|
||||||
WHEN COALESCE(pc.parts_total_cost, 0) = 0 THEN COALESCE(w.mat_cost_max , 0)
|
|
||||||
ELSE COALESCE(pc.parts_total_cost, 0)
|
|
||||||
END AS total_wo_cost
|
|
||||||
FROM wo_staging_maximo_2 w
|
|
||||||
LEFT JOIN part_costs pc
|
|
||||||
ON w.wonum = pc.wonum
|
|
||||||
WHERE
|
|
||||||
w.worktype = 'OH'
|
|
||||||
AND w.reportdate IS NOT NULL
|
|
||||||
AND w.actstart IS NOT NULL
|
|
||||||
AND w.actfinish IS NOT NULL
|
|
||||||
AND w.asset_unit IN ('3', '00')
|
|
||||||
AND w.wonum NOT LIKE 'T%'
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
asset_location,
|
|
||||||
AVG(total_wo_cost) AS avg_cost
|
|
||||||
FROM wo_costs
|
|
||||||
GROUP BY asset_location
|
|
||||||
ORDER BY COUNT(wonum) DESC;
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date: datetime, upcoming_oh_date: datetime):
|
||||||
|
query = text("\n WITH part_costs AS (\n SELECT \n wm.wonum,\n SUM(wm.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost\n FROM public.maximo_workorder_materials wm\n JOIN public.maximo_inventory AS inv on inv.itemnum = wm.itemnum\n LEFT JOIN (\n SELECT item_num, AVG(unit_cost) AS unit_cost\n FROM public.maximo_sparepart_pr_po_line\n GROUP BY item_num\n ) po ON wm.itemnum = po.item_num\n WHERE wm.itemnum IS NOT NULL\n GROUP BY wm.wonum\n ),\n wo_costs AS (\n SELECT \n w.wonum,\n w.asset_location,\n -- Use mat_cost_max if parts_total_cost = 0\n CASE \n WHEN COALESCE(pc.parts_total_cost, 0) = 0 THEN COALESCE(w.mat_cost_max , 0)\n ELSE COALESCE(pc.parts_total_cost, 0)\n END AS total_wo_cost\n FROM wo_staging_maximo_2 w\n LEFT JOIN part_costs pc\n ON w.wonum = pc.wonum\n WHERE \n w.worktype = 'OH'\n AND w.reportdate IS NOT NULL\n AND w.actstart IS NOT NULL\n AND w.actfinish IS NOT NULL\n AND w.asset_unit IN ('3', '00')\n AND w.wonum NOT LIKE 'T%'\n )\n SELECT \n asset_location,\n AVG(total_wo_cost) AS avg_cost\n FROM wo_costs\n GROUP BY asset_location\n ORDER BY COUNT(wonum) DESC;\n ")
|
||||||
result = await collector_db.execute(query)
|
result = await collector_db.execute(query)
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for row in result:
|
for row in result:
|
||||||
data.append({
|
data.append({'location_tag': row.asset_location, 'avg_cost': row.avg_cost})
|
||||||
"location_tag": row.asset_location,
|
return {item['location_tag']: item['avg_cost'] for item in data}
|
||||||
"avg_cost": row.avg_cost
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
item["location_tag"]: item["avg_cost"] for item in data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
async def get_history_oh_wo(*, db_session: DbSession, collector_db_session: CollectorDbSession, oh_session_id: UUID, parent_wo_num: Optional[Union[str, list]] = None):
|
async def get_history_oh_wo(*, db_session: DbSession, collector_db_session: CollectorDbSession, oh_session_id: UUID, parent_wo_num: Optional[Union[str, list]]=None):
|
||||||
## Get Parent wo num from oh session table
|
|
||||||
if not parent_wo_num:
|
if not parent_wo_num:
|
||||||
query = select(OverhaulScope.wo_parent).where(OverhaulScope.id == oh_session_id)
|
query = select(OverhaulScope.wo_parent).where(OverhaulScope.id == oh_session_id)
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
parent_wo_num = result.scalar()
|
parent_wo_num = result.scalar()
|
||||||
|
|
||||||
if not parent_wo_num:
|
if not parent_wo_num:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Ensure parent_wo_num is a list and removed duplicates if any
|
|
||||||
if isinstance(parent_wo_num, str):
|
if isinstance(parent_wo_num, str):
|
||||||
parent_wo_num = [parent_wo_num]
|
parent_wo_num = [parent_wo_num]
|
||||||
else:
|
else:
|
||||||
parent_wo_num = list(set(parent_wo_num))
|
parent_wo_num = list(set(parent_wo_num))
|
||||||
|
sql_query = text("\n WITH target_wos AS (\n SELECT \n w.wonum,\n w.assetnum,\n COALESCE(w.actmatcost, 0) as actmatcost,\n COALESCE(w.actservcost, 0) as actservcost\n FROM public.wo_maximo w\n WHERE w.xx_parent = ANY(:parent_wo_num)\n ),\n wo_tasks AS (\n SELECT \n t.xx_parent AS parent_wonum,\n JSON_AGG(t.description) AS task_list\n FROM public.wo_maximo t\n JOIN target_wos tw ON t.xx_parent = tw.wonum\n GROUP BY t.xx_parent\n )\n SELECT \n w.assetnum,\n e.name AS equipment_name,\n e.location_tag,\n JSON_OBJECT_AGG(w.wonum, COALESCE(wt.task_list, '[]'::json)) AS wonum_list,\n COUNT(w.wonum) AS total_wo_count,\n COALESCE(SUM(w.actmatcost), 0) AS total_material_cost,\n COALESCE(SUM(w.actservcost), 0) AS total_service_cost,\n COALESCE(SUM(w.actmatcost + w.actservcost), 0) AS total_actual_cost\n FROM target_wos w\n INNER JOIN public.ms_equipment_master e \n ON w.assetnum = e.assetnum\n LEFT JOIN wo_tasks wt \n ON w.wonum = wt.parent_wonum\n GROUP BY \n w.assetnum, \n e.name, \n e.location_tag\n ORDER BY total_actual_cost DESC;\n ")
|
||||||
sql_query = text("""
|
results = await collector_db_session.execute(sql_query, {'parent_wo_num': parent_wo_num})
|
||||||
WITH target_wos AS (
|
return [{'assetnum': row.assetnum, 'equipment_name': row.equipment_name, 'location_tag': row.location_tag, 'wonum_list': row.wonum_list, 'total_wo_count': row.total_wo_count, 'total_material_cost': float(row.total_material_cost), 'total_service_cost': float(row.total_service_cost), 'total_actual_cost': float(row.total_actual_cost)} for row in results]
|
||||||
SELECT
|
|
||||||
w.wonum,
|
|
||||||
w.assetnum,
|
|
||||||
COALESCE(w.actmatcost, 0) as actmatcost,
|
|
||||||
COALESCE(w.actservcost, 0) as actservcost
|
|
||||||
FROM public.wo_maximo w
|
|
||||||
WHERE w.xx_parent = ANY(:parent_wo_num)
|
|
||||||
),
|
|
||||||
wo_tasks AS (
|
|
||||||
SELECT
|
|
||||||
t.xx_parent AS parent_wonum,
|
|
||||||
JSON_AGG(t.description) AS task_list
|
|
||||||
FROM public.wo_maximo t
|
|
||||||
JOIN target_wos tw ON t.xx_parent = tw.wonum
|
|
||||||
GROUP BY t.xx_parent
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
w.assetnum,
|
|
||||||
e.name AS equipment_name,
|
|
||||||
e.location_tag,
|
|
||||||
JSON_OBJECT_AGG(w.wonum, COALESCE(wt.task_list, '[]'::json)) AS wonum_list,
|
|
||||||
COUNT(w.wonum) AS total_wo_count,
|
|
||||||
COALESCE(SUM(w.actmatcost), 0) AS total_material_cost,
|
|
||||||
COALESCE(SUM(w.actservcost), 0) AS total_service_cost,
|
|
||||||
COALESCE(SUM(w.actmatcost + w.actservcost), 0) AS total_actual_cost
|
|
||||||
FROM target_wos w
|
|
||||||
INNER JOIN public.ms_equipment_master e
|
|
||||||
ON w.assetnum = e.assetnum
|
|
||||||
LEFT JOIN wo_tasks wt
|
|
||||||
ON w.wonum = wt.parent_wonum
|
|
||||||
GROUP BY
|
|
||||||
w.assetnum,
|
|
||||||
e.name,
|
|
||||||
e.location_tag
|
|
||||||
ORDER BY total_actual_cost DESC;
|
|
||||||
""")
|
|
||||||
|
|
||||||
results = await collector_db_session.execute(sql_query, {"parent_wo_num": parent_wo_num})
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"assetnum": row.assetnum,
|
|
||||||
"equipment_name": row.equipment_name,
|
|
||||||
"location_tag": row.location_tag,
|
|
||||||
"wonum_list": row.wonum_list,
|
|
||||||
"total_wo_count": row.total_wo_count,
|
|
||||||
"total_material_cost": float(row.total_material_cost),
|
|
||||||
"total_service_cost": float(row.total_service_cost),
|
|
||||||
"total_actual_cost": float(row.total_actual_cost)
|
|
||||||
}
|
|
||||||
for row in results
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,72 +1,35 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
from fastapi import APIRouter
|
||||||
from fastapi import APIRouter, HTTPException, status
|
|
||||||
|
|
||||||
from src.auth.service import Token
|
from src.auth.service import Token
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.overhaul.service import (get_overhaul_critical_parts,
|
from src.overhaul.service import get_overhaul_critical_parts, get_overhaul_overview, get_overhaul_schedules, get_overhaul_system_components
|
||||||
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 OverhaulCriticalParts, OverhaulRead, OverhaulSystemComponents
|
||||||
from .schema import (OverhaulCriticalParts, OverhaulRead,
|
|
||||||
OverhaulSystemComponents)
|
|
||||||
from src.auth.access_control import required_permission, OHPermission
|
from src.auth.access_control import required_permission, OHPermission
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[OverhaulRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("", response_model=StandardResponse[OverhaulRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
async def get_overhaul(db_session: DbSession, token: Token, collector_db_session: CollectorDbSession):
|
||||||
async def get_overhaul(db_session: DbSession, token:Token, collector_db_session:CollectorDbSession):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
overview = await get_overhaul_overview(db_session=db_session)
|
overview = await get_overhaul_overview(db_session=db_session)
|
||||||
schedules = await get_overhaul_schedules(db_session=db_session)
|
schedules = await get_overhaul_schedules(db_session=db_session)
|
||||||
criticalParts = await get_overhaul_critical_parts(db_session=db_session, session_id=overview["overhaul"]["id"], token=token, collector_db_session=collector_db_session)
|
criticalParts = await get_overhaul_critical_parts(db_session=db_session, session_id=overview['overhaul']['id'], token=token, collector_db_session=collector_db_session)
|
||||||
systemComponents = get_overhaul_system_components()
|
systemComponents = get_overhaul_system_components()
|
||||||
|
return StandardResponse(data=OverhaulRead(overview=overview, schedules=schedules, criticalParts=criticalParts, systemComponents=systemComponents), message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/schedules', response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
data=OverhaulRead(
|
|
||||||
overview=overview,
|
|
||||||
schedules=schedules,
|
|
||||||
criticalParts=criticalParts,
|
|
||||||
systemComponents=systemComponents,
|
|
||||||
),
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schedules", response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_schedules():
|
async def get_schedules():
|
||||||
"""Get all overhaul schedules."""
|
|
||||||
schedules = get_overhaul_schedules()
|
schedules = get_overhaul_schedules()
|
||||||
return StandardResponse(
|
return StandardResponse(data=schedules, message='Data retrieved successfully')
|
||||||
data=schedules,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.get('/critical-parts', response_model=StandardResponse[OverhaulCriticalParts], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("/critical-parts", response_model=StandardResponse[OverhaulCriticalParts], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_critical_parts():
|
async def get_critical_parts():
|
||||||
"""Get all overhaul critical parts."""
|
|
||||||
criticalParts = get_overhaul_critical_parts()
|
criticalParts = get_overhaul_critical_parts()
|
||||||
return StandardResponse(
|
return StandardResponse(data=OverhaulCriticalParts(criticalParts=criticalParts), message='Data retrieved successfully')
|
||||||
data=OverhaulCriticalParts(criticalParts=criticalParts),
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('/system-components', response_model=StandardResponse[OverhaulSystemComponents], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
"/system-components", response_model=StandardResponse[OverhaulSystemComponents],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_system_components():
|
async def get_system_components():
|
||||||
"""Get all overhaul system components."""
|
|
||||||
systemComponents = get_overhaul_system_components()
|
systemComponents = get_overhaul_system_components()
|
||||||
return StandardResponse(
|
return StandardResponse(data=OverhaulSystemComponents(systemComponents=systemComponents), message='Data retrieved successfully')
|
||||||
data=OverhaulSystemComponents(systemComponents=systemComponents),
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,355 +1,51 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from sqlalchemy import Delete, Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.calculation_target_reliability.service import RBD_SERVICE_API
|
from src.calculation_target_reliability.service import RBD_SERVICE_API
|
||||||
from src.config import TC_RBD_ID
|
from src.config import TC_RBD_ID
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.contribution_util import calculate_contribution
|
from src.contribution_util import calculate_contribution
|
||||||
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||||
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
|
|
||||||
from src.overhaul_scope.service import get_overview_overhaul
|
from src.overhaul_scope.service import get_overview_overhaul
|
||||||
from src.standard_scope.service import get_by_oh_session_id
|
|
||||||
|
|
||||||
|
|
||||||
async def get_overhaul_overview(db_session: DbSession):
|
async def get_overhaul_overview(db_session: DbSession):
|
||||||
"""Get all overhaul overview."""
|
return await get_overview_overhaul(db_session=db_session)
|
||||||
results = await get_overview_overhaul(db_session=db_session)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def get_simulation_results(*, simulation_id: str, token: str):
|
async def get_simulation_results(*, simulation_id: str, token: str):
|
||||||
headers = {
|
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
|
||||||
"Authorization": f"Bearer {token}",
|
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode'
|
||||||
"Content-Type": "application/json"
|
calc_plant_result = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/plant'
|
||||||
}
|
|
||||||
|
|
||||||
calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode"
|
|
||||||
# plot_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/plot/{simulation_id}?nodetype=RegularNode"
|
|
||||||
calc_plant_result = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/plant"
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=300.0) as client:
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||||
calc_task = client.get(calc_result_url, headers=headers)
|
calc_task = client.get(calc_result_url, headers=headers)
|
||||||
# plot_task = client.get(plot_result_url, headers=headers)
|
|
||||||
plant_task = client.get(calc_plant_result, headers=headers)
|
plant_task = client.get(calc_plant_result, headers=headers)
|
||||||
|
|
||||||
# Run all three requests concurrently
|
|
||||||
calc_response, plant_response = await asyncio.gather(calc_task, plant_task)
|
calc_response, plant_response = await asyncio.gather(calc_task, plant_task)
|
||||||
|
|
||||||
calc_response.raise_for_status()
|
calc_response.raise_for_status()
|
||||||
# plot_response.raise_for_status()
|
|
||||||
plant_response.raise_for_status()
|
plant_response.raise_for_status()
|
||||||
|
calc_data = calc_response.json()['data']
|
||||||
calc_data = calc_response.json()["data"]
|
plant_data = plant_response.json()['data']
|
||||||
# plot_data = plot_response.json()["data"]
|
return {'calc_result': calc_data, 'plant_result': plant_data}
|
||||||
plant_data = plant_response.json()["data"]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"calc_result": calc_data,
|
|
||||||
# "plot_result": plot_data,
|
|
||||||
"plant_result": plant_data
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_overhaul_critical_parts(db_session, session_id, token, collector_db_session):
|
async def get_overhaul_critical_parts(db_session, session_id, token, collector_db_session):
|
||||||
"""Get all overhaul critical parts."""
|
equipments = await get_standard_scope_by_session_id(db_session=db_session, overhaul_session_id=session_id, collector_db=collector_db_session)
|
||||||
equipments = await get_standard_scope_by_session_id(
|
criticality_simulation = await get_simulation_results(simulation_id=TC_RBD_ID, token=token)
|
||||||
db_session=db_session,
|
rbd_simulation = {asset['aeros_node']['node_name']: {'availability': asset['availability'], 'criticality': asset['criticality']} for asset in criticality_simulation['calc_result']}
|
||||||
overhaul_session_id=session_id,
|
base_result = [{'id': equipment.id, 'location_tag': equipment.location_tag, 'name': equipment.equipment_name, 'matrix': rbd_simulation.get(equipment.location_tag)} for equipment in equipments]
|
||||||
collector_db=collector_db_session
|
filtered_result = [item for item in base_result if item['matrix'] is not None]
|
||||||
)
|
availability_result = sorted(filtered_result, key=lambda x: x['matrix']['availability'])[:10]
|
||||||
|
criticality_result = sorted(filtered_result, key=lambda x: x['matrix']['criticality'], reverse=True)[:10]
|
||||||
|
return {'availability': availability_result, 'criticality': criticality_result}
|
||||||
criticality_simulation = await get_simulation_results(
|
|
||||||
simulation_id = TC_RBD_ID,
|
|
||||||
token=token
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
rbd_simulation = {asset['aeros_node']["node_name"]: {
|
|
||||||
"availability": asset["availability"],
|
|
||||||
"criticality": asset["criticality"]
|
|
||||||
} for asset in criticality_simulation["calc_result"]}
|
|
||||||
|
|
||||||
# Create the base result list
|
|
||||||
base_result = [
|
|
||||||
{
|
|
||||||
"id": equipment.id,
|
|
||||||
"location_tag": equipment.location_tag,
|
|
||||||
"name": equipment.equipment_name,
|
|
||||||
"matrix": rbd_simulation.get(equipment.location_tag)
|
|
||||||
|
|
||||||
} for equipment in equipments
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Filter out items without matrix data (where rbd_simulation.get() returned None)
|
|
||||||
filtered_result = [item for item in base_result if item["matrix"] is not None]
|
|
||||||
|
|
||||||
# Sort by availability (lowest to highest) and limit to 10
|
|
||||||
availability_result = sorted(
|
|
||||||
filtered_result,
|
|
||||||
key=lambda x: x["matrix"]["availability"]
|
|
||||||
)[:10]
|
|
||||||
|
|
||||||
# Sort by criticality (highest to lowest) and limit to 10
|
|
||||||
criticality_result = sorted(
|
|
||||||
filtered_result,
|
|
||||||
key=lambda x: x["matrix"]["criticality"],
|
|
||||||
reverse=True
|
|
||||||
)[:10]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"availability" :availability_result,
|
|
||||||
"criticality": criticality_result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_overhaul_schedules(*, db_session: DbSession):
|
async def get_overhaul_schedules(*, db_session: DbSession):
|
||||||
"""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()
|
||||||
|
|
||||||
|
|
||||||
def get_overhaul_system_components():
|
def get_overhaul_system_components():
|
||||||
"""Get all overhaul system components with dummy data."""
|
powerplant_reliability = {'Plant Control': {'availability': 0.9994866529774127, 'efficiency': 0.9994956204510826, 'total_uptime': 17523.0}, 'SPS': {'availability': 0.9932694501483038, 'efficiency': 0.9810193821516867, 'total_uptime': 17414.000000000062}, 'Turbine': {'availability': 0.9931553730321733, 'efficiency': 0.9666721976783572, 'total_uptime': 17412.000000000062}, 'Generator': {'availability': 0.9934405658224995, 'efficiency': 0.9625424646119242, 'total_uptime': 17417.000000000062}, 'Condensate Water': {'availability': 0.9934405658224995, 'efficiency': 0.9531653517377116, 'total_uptime': 17417.000000000062}, 'Feedwater System': {'availability': 0.9936116814966953, 'efficiency': 0.9687512254370128, 'total_uptime': 17420.000000000062}, 'Cooling Water': {'availability': 0.99355464293863, 'efficiency': 0.9749999189617731, 'total_uptime': 17419.000000000062}, 'SCR': {'availability': 0.9196577686516085, 'efficiency': 0.9996690612127086, 'total_uptime': 17526.0}, 'Ash Handling': {'availability': 0.9931838923112059, 'efficiency': 0.9933669638506657, 'total_uptime': 17412.500000000062}, 'Air Flue Gas': {'availability': 0.9906456764773022, 'efficiency': 0.8565868045084128, 'total_uptime': 17368.000000000062}, 'Boiler': {'availability': 0.9934976043805648, 'efficiency': 0.9693239775686654, 'total_uptime': 17418.000000000062}, 'SAC-IAC': {'availability': 0.9936116814966953, 'efficiency': 0.9878742473840645, 'total_uptime': 17420.000000000062}, 'KLH': {'availability': 0.992185717545064, 'efficiency': 0.9426658166507496, 'total_uptime': 17395.000000000062}, 'CL': {'availability': 0.9984029203741729, 'efficiency': 0.9984546987034779, 'total_uptime': 17504.0}, 'Desalination': {'total_uptime': 17275.500000000062, 'availability': 0.9853696098562663, 'efficiency': 0.9118366404915063}, 'FGD': {'availability': 0.9933835272644342, 'efficiency': 0.9623105228919693, 'total_uptime': 17416.000000000062}, 'CHS': {'availability': 1.0, 'efficiency': 0.9665857756206829, 'total_uptime': 17532.0}, 'SSB': {'availability': 0.9933264887063691, 'efficiency': 0.993508327346586, 'total_uptime': 17415.000000000062}, 'WTP': {'availability': 0.9925849874515208, 'efficiency': 0.9925849874515206, 'total_uptime': 17402.000000000062}}
|
||||||
|
availabilities = {schematic: item['availability'] for schematic, item in powerplant_reliability.items()}
|
||||||
powerplant_reliability = {
|
|
||||||
"Plant Control": {
|
|
||||||
"availability": 0.9994866529774127,
|
|
||||||
"efficiency": 0.9994956204510826,
|
|
||||||
"total_uptime": 17523.0,
|
|
||||||
},
|
|
||||||
"SPS": {
|
|
||||||
"availability": 0.9932694501483038,
|
|
||||||
"efficiency": 0.9810193821516867,
|
|
||||||
"total_uptime": 17414.000000000062,
|
|
||||||
},
|
|
||||||
"Turbine": {
|
|
||||||
"availability": 0.9931553730321733,
|
|
||||||
"efficiency": 0.9666721976783572,
|
|
||||||
"total_uptime": 17412.000000000062,
|
|
||||||
},
|
|
||||||
"Generator": {
|
|
||||||
"availability": 0.9934405658224995,
|
|
||||||
"efficiency": 0.9625424646119242,
|
|
||||||
"total_uptime": 17417.000000000062,
|
|
||||||
},
|
|
||||||
"Condensate Water": {
|
|
||||||
"availability": 0.9934405658224995,
|
|
||||||
"efficiency": 0.9531653517377116,
|
|
||||||
"total_uptime": 17417.000000000062,
|
|
||||||
},
|
|
||||||
"Feedwater System": {
|
|
||||||
"availability": 0.9936116814966953,
|
|
||||||
"efficiency": 0.9687512254370128,
|
|
||||||
"total_uptime": 17420.000000000062,
|
|
||||||
},
|
|
||||||
"Cooling Water": {
|
|
||||||
"availability": 0.99355464293863,
|
|
||||||
"efficiency": 0.9749999189617731,
|
|
||||||
"total_uptime": 17419.000000000062,
|
|
||||||
},
|
|
||||||
"SCR": {
|
|
||||||
"availability": 0.9196577686516085,
|
|
||||||
"efficiency": 0.9996690612127086,
|
|
||||||
"total_uptime": 17526.0,
|
|
||||||
},
|
|
||||||
|
|
||||||
"Ash Handling": {
|
|
||||||
"availability": 0.9931838923112059,
|
|
||||||
"efficiency": 0.9933669638506657,
|
|
||||||
"total_uptime": 17412.500000000062,
|
|
||||||
},
|
|
||||||
"Air Flue Gas": {
|
|
||||||
"availability": 0.9906456764773022,
|
|
||||||
"efficiency": 0.8565868045084128,
|
|
||||||
"total_uptime": 17368.000000000062,
|
|
||||||
},
|
|
||||||
"Boiler": {
|
|
||||||
"availability": 0.9934976043805648,
|
|
||||||
"efficiency": 0.9693239775686654,
|
|
||||||
"total_uptime": 17418.000000000062,
|
|
||||||
},
|
|
||||||
"SAC-IAC": {
|
|
||||||
"availability": 0.9936116814966953,
|
|
||||||
"efficiency": 0.9878742473840645,
|
|
||||||
"total_uptime": 17420.000000000062,
|
|
||||||
},
|
|
||||||
"KLH": {
|
|
||||||
"availability": 0.992185717545064,
|
|
||||||
"efficiency": 0.9426658166507496,
|
|
||||||
"total_uptime": 17395.000000000062,
|
|
||||||
},
|
|
||||||
"CL": {
|
|
||||||
"availability": 0.9984029203741729,
|
|
||||||
"efficiency": 0.9984546987034779,
|
|
||||||
"total_uptime": 17504.0,
|
|
||||||
},
|
|
||||||
"Desalination": {
|
|
||||||
"total_uptime": 17275.500000000062,
|
|
||||||
"availability": 0.9853696098562663,
|
|
||||||
"efficiency": 0.9118366404915063,
|
|
||||||
|
|
||||||
},
|
|
||||||
"FGD": {
|
|
||||||
"availability": 0.9933835272644342,
|
|
||||||
"efficiency": 0.9623105228919693,
|
|
||||||
"total_uptime": 17416.000000000062,
|
|
||||||
},
|
|
||||||
"CHS": {
|
|
||||||
"availability": 1.0,
|
|
||||||
"efficiency": 0.9665857756206829,
|
|
||||||
"total_uptime": 17532.0,
|
|
||||||
},
|
|
||||||
"SSB": {
|
|
||||||
"availability": 0.9933264887063691,
|
|
||||||
"efficiency": 0.993508327346586,
|
|
||||||
"total_uptime": 17415.000000000062,
|
|
||||||
|
|
||||||
},
|
|
||||||
"WTP": {
|
|
||||||
"availability": 0.9925849874515208,
|
|
||||||
"efficiency": 0.9925849874515206,
|
|
||||||
"total_uptime": 17402.000000000062,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
availabilities = {schematic: item['availability'] for schematic, item in powerplant_reliability.items() }
|
|
||||||
|
|
||||||
|
|
||||||
percentages = calculate_contribution(availabilities)
|
percentages = calculate_contribution(availabilities)
|
||||||
|
|
||||||
for schema, contribution in percentages.items():
|
for schema, contribution in percentages.items():
|
||||||
powerplant_reliability[schema]["critical_contribution"] = contribution['criticality_importance']
|
powerplant_reliability[schema]['critical_contribution'] = contribution['criticality_importance']
|
||||||
|
return dict(sorted(powerplant_reliability.items(), key=lambda x: x[1]['critical_contribution'], reverse=True))
|
||||||
# Sort the powerplant_reliability dictionary by critical_contribution in descending order
|
return {'HPT': {'efficiency': '92%', 'work_hours': '1200', 'reliability': '96%'}, 'IPT': {'efficiency': '91%', 'work_hours': '1100', 'reliability': '95%'}, 'LPT': {'efficiency': '90%', 'work_hours': '1000', 'reliability': '94%'}, 'EG': {'efficiency': '88%', 'work_hours': '950', 'reliability': '93%'}, 'boiler': {'efficiency': '90%', 'work_hours': '1000', 'reliability': '95%'}, 'HPH1': {'efficiency': '89%', 'work_hours': '1050', 'reliability': '94%'}, 'HPH2': {'efficiency': '88%', 'work_hours': '1020', 'reliability': '93%'}, 'HPH3': {'efficiency': '87%', 'work_hours': '1010', 'reliability': '92%'}, 'HPH5': {'efficiency': '86%', 'work_hours': '980', 'reliability': '91%'}, 'HPH6': {'efficiency': '85%', 'work_hours': '970', 'reliability': '90%'}, 'HPH7': {'efficiency': '84%', 'work_hours': '960', 'reliability': '89%'}, 'Condensor': {'efficiency': '83%', 'work_hours': '940', 'reliability': '88%'}, 'Deaerator': {'efficiency': '82%', 'work_hours': '930', 'reliability': '87%'}}
|
||||||
sorted_powerplant_reliability = dict(sorted(
|
|
||||||
powerplant_reliability.items(),
|
|
||||||
key=lambda x: x[1]["critical_contribution"],
|
|
||||||
reverse=True # Set to True for high to low sorting
|
|
||||||
))
|
|
||||||
|
|
||||||
return sorted_powerplant_reliability
|
|
||||||
|
|
||||||
return {
|
|
||||||
"HPT": {
|
|
||||||
"efficiency": "92%",
|
|
||||||
"work_hours": "1200",
|
|
||||||
"reliability": "96%",
|
|
||||||
},
|
|
||||||
"IPT": {
|
|
||||||
"efficiency": "91%",
|
|
||||||
"work_hours": "1100",
|
|
||||||
"reliability": "95%",
|
|
||||||
},
|
|
||||||
"LPT": {
|
|
||||||
"efficiency": "90%",
|
|
||||||
"work_hours": "1000",
|
|
||||||
"reliability": "94%",
|
|
||||||
},
|
|
||||||
"EG": {
|
|
||||||
"efficiency": "88%",
|
|
||||||
"work_hours": "950",
|
|
||||||
"reliability": "93%",
|
|
||||||
},
|
|
||||||
"boiler": {
|
|
||||||
"efficiency": "90%",
|
|
||||||
"work_hours": "1000",
|
|
||||||
"reliability": "95%",
|
|
||||||
},
|
|
||||||
"HPH1": {
|
|
||||||
"efficiency": "89%",
|
|
||||||
"work_hours": "1050",
|
|
||||||
"reliability": "94%",
|
|
||||||
},
|
|
||||||
"HPH2": {
|
|
||||||
"efficiency": "88%",
|
|
||||||
"work_hours": "1020",
|
|
||||||
"reliability": "93%",
|
|
||||||
},
|
|
||||||
"HPH3": {
|
|
||||||
"efficiency": "87%",
|
|
||||||
"work_hours": "1010",
|
|
||||||
"reliability": "92%",
|
|
||||||
},
|
|
||||||
"HPH5": {
|
|
||||||
"efficiency": "86%",
|
|
||||||
"work_hours": "980",
|
|
||||||
"reliability": "91%",
|
|
||||||
},
|
|
||||||
"HPH6": {
|
|
||||||
"efficiency": "85%",
|
|
||||||
"work_hours": "970",
|
|
||||||
"reliability": "90%",
|
|
||||||
},
|
|
||||||
"HPH7": {
|
|
||||||
"efficiency": "84%",
|
|
||||||
"work_hours": "960",
|
|
||||||
"reliability": "89%",
|
|
||||||
},
|
|
||||||
"Condensor": {
|
|
||||||
"efficiency": "83%",
|
|
||||||
"work_hours": "940",
|
|
||||||
"reliability": "88%",
|
|
||||||
},
|
|
||||||
"Deaerator": {
|
|
||||||
"efficiency": "82%",
|
|
||||||
"work_hours": "930",
|
|
||||||
"reliability": "87%",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def get(*, db_session: DbSession, scope_id: str) -> Optional[Scope]:
|
|
||||||
# """Returns a document based on the given document id."""
|
|
||||||
# query = Select(Scope).filter(Scope.id == scope_id)
|
|
||||||
# result = await db_session.execute(query)
|
|
||||||
# return result.scalars().one_or_none()
|
|
||||||
|
|
||||||
|
|
||||||
# async def get_all(*, db_session: DbSession):
|
|
||||||
# """Returns all documents."""
|
|
||||||
# query = Select(Scope)
|
|
||||||
# result = await db_session.execute(query)
|
|
||||||
# return result.scalars().all()
|
|
||||||
|
|
||||||
|
|
||||||
# async def create(*, db_session: DbSession, scope_id: ScopeCreate):
|
|
||||||
# """Creates a new document."""
|
|
||||||
# scope = Scope(**scope_id.model_dump())
|
|
||||||
# db_session.add(scope)
|
|
||||||
# await db_session.commit()
|
|
||||||
# return scope
|
|
||||||
|
|
||||||
|
|
||||||
# async def update(*, db_session: DbSession, scope: Scope, scope_id: ScopeUpdate):
|
|
||||||
# """Updates a document."""
|
|
||||||
# data = scope_id.model_dump()
|
|
||||||
|
|
||||||
# update_data = scope_id.model_dump(exclude_defaults=True)
|
|
||||||
|
|
||||||
# for field in data:
|
|
||||||
# if field in update_data:
|
|
||||||
# setattr(scope, field, update_data[field])
|
|
||||||
|
|
||||||
# await db_session.commit()
|
|
||||||
|
|
||||||
# return scope
|
|
||||||
|
|
||||||
|
|
||||||
# async def delete(*, db_session: DbSession, scope_id: str):
|
|
||||||
# """Deletes a document."""
|
|
||||||
# query = Delete(Scope).where(Scope.id == scope_id)
|
|
||||||
# await db_session.execute(query)
|
|
||||||
# await db_session.commit()
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,43 +1,11 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
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
|
||||||
from src.workorder.model import MasterWorkOrder
|
|
||||||
|
|
||||||
|
|
||||||
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(
|
overhaul_scope_id = Column(UUID(as_uuid=True), ForeignKey('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')
|
||||||
|
|
||||||
# equipment = relationship(
|
|
||||||
# "MasterEquipment",
|
|
||||||
# lazy="raise",
|
|
||||||
# primaryjoin="and_(OverhaulActivity.assetnum == foreign(MasterEquipment.assetnum))",
|
|
||||||
# uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # sparepart_equipments = relationship(
|
|
||||||
# # "ScopeEquipmentPart",
|
|
||||||
# # lazy="select", # or "joined", "subquery", "dynamic" depending on your needs
|
|
||||||
# # primaryjoin="OverhaulActivity.assetnum == foreign(ScopeEquipmentPart.assetnum)",
|
|
||||||
# # uselist=True
|
|
||||||
# # )
|
|
||||||
|
|
||||||
|
|
||||||
# overhaul_scope = relationship(
|
|
||||||
# "OverhaulScope",
|
|
||||||
# lazy="raise",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# overhaul_jobs = relationship(
|
|
||||||
# "OverhaulJob", back_populates="overhaul_activity", lazy="raise"
|
|
||||||
# )
|
|
||||||
|
|||||||
@ -1,119 +1,33 @@
|
|||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.database.service import (CommonParameters, DbSession,
|
from src.database.service import CommonParameters, DbSession
|
||||||
search_filter_sort_paginate)
|
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
|
from .schema import OverhaulActivityCreate, OverhaulActivityRead
|
||||||
from .schema import (OverhaulActivityCreate, OverhaulActivityPagination,
|
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session
|
||||||
OverhaulActivityRead, OverhaulActivityUpdate)
|
|
||||||
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session, update
|
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('/{overhaul_session}', response_model=StandardResponse[dict])
|
||||||
|
async def get_scope_equipments(common: CommonParameters, overhaul_session: str, collector_db: CollectorDbSession, location_tag: Optional[str]=Query(None), scope_name: Optional[str]=Query(None)):
|
||||||
|
data = await get_all(common=common, location_tag=location_tag, scope_name=scope_name, overhaul_session_id=overhaul_session, collector_db=collector_db)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
|
|
||||||
@router.get(
|
@router.post('/{overhaul_session_id}', response_model=StandardResponse[None])
|
||||||
"/{overhaul_session}", response_model=StandardResponse[dict]
|
async def create_overhaul_equipment(db_session: DbSession, collector_db_session: CollectorDbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID):
|
||||||
)
|
await add_multiple_equipment_to_session(db_session=db_session, collector_db=collector_db_session, overhaul_session_id=overhaul_session_id, location_tags=overhaul_activty_in.location_tags)
|
||||||
async def get_scope_equipments(
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
common: CommonParameters,
|
|
||||||
overhaul_session: str,
|
|
||||||
collector_db: CollectorDbSession,
|
|
||||||
location_tag: Optional[str] = Query(None),
|
|
||||||
scope_name: Optional[str] = Query(None),
|
|
||||||
):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
# return
|
|
||||||
data = await get_all(
|
|
||||||
common=common,
|
|
||||||
location_tag=location_tag,
|
|
||||||
scope_name=scope_name,
|
|
||||||
overhaul_session_id=overhaul_session,
|
|
||||||
collector_db=collector_db,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=data,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{overhaul_session_id}", response_model=StandardResponse[None])
|
|
||||||
async def create_overhaul_equipment(
|
|
||||||
db_session: DbSession,
|
|
||||||
collector_db_session: CollectorDbSession,
|
|
||||||
overhaul_activty_in: OverhaulActivityCreate,
|
|
||||||
overhaul_session_id: UUID,
|
|
||||||
):
|
|
||||||
|
|
||||||
activity = await add_multiple_equipment_to_session(
|
|
||||||
db_session=db_session,
|
|
||||||
collector_db=collector_db_session,
|
|
||||||
overhaul_session_id=overhaul_session_id,
|
|
||||||
location_tags=overhaul_activty_in.location_tags
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=None, message="Data created successfully")
|
@router.get('/{overhaul_session}/{assetnum}', 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)
|
||||||
@router.get(
|
|
||||||
"/{overhaul_session}/{assetnum}",
|
|
||||||
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, detail='A data with this id does not exist.')
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
return StandardResponse(data=equipment, message='Data retrieved successfully')
|
||||||
detail="A data with this id does not exist.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=equipment, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.put(
|
|
||||||
# "/{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)
|
|
||||||
|
|
||||||
# if not activity:
|
@router.post('/delete/{overhaul_session}/{location_tag}', response_model=StandardResponse[None], dependencies=[Depends(csrf_protect)])
|
||||||
# raise HTTPException(
|
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session: UUID):
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# 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",
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/delete/{overhaul_session}/{location_tag}",
|
|
||||||
response_model=StandardResponse[None],
|
|
||||||
dependencies=[Depends(csrf_protect)],
|
|
||||||
)
|
|
||||||
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session:UUID):
|
|
||||||
await remove_equipment_from_session(db_session=db_session, overhaul_session_id=overhaul_session, location_tag=location_tag)
|
await remove_equipment_from_session(db_session=db_session, overhaul_session_id=overhaul_session, location_tag=location_tag)
|
||||||
|
return StandardResponse(message='Data deleted successfully', data=None)
|
||||||
return StandardResponse(message="Data deleted successfully", data=None)
|
|
||||||
|
|||||||
@ -1,37 +1,24 @@
|
|||||||
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)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
if not total_equipment: # Guard against division by zero
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|
||||||
cost = 365539731101 / 10
|
cost = 365539731101 / 10
|
||||||
|
if scope == 'B':
|
||||||
if scope == "B":
|
result = Decimal(f'{cost}') / Decimal(str(total_equipment))
|
||||||
result = Decimal(f"{cost}") / Decimal(str(total_equipment))
|
|
||||||
return float(result)
|
|
||||||
else:
|
|
||||||
result = Decimal("8565468127") / Decimal(str(total_equipment))
|
|
||||||
return float(result)
|
return float(result)
|
||||||
|
result = Decimal('8565468127') / Decimal(str(total_equipment))
|
||||||
|
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)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
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:
|
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)
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,17 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy import Column, String
|
from sqlalchemy import Column, String
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class OverhaulGantt(Base, DefaultMixin):
|
class OverhaulGantt(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_monitoring_spreadsheet"
|
__tablename__ = 'oh_ms_monitoring_spreadsheet'
|
||||||
|
|
||||||
spreadsheet_id = Column(String, nullable=True)
|
spreadsheet_id = Column(String, nullable=True)
|
||||||
spreadsheet_link = Column(String, nullable=True)
|
spreadsheet_link = Column(String, nullable=True)
|
||||||
|
|
||||||
@ -1,143 +1,46 @@
|
|||||||
import re
|
import re
|
||||||
from typing import List, Optional
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from src.auth.service import Admin
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import CommonParameters
|
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.overhaul_gantt.model import OverhaulGantt
|
from src.overhaul_gantt.model import OverhaulGantt
|
||||||
from src.overhaul_gantt.schema import OverhaulGanttIn
|
from src.overhaul_gantt.schema import OverhaulGanttIn
|
||||||
|
|
||||||
# from .schema import (OverhaulScheduleCreate, OverhaulSchedulePagination, OverhaulScheduleUpdate)
|
|
||||||
from .service import get_gantt_performance_chart
|
from .service import get_gantt_performance_chart
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[dict])
|
||||||
@router.get(
|
|
||||||
"", response_model=StandardResponse[dict]
|
|
||||||
)
|
|
||||||
async def get_gantt_performance(db_session: DbSession):
|
async def get_gantt_performance(db_session: DbSession):
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
query = select(OverhaulGantt).limit(1)
|
query = select(OverhaulGantt).limit(1)
|
||||||
|
|
||||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
results, gantt_data = await get_gantt_performance_chart(spreadsheet_id=data.spreadsheet_id)
|
results, gantt_data = await get_gantt_performance_chart(spreadsheet_id=data.spreadsheet_id)
|
||||||
|
return StandardResponse(data={'chart_data': results, 'gantt_data': gantt_data}, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/spreadsheet', response_model=StandardResponse[dict])
|
||||||
data={
|
|
||||||
"chart_data": results,
|
|
||||||
"gantt_data": gantt_data
|
|
||||||
},
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/spreadsheet", response_model=StandardResponse[dict]
|
|
||||||
)
|
|
||||||
async def get_gantt_spreadsheet(db_session: DbSession):
|
async def get_gantt_spreadsheet(db_session: DbSession):
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
query = select(OverhaulGantt).limit(1)
|
query = select(OverhaulGantt).limit(1)
|
||||||
|
|
||||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||||
result = {
|
result = {'spreadsheet_id': None, 'spreadsheet_link': None}
|
||||||
"spreadsheet_id": None,
|
|
||||||
"spreadsheet_link": None
|
|
||||||
}
|
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
result = {
|
result = {'spreadsheet_id': data.spreadsheet_id, 'spreadsheet_link': data.spreadsheet_link}
|
||||||
"spreadsheet_id": data.spreadsheet_id,
|
return StandardResponse(data=result, message='Data retrieved successfully')
|
||||||
"spreadsheet_link": data.spreadsheet_link
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=result,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post(
|
@router.post('/spreadsheet', response_model=StandardResponse[dict], dependencies=[Depends(csrf_protect)])
|
||||||
"/spreadsheet", response_model=StandardResponse[dict], dependencies=[Depends(csrf_protect)]
|
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn, admin: Admin):
|
||||||
)
|
match = re.search('/d/([a-zA-Z0-9-_]+)', spreadsheet_in.spreadsheet_link)
|
||||||
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
|
|
||||||
match = re.search(r"/d/([a-zA-Z0-9-_]+)", spreadsheet_in.spreadsheet_link)
|
|
||||||
if not match:
|
if not match:
|
||||||
raise ValueError("Invalid Google Sheets URL")
|
raise ValueError('Invalid Google Sheets URL')
|
||||||
|
|
||||||
spreadsheet_id = match.group(1)
|
spreadsheet_id = match.group(1)
|
||||||
|
|
||||||
query = select(OverhaulGantt).limit(1)
|
query = select(OverhaulGantt).limit(1)
|
||||||
|
|
||||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||||
if data:
|
if data:
|
||||||
data.spreadsheet_link = spreadsheet_in.spreadsheet_link
|
data.spreadsheet_link = spreadsheet_in.spreadsheet_link
|
||||||
data.spreadsheet_id = spreadsheet_id
|
data.spreadsheet_id = spreadsheet_id
|
||||||
else:
|
else:
|
||||||
spreadsheet = OverhaulGantt(
|
spreadsheet = OverhaulGantt(spreadsheet_id=spreadsheet_id, spreadsheet_link=spreadsheet_in.spreadsheet_link)
|
||||||
spreadsheet_id=spreadsheet_id,
|
|
||||||
spreadsheet_link=spreadsheet_in.spreadsheet_link
|
|
||||||
)
|
|
||||||
db_session.add(spreadsheet)
|
db_session.add(spreadsheet)
|
||||||
|
|
||||||
|
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
result = {
|
result = {'spreadsheet_id': spreadsheet_id}
|
||||||
"spreadsheet_id": spreadsheet_id
|
return StandardResponse(data=result, message='Data retrieved successfully')
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=result,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.post("", response_model=StandardResponse[None])
|
|
||||||
# async def create_overhaul_equipment_jobs(
|
|
||||||
# db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
# ):
|
|
||||||
# await create(
|
|
||||||
# db_session=db_session,
|
|
||||||
# overhaul_job_in=overhaul_job_in,
|
|
||||||
# )
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# data=None,
|
|
||||||
# message="Data created successfully",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# @router.put("/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
# async def update_overhaul_schedule(
|
|
||||||
# db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate
|
|
||||||
# ):
|
|
||||||
# await update(db_session=db_session, overhaul_schedule_id=overhaul_job_id, overhaul_job_in=overhaul_job_in)
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# data=None,
|
|
||||||
# message="Data updated successfully",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# @router.delete("/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
# async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
|
||||||
# await delete(db_session=db_session, overhaul_schedule_id=overhaul_job_id)
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# data=None,
|
|
||||||
# message="Data deleted successfully",
|
|
||||||
# )
|
|
||||||
|
|||||||
@ -1,112 +1,34 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import Delete, Select, func
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
# from .model import OverhaulSchedule
|
|
||||||
# from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
|
|
||||||
from .utils import fetch_all_sections, get_google_creds, get_spreatsheed_service, process_spreadsheet_data
|
from .utils import fetch_all_sections, get_google_creds, get_spreatsheed_service, process_spreadsheet_data
|
||||||
|
|
||||||
# async def get_all(*, common):
|
async def get_gantt_performance_chart(*, spreadsheet_id='1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk'):
|
||||||
# """Returns all documents."""
|
|
||||||
# query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
|
|
||||||
|
|
||||||
# results = await search_filter_sort_paginate(model=query, **common)
|
|
||||||
# return results
|
|
||||||
|
|
||||||
|
|
||||||
# async def create(
|
|
||||||
# *, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
# ):
|
|
||||||
|
|
||||||
|
|
||||||
# schedule = OverhaulSchedule(**overhaul_job_in.model_dump())
|
|
||||||
# db_session.add(schedule)
|
|
||||||
# await db_session.commit()
|
|
||||||
# return schedule
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
|
||||||
# """Updates a document."""
|
|
||||||
# data = overhaul_job_in.model_dump()
|
|
||||||
# overhaul_schedule = await db_session.get(OverhaulSchedule, overhaul_schedule_id)
|
|
||||||
|
|
||||||
# update_data = overhaul_job_in.model_dump(exclude_defaults=True)
|
|
||||||
|
|
||||||
# for field in data:
|
|
||||||
# if field in update_data:
|
|
||||||
# setattr(overhaul_schedule, field, update_data[field])
|
|
||||||
|
|
||||||
# await db_session.commit()
|
|
||||||
|
|
||||||
# return overhaul_schedule
|
|
||||||
|
|
||||||
|
|
||||||
# async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
|
||||||
# """Deletes a document."""
|
|
||||||
# query = Delete(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
|
|
||||||
# await db_session.execute(query)
|
|
||||||
# await db_session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_gantt_performance_chart(*, spreadsheet_id = "1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk"):
|
|
||||||
creds = get_google_creds()
|
creds = get_google_creds()
|
||||||
RANGE_NAME = "'SUMMARY'!K34:AZ38" # Or just "2024 schedule"
|
RANGE_NAME = "'SUMMARY'!K34:AZ38"
|
||||||
GANTT_DATA_NAME = "ACTUAL PROGRESS"
|
GANTT_DATA_NAME = 'ACTUAL PROGRESS'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
service = get_spreatsheed_service(creds)
|
service = get_spreatsheed_service(creds)
|
||||||
sheet = service.spreadsheets()
|
sheet = service.spreadsheets()
|
||||||
|
response = sheet.values().get(spreadsheetId=spreadsheet_id, range=RANGE_NAME).execute()
|
||||||
response = sheet.values().get(
|
values = response.get('values', [])
|
||||||
spreadsheetId=spreadsheet_id,
|
|
||||||
range=RANGE_NAME
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
values = response.get("values", [])
|
|
||||||
|
|
||||||
if len(values) < 4:
|
if len(values) < 4:
|
||||||
raise Exception("Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).")
|
raise Exception('Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).')
|
||||||
|
day_row = values[0][1:]
|
||||||
# Extract rows
|
date_row = values[1][1:]
|
||||||
day_row = values[0][1:]
|
plan_row = values[3][1:]
|
||||||
date_row = values[1][1:]
|
|
||||||
plan_row = values[3][1:]
|
|
||||||
actual_row = values[4][1:]
|
actual_row = values[4][1:]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
total_days = len(day_row)
|
total_days = len(day_row)
|
||||||
|
date_row += [''] * (total_days - len(date_row))
|
||||||
# PAD rows so lengths match day count
|
plan_row += [''] * (total_days - len(plan_row))
|
||||||
date_row += [""] * (total_days - len(date_row))
|
actual_row += [''] * (total_days - len(actual_row))
|
||||||
plan_row += [""] * (total_days - len(plan_row))
|
|
||||||
actual_row += [""] * (total_days - len(actual_row))
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for i in range(total_days):
|
for i in range(total_days):
|
||||||
|
|
||||||
day = day_row[i]
|
day = day_row[i]
|
||||||
date = date_row[i]
|
date = date_row[i]
|
||||||
plan = plan_row[i]
|
plan = plan_row[i]
|
||||||
actual = actual_row[i] if actual_row[i] else "0%" # <-- FIX HERE
|
actual = actual_row[i] if actual_row[i] else '0%'
|
||||||
|
results.append({'day': day, 'date': date, 'plan': plan, 'actual': actual})
|
||||||
results.append({
|
|
||||||
"day": day,
|
|
||||||
"date": date,
|
|
||||||
"plan": plan,
|
|
||||||
"actual": actual
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
processed_data = process_spreadsheet_data(results)
|
processed_data = process_spreadsheet_data(results)
|
||||||
gantt_data = fetch_all_sections(service=service, spreadsheet_id=spreadsheet_id, sheet_name=GANTT_DATA_NAME)
|
gantt_data = fetch_all_sections(service=service, spreadsheet_id=spreadsheet_id, sheet_name=GANTT_DATA_NAME)
|
||||||
|
return (processed_data, gantt_data)
|
||||||
|
|
||||||
return processed_data, gantt_data
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,91 +1,23 @@
|
|||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Query
|
||||||
from fastapi import APIRouter, HTTPException, status, Query
|
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import CommonParameters
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
from .schema import OverhaulJobCreate, OverhaulJobPagination
|
||||||
from .schema import (OverhaulJobBase, OverhaulJobCreate, OverhaulJobPagination,
|
|
||||||
OverhaulJobRead)
|
|
||||||
from .service import create, delete, get_all
|
from .service import create, delete, get_all
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('/{location_tag}', response_model=StandardResponse[OverhaulJobPagination])
|
||||||
@router.get(
|
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
||||||
"/{location_tag}", response_model=StandardResponse[OverhaulJobPagination]
|
|
||||||
)
|
|
||||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str] = Query(None)):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/{overhaul_equipment_id}', response_model=StandardResponse[None])
|
||||||
data=results,
|
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate):
|
||||||
message="Data retrieved successfully",
|
await create(db_session=db_session, overhaul_equipment_id=overhaul_equipment_id, overhaul_job_in=overhaul_job_in)
|
||||||
)
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{overhaul_equipment_id}", response_model=StandardResponse[None])
|
@router.post('/delete/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||||
async def create_overhaul_equipment_jobs(
|
|
||||||
db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
|
|
||||||
):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
# return
|
|
||||||
await create(
|
|
||||||
db_session=db_session,
|
|
||||||
overhaul_equipment_id=overhaul_equipment_id,
|
|
||||||
overhaul_job_in=overhaul_job_in,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data created successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
||||||
await delete(db_session=db_session, overhaul_job_id=overhaul_job_id)
|
await delete(db_session=db_session, overhaul_job_id=overhaul_job_id)
|
||||||
|
return StandardResponse(data=None, message='Data deleted successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data deleted successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# @router.post("", response_model=StandardResponse[List[str]])
|
|
||||||
# async def create_scope(db_session: DbSession, scope_in: OverhaulJobCreate):
|
|
||||||
# overhaul_job = await create(db_session=db_session, scope_in=scope_in)
|
|
||||||
|
|
||||||
# return StandardResponse(data=overhaul_job, message="Data created successfully")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.put("/{scope_id}", response_model=StandardResponse[ScopeRead])
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# if not scope:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.delete("/{scope_id}", response_model=StandardResponse[ScopeRead])
|
|
||||||
# async def delete_scope(db_session: DbSession, scope_id: str):
|
|
||||||
# scope = await get(db_session=db_session, scope_id=scope_id)
|
|
||||||
|
|
||||||
# if not scope:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# detail=[{"msg": "A data with this id does not exist."}],
|
|
||||||
# )
|
|
||||||
|
|
||||||
# await delete(db_session=db_session, scope_id=scope_id)
|
|
||||||
|
|
||||||
# return StandardResponse(message="Data deleted successfully", data=scope)
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,18 +1,13 @@
|
|||||||
from sqlalchemy import (UUID, Column, DateTime, Float, ForeignKey, Integer,
|
from sqlalchemy import Column, DateTime, Integer, String
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class OverhaulSchedule(Base, DefaultMixin):
|
class OverhaulSchedule(Base, DefaultMixin):
|
||||||
__tablename__ = "rp_oh_schedule"
|
__tablename__ = 'rp_oh_schedule'
|
||||||
|
|
||||||
year = Column(Integer, nullable=False)
|
year = Column(Integer, nullable=False)
|
||||||
plan_duration = Column(Integer, nullable=True)
|
plan_duration = Column(Integer, nullable=True)
|
||||||
planned_outage = Column(Integer, nullable=True)
|
planned_outage = Column(Integer, nullable=True)
|
||||||
actual_shutdown = Column(Integer, nullable=True)
|
actual_shutdown = Column(Integer, nullable=True)
|
||||||
start = Column(DateTime(timezone=True)) # This will be TIMESTAMP WITH TIME ZONE
|
start = Column(DateTime(timezone=True))
|
||||||
finish = Column(DateTime(timezone=True))
|
finish = Column(DateTime(timezone=True))
|
||||||
remark = Column(String, nullable=True)
|
remark = Column(String, nullable=True)
|
||||||
|
|||||||
@ -1,63 +1,27 @@
|
|||||||
from typing import List, Optional
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import CommonParameters
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
from .schema import OverhaulScheduleCreate, OverhaulSchedulePagination, OverhaulScheduleUpdate
|
||||||
from .schema import (OverhaulScheduleCreate, OverhaulSchedulePagination, OverhaulScheduleUpdate)
|
|
||||||
from .service import create, get_all, delete, update
|
from .service import create, get_all, delete, update
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[OverhaulSchedulePagination])
|
||||||
@router.get(
|
|
||||||
"", response_model=StandardResponse[OverhaulSchedulePagination]
|
|
||||||
)
|
|
||||||
async def get_schedules(common: CommonParameters):
|
async def get_schedules(common: CommonParameters):
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
results = await get_all(common=common)
|
results = await get_all(common=common)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
|
@router.post('', response_model=StandardResponse[None])
|
||||||
|
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate):
|
||||||
|
await create(db_session=db_session, overhaul_job_in=overhaul_job_in)
|
||||||
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/update/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||||
data=results,
|
async def update_overhaul_schedule(db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[None])
|
|
||||||
async def create_overhaul_equipment_jobs(
|
|
||||||
db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
):
|
|
||||||
await create(
|
|
||||||
db_session=db_session,
|
|
||||||
overhaul_job_in=overhaul_job_in,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data created successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/update/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def update_overhaul_schedule(
|
|
||||||
db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate
|
|
||||||
):
|
|
||||||
await update(db_session=db_session, overhaul_schedule_id=overhaul_job_id, overhaul_job_in=overhaul_job_in)
|
await update(db_session=db_session, overhaul_schedule_id=overhaul_job_id, overhaul_job_in=overhaul_job_in)
|
||||||
|
return StandardResponse(data=None, message='Data updated successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/delete/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||||
data=None,
|
|
||||||
message="Data updated successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/delete/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
||||||
await delete(db_session=db_session, overhaul_schedule_id=overhaul_job_id)
|
await delete(db_session=db_session, overhaul_schedule_id=overhaul_job_id)
|
||||||
|
return StandardResponse(data=None, message='Data deleted successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data deleted successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,60 +1,33 @@
|
|||||||
from typing import Optional
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
|
||||||
from sqlalchemy import Delete, Select, func
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.database.core import DbSession
|
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.utils import update_model
|
from src.utils import update_model
|
||||||
from src.scope_equipment_job.model import ScopeEquipmentJob
|
|
||||||
from src.overhaul_activity.model import OverhaulActivity
|
|
||||||
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
|
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
|
||||||
|
|
||||||
from .model import OverhaulSchedule
|
from .model import OverhaulSchedule
|
||||||
from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
|
from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
|
||||||
|
|
||||||
|
|
||||||
async def get_all(*, common):
|
async def get_all(*, common):
|
||||||
"""Returns all documents."""
|
|
||||||
query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
|
query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
|
||||||
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
||||||
|
return await search_filter_sort_paginate(model=query, **common)
|
||||||
|
|
||||||
results = await search_filter_sort_paginate(model=query, **common)
|
async def create(*, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate):
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
*, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
):
|
|
||||||
|
|
||||||
|
|
||||||
schedule = OverhaulSchedule(**overhaul_job_in.model_dump())
|
schedule = OverhaulSchedule(**overhaul_job_in.model_dump())
|
||||||
db_session.add(schedule)
|
db_session.add(schedule)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
||||||
"""Updates a document."""
|
overhaul_job_in.model_dump()
|
||||||
data = overhaul_job_in.model_dump()
|
|
||||||
query = Select(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
|
query = Select(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
|
||||||
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
||||||
query = query.with_for_update()
|
query = query.with_for_update()
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
overhaul_schedule = result.scalars().one_or_none()
|
overhaul_schedule = result.scalars().one_or_none()
|
||||||
|
|
||||||
update_data = overhaul_job_in.model_dump(exclude_defaults=True)
|
update_data = overhaul_job_in.model_dump(exclude_defaults=True)
|
||||||
|
|
||||||
update_model(overhaul_schedule, update_data)
|
update_model(overhaul_schedule, update_data)
|
||||||
|
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
return overhaul_schedule
|
return overhaul_schedule
|
||||||
|
|
||||||
|
|
||||||
async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
||||||
"""Soft-deletes a document."""
|
|
||||||
await soft_delete_record(db_session=db_session, model=OverhaulSchedule, record_id=overhaul_schedule_id)
|
await soft_delete_record(db_session=db_session, model=OverhaulSchedule, record_id=overhaul_schedule_id)
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,48 +1,29 @@
|
|||||||
from sqlalchemy import JSON
|
from sqlalchemy import JSON
|
||||||
from sqlalchemy import Column, DateTime , Integer, String, ForeignKey, UUID
|
from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UUID
|
||||||
from sqlalchemy.orm import relationship
|
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
|
||||||
|
|
||||||
|
|
||||||
from src.auth.access_control import Allow, RolePrincipal, OHPermission
|
from src.auth.access_control import Allow, RolePrincipal, OHPermission
|
||||||
|
|
||||||
class OverhaulScope(Base, DefaultMixin):
|
class OverhaulScope(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_overhaul"
|
__tablename__ = 'oh_ms_overhaul'
|
||||||
start_date = Column(DateTime(timezone=True), nullable=False) # Made non-nullable to match model
|
start_date = Column(DateTime(timezone=True), nullable=False)
|
||||||
end_date = Column(DateTime(timezone=True), nullable=True) # Already nullable
|
end_date = Column(DateTime(timezone=True), nullable=True)
|
||||||
duration_oh = Column(Integer, nullable=True)
|
duration_oh = Column(Integer, nullable=True)
|
||||||
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')
|
||||||
maintenance_type_id = Column(
|
maintenance_type_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_maintenance_type.id'), nullable=False)
|
||||||
UUID(as_uuid=True), ForeignKey("oh_ms_maintenance_type.id"), nullable=False)
|
|
||||||
wo_parent = Column(JSON, nullable=True)
|
wo_parent = Column(JSON, nullable=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __acl__(self):
|
def __acl__(self):
|
||||||
basic_permissions = [OHPermission.READ]
|
basic_permissions = [OHPermission.READ]
|
||||||
engineer_permissions = [
|
engineer_permissions = [OHPermission.READ, OHPermission.CREATE, OHPermission.EDIT, OHPermission.DELETE]
|
||||||
OHPermission.READ,
|
|
||||||
OHPermission.CREATE,
|
|
||||||
OHPermission.EDIT,
|
|
||||||
OHPermission.DELETE,
|
|
||||||
]
|
|
||||||
all_permissions = list(OHPermission)
|
all_permissions = list(OHPermission)
|
||||||
|
return [(Allow, RolePrincipal('Management'), basic_permissions), (Allow, RolePrincipal('Engineer'), engineer_permissions), (Allow, RolePrincipal('Admin'), all_permissions), (Allow, RolePrincipal('Application Administrator'), all_permissions)]
|
||||||
return [
|
maintenance_type = relationship('MaintenanceType', lazy='selectin', backref='overhaul_scopes')
|
||||||
(Allow, RolePrincipal("Management"), basic_permissions),
|
|
||||||
(Allow, RolePrincipal("Engineer"), engineer_permissions),
|
|
||||||
(Allow, RolePrincipal("Admin"), all_permissions),
|
|
||||||
(Allow, RolePrincipal("Application Administrator"), all_permissions),
|
|
||||||
]
|
|
||||||
|
|
||||||
maintenance_type = relationship("MaintenanceType", lazy="selectin", backref="overhaul_scopes")
|
|
||||||
|
|
||||||
# activity_equipments = relationship("OverhaulActivity", lazy="selectin")
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceType(Base, DefaultMixin):
|
class MaintenanceType(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_maintenance_type"
|
__tablename__ = 'oh_ms_maintenance_type'
|
||||||
code = Column(String, nullable=False, default="OH")
|
code = Column(String, nullable=False, default='OH')
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
|
|||||||
@ -1,82 +1,47 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
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
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
|
||||||
from .model import OverhaulScope
|
from .model import OverhaulScope
|
||||||
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
|
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
|
||||||
from .service import create, delete, get, get_all, update,get_all_oh_with_history_service
|
from .service import create, delete, get, get_all, update, get_all_oh_with_history_service
|
||||||
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[ScopePagination], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("", response_model=StandardResponse[ScopePagination], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
async def get_scopes(common: CommonParameters, scope_name: Optional[str]=None):
|
||||||
async def get_scopes(common: CommonParameters, scope_name: Optional[str] = None):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
results = await get_all(common=common, scope_name=scope_name)
|
results = await get_all(common=common, scope_name=scope_name)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/history', response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
data=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/history", response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_history(db_session: DbSession):
|
async def get_history(db_session: DbSession):
|
||||||
return StandardResponse(data=await get_all_oh_with_history_service(db_session=db_session), message="Data retrieved successfully")
|
return StandardResponse(data=await get_all_oh_with_history_service(db_session=db_session), message='Data retrieved successfully')
|
||||||
|
|
||||||
@router.get("/{overhaul_session_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
@router.get('/{overhaul_session_id}', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
async def get_scope(db_session: DbSession, overhaul_session_id: str):
|
async def get_scope(db_session: DbSession, overhaul_session_id: str):
|
||||||
scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id)
|
scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id)
|
||||||
if not scope:
|
if not scope:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
return StandardResponse(data=scope, message='Data retrieved successfully')
|
||||||
detail="A data with this id does not exist.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=scope, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
@router.post('', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
||||||
async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
|
async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
|
||||||
scope = await create(db_session=db_session, scope_in=scope_in)
|
scope = await create(db_session=db_session, scope_in=scope_in)
|
||||||
|
return StandardResponse(data=scope, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(data=scope, message="Data created successfully")
|
@router.post('/update/{scope_id}', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope))])
|
||||||
|
async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpdate, current_user: CurrentUser):
|
||||||
|
return StandardResponse(data=await update(db_session=db_session, scope_id=scope_id, scope_in=scope_in), message='Data updated successfully')
|
||||||
|
|
||||||
|
@router.post('/delete/{scope_id}', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.DELETE, OverhaulScope)), Depends(csrf_protect)])
|
||||||
@router.post("/update/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope))])
|
|
||||||
async def update_scope(
|
|
||||||
db_session: DbSession,
|
|
||||||
scope_id: str,
|
|
||||||
scope_in: ScopeUpdate,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
):
|
|
||||||
return StandardResponse(
|
|
||||||
data=await update(db_session=db_session, scope_id=scope_id, scope_in=scope_in),
|
|
||||||
message="Data updated successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.DELETE, OverhaulScope)), Depends(csrf_protect)])
|
|
||||||
async def delete_scope(db_session: DbSession, scope_id: str):
|
async def delete_scope(db_session: DbSession, scope_id: str):
|
||||||
scope = await get(db_session=db_session, overhaul_session_id=scope_id)
|
scope = await get(db_session=db_session, overhaul_session_id=scope_id)
|
||||||
|
|
||||||
if not scope:
|
if not scope:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=[{'msg': 'A data with this id does not exist.'}])
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=[{"msg": "A data with this id does not exist."}],
|
|
||||||
)
|
|
||||||
|
|
||||||
await delete(db_session=db_session, scope_id=scope_id)
|
await delete(db_session=db_session, scope_id=scope_id)
|
||||||
|
return StandardResponse(message='Data deleted successfully', data=scope)
|
||||||
return StandardResponse(message="Data deleted successfully", data=scope)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,23 @@
|
|||||||
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)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
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:
|
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)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
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:
|
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,10 +1 @@
|
|||||||
from slowapi import Limiter
|
|
||||||
from slowapi.util import get_remote_address
|
|
||||||
from src.config import RATELIMIT_STORAGE_URI
|
|
||||||
|
|
||||||
# limiter = Limiter(
|
|
||||||
# key_func=get_remote_address,
|
|
||||||
# default_limits=["120 per hour", "2880 per day"],
|
|
||||||
# strategy="fixed-window",
|
|
||||||
# storage_uri=RATELIMIT_STORAGE_URI,
|
|
||||||
# )
|
|
||||||
@ -1,77 +1,22 @@
|
|||||||
"""
|
|
||||||
Soft delete utilities.
|
|
||||||
|
|
||||||
Provides reusable functions to convert hard-delete operations into soft-deletes.
|
|
||||||
The `soft_delete_record` function works with any model that inherits `SoftDeleteMixin`
|
|
||||||
(which is included in `DefaultMixin`).
|
|
||||||
|
|
||||||
Usage in service files:
|
|
||||||
from src.soft_delete import soft_delete_record, apply_not_deleted_filter
|
|
||||||
|
|
||||||
async def delete(*, db_session, record_id: str):
|
|
||||||
await soft_delete_record(db_session=db_session, model=MyModel, record_id=record_id)
|
|
||||||
|
|
||||||
async def get_all(*, db_session, ...):
|
|
||||||
query = Select(MyModel)
|
|
||||||
query = apply_not_deleted_filter(query, MyModel)
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TypeVar, Type
|
from typing import TypeVar, Type
|
||||||
|
|
||||||
from sqlalchemy import Select, Update
|
from sqlalchemy import Select, Update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from src.config import TIMEZONE
|
from src.config import TIMEZONE
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
T = TypeVar('T')
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
def apply_not_deleted_filter(query: Select, model) -> Select:
|
def apply_not_deleted_filter(query: Select, model) -> Select:
|
||||||
"""
|
if hasattr(model, 'deleted_at'):
|
||||||
Appends a WHERE clause to exclude soft-deleted records.
|
|
||||||
Use this on all "get" queries to hide deleted data.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
query = Select(OverhaulScope)
|
|
||||||
query = apply_not_deleted_filter(query, OverhaulScope)
|
|
||||||
"""
|
|
||||||
if hasattr(model, "deleted_at"):
|
|
||||||
return query.filter(model.deleted_at.is_(None))
|
return query.filter(model.deleted_at.is_(None))
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
async def soft_delete_record(*, db_session: AsyncSession, model: Type[T], record_id: str, id_column: str='id') -> None:
|
||||||
async def soft_delete_record(
|
|
||||||
*,
|
|
||||||
db_session: AsyncSession,
|
|
||||||
model: Type[T],
|
|
||||||
record_id: str,
|
|
||||||
id_column: str = "id",
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Soft-delete a record by setting its `deleted_at` timestamp.
|
|
||||||
Instead of DELETE, this performs an UPDATE.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_session: The async database session.
|
|
||||||
model: The SQLAlchemy model class.
|
|
||||||
record_id: The value of the ID to match.
|
|
||||||
id_column: The name of the ID column (default: "id").
|
|
||||||
"""
|
|
||||||
now = datetime.now(pytz.timezone(TIMEZONE))
|
now = datetime.now(pytz.timezone(TIMEZONE))
|
||||||
id_col = getattr(model, id_column)
|
id_col = getattr(model, id_column)
|
||||||
|
stmt = Update(model).where(id_col == record_id).values(deleted_at=now)
|
||||||
stmt = (
|
|
||||||
Update(model)
|
|
||||||
.where(id_col == record_id)
|
|
||||||
.values(deleted_at=now)
|
|
||||||
)
|
|
||||||
await db_session.execute(stmt)
|
await db_session.execute(stmt)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
log.info(f'Soft-deleted {model.__tablename__} record: {record_id}')
|
||||||
log.info(f"Soft-deleted {model.__tablename__} record: {record_id}")
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,43 +1,28 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String, Date
|
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String, Date
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
from sqlalchemy.orm import relationship
|
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
|
||||||
from src.workorder.model import MasterWorkOrder
|
|
||||||
|
|
||||||
|
|
||||||
class MasterSparePart(Base, DefaultMixin):
|
class MasterSparePart(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_sparepart"
|
__tablename__ = 'oh_ms_sparepart'
|
||||||
|
|
||||||
assetnum = Column(String, nullable=False)
|
assetnum = Column(String, nullable=False)
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
stock = Column(Integer, nullable=False, default=0)
|
stock = Column(Integer, nullable=False, default=0)
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
cost_per_stock = Column(Float, nullable=False)
|
cost_per_stock = Column(Float, nullable=False)
|
||||||
unit = Column(String, nullable=False)
|
unit = Column(String, nullable=False)
|
||||||
|
sparepart_procurements = relationship('MasterSparepartProcurement', lazy='selectin')
|
||||||
sparepart_procurements = relationship("MasterSparepartProcurement", lazy="selectin")
|
|
||||||
|
|
||||||
|
|
||||||
class MasterSparepartProcurement(Base, DefaultMixin):
|
class MasterSparepartProcurement(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_sparepart_procurement"
|
__tablename__ = 'oh_ms_sparepart_procurement'
|
||||||
|
sparepart_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_sparepart.id', ondelete='cascade'), nullable=False)
|
||||||
sparepart_id = Column(
|
|
||||||
UUID(as_uuid=True),
|
|
||||||
ForeignKey("oh_ms_sparepart.id", ondelete="cascade"),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
quantity = Column(Integer, nullable=False)
|
quantity = Column(Integer, nullable=False)
|
||||||
status = Column(String, nullable=False)
|
status = Column(String, nullable=False)
|
||||||
eta_requisition = Column(Date, nullable=False)
|
eta_requisition = Column(Date, nullable=False)
|
||||||
eta_ordered = Column(Date, nullable=True)
|
eta_ordered = Column(Date, nullable=True)
|
||||||
eta_received = Column(Date, nullable=True)
|
eta_received = Column(Date, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SparepartRemark(Base, DefaultMixin):
|
class SparepartRemark(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_sparepart_remark"
|
__tablename__ = 'oh_ms_sparepart_remark'
|
||||||
|
|
||||||
itemnum = Column(String, nullable=False)
|
itemnum = Column(String, nullable=False)
|
||||||
remark = Column(String, nullable=False)
|
remark = Column(String, nullable=False)
|
||||||
@ -1,94 +1,19 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.database.service import (CommonParameters, DbSession, search_filter_sort_paginate)
|
from src.database.service import DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.sparepart.schema import SparepartRemark
|
from src.sparepart.schema import SparepartRemark
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
from .service import create_remark, get_spareparts_paginated
|
from .service import create_remark, get_spareparts_paginated
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[list])
|
||||||
@router.get("", response_model=StandardResponse[list])
|
async def get_sparepart(collector_db_session: CollectorDbSession, db_session: DbSession):
|
||||||
async def get_sparepart(collector_db_session:CollectorDbSession, db_session: DbSession):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
# return
|
|
||||||
data = await get_spareparts_paginated(db_session=db_session, collector_db_session=collector_db_session)
|
data = await get_spareparts_paginated(db_session=db_session, collector_db_session=collector_db_session)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('', response_model=StandardResponse[SparepartRemark], dependencies=[Depends(csrf_protect)])
|
||||||
data=data,
|
async def create_remark_route(collector_db_session: CollectorDbSession, db_session: DbSession, remark_in: SparepartRemark):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[SparepartRemark], dependencies=[Depends(csrf_protect)])
|
|
||||||
async def create_remark_route(collector_db_session:CollectorDbSession, db_session: DbSession, remark_in:SparepartRemark):
|
|
||||||
sparepart_remark = await create_remark(db_session=db_session, collector_db_session=collector_db_session, remark_in=remark_in)
|
sparepart_remark = await create_remark(db_session=db_session, collector_db_session=collector_db_session, remark_in=remark_in)
|
||||||
|
return StandardResponse(data=sparepart_remark, message='Remark Created successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=sparepart_remark,
|
|
||||||
message="Remark Created successfully"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.post("", response_model=StandardResponse[ActivityMasterCreate])
|
|
||||||
# async def create_activity(db_session: DbSession, activity_in: ActivityMasterCreate):
|
|
||||||
|
|
||||||
# activity = await create(db_session=db_session, activty_in=activity_in)
|
|
||||||
|
|
||||||
# return StandardResponse(data=activity, message="Data created successfully")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.get(
|
|
||||||
# "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
|
|
||||||
# )
|
|
||||||
# async def get_activity(db_session: DbSession, activity_id: str):
|
|
||||||
# activity = await get(db_session=db_session, activity_id=activity_id)
|
|
||||||
# if not activity:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# detail="A data with this id does not exist.",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# return StandardResponse(data=activity, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.put(
|
|
||||||
# "/{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)
|
|
||||||
|
|
||||||
# if not activity:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# 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",
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
# @router.delete(
|
|
||||||
# "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
|
|
||||||
# )
|
|
||||||
# async def delete_scope(db_session: DbSession, activity_id: str):
|
|
||||||
# activity = await get(db_session=db_session, activity_id=activity_id)
|
|
||||||
|
|
||||||
# if not activity:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# detail=[{"msg": "A data with this id does not exist."}],
|
|
||||||
# )
|
|
||||||
|
|
||||||
# await delete(db_session=db_session, activity_id=activity_id)
|
|
||||||
|
|
||||||
# return StandardResponse(message="Data deleted successfully", data=activity)
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
from src.enums import OptimumOHEnum
|
from src.enums import OptimumOHEnum
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentType(OptimumOHEnum):
|
class ScopeEquipmentType(OptimumOHEnum):
|
||||||
TEMP = "Temporary"
|
TEMP = 'Temporary'
|
||||||
PERM = "Permanent"
|
PERM = 'Permanent'
|
||||||
|
|||||||
@ -1,71 +1,36 @@
|
|||||||
from sqlalchemy import UUID, Column, Date, Float, ForeignKey, Integer, String, Boolean
|
from sqlalchemy import UUID, Column, Date, Float, ForeignKey, Integer, String, Boolean
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
from sqlalchemy.orm import relationship
|
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
|
||||||
from src.workorder.model import MasterWorkOrder
|
|
||||||
|
|
||||||
|
|
||||||
class StandardScope(Base, DefaultMixin):
|
class StandardScope(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_standard_scope"
|
__tablename__ = 'oh_ms_standard_scope'
|
||||||
|
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
is_alternating_oh = Column(Boolean, nullable=False, default=False)
|
is_alternating_oh = Column(Boolean, nullable=False, default=False)
|
||||||
assigned_date = Column(Date, nullable=True)
|
assigned_date = Column(Date, nullable=True)
|
||||||
service_cost = Column(Float, nullable=True)
|
service_cost = Column(Float, nullable=True)
|
||||||
|
master_equipment = relationship('MasterEquipment', lazy='selectin', primaryjoin='and_(StandardScope.location_tag == foreign(MasterEquipment.location_tag))', uselist=False)
|
||||||
master_equipment = relationship(
|
oh_history = relationship('EquipmentOHHistory', lazy='selectin', primaryjoin='and_(StandardScope.location_tag == foreign(EquipmentOHHistory.location_tag))', uselist=False)
|
||||||
"MasterEquipment",
|
workscope_groups = relationship('EquipmentWorkscopeGroup', lazy='selectin', primaryjoin='and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))', uselist=True)
|
||||||
lazy="selectin",
|
|
||||||
primaryjoin="and_(StandardScope.location_tag == foreign(MasterEquipment.location_tag))",
|
|
||||||
uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|
||||||
oh_history = relationship(
|
|
||||||
"EquipmentOHHistory",
|
|
||||||
lazy="selectin",
|
|
||||||
primaryjoin="and_(StandardScope.location_tag == foreign(EquipmentOHHistory.location_tag))",
|
|
||||||
uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|
||||||
workscope_groups = relationship(
|
|
||||||
"EquipmentWorkscopeGroup",
|
|
||||||
lazy="selectin",
|
|
||||||
primaryjoin="and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))",
|
|
||||||
uselist=True, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|
||||||
class EquipmentOHHistory(Base, DefaultMixin):
|
class EquipmentOHHistory(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_equipment_oh_history"
|
__tablename__ = 'oh_ms_equipment_oh_history'
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
last_oh_date = Column(Date, nullable=True)
|
last_oh_date = Column(Date, nullable=True)
|
||||||
last_oh_type = Column(String, nullable=True)
|
last_oh_type = Column(String, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class MasterEquipment(Base, DefaultMixin):
|
class MasterEquipment(Base, DefaultMixin):
|
||||||
__tablename__ = "ms_equipment_master"
|
__tablename__ = 'ms_equipment_master'
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, index=True)
|
id = Column(UUID(as_uuid=True), primary_key=True, index=True)
|
||||||
parent_id = Column(
|
parent_id = Column(UUID(as_uuid=True), ForeignKey('ms_equipment_master.id', ondelete='CASCADE'), nullable=True)
|
||||||
UUID(as_uuid=True),
|
|
||||||
ForeignKey("ms_equipment_master.id", ondelete="CASCADE"),
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assetnum = Column(String, nullable=True)
|
assetnum = Column(String, nullable=True)
|
||||||
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(
|
equipment_tree_id = Column(UUID(as_uuid=True), ForeignKey('ms_equipment_tree.id'), nullable=True)
|
||||||
UUID(as_uuid=True), ForeignKey("ms_equipment_tree.id"), nullable=True
|
equipment_tree = relationship('MasterEquipmentTree', backref='master_equipments')
|
||||||
)
|
parent = relationship('MasterEquipment', remote_side=[id], lazy='selectin')
|
||||||
|
|
||||||
equipment_tree = relationship("MasterEquipmentTree", backref="master_equipments")
|
|
||||||
parent = relationship("MasterEquipment", remote_side=[id], lazy="selectin")
|
|
||||||
|
|
||||||
|
|
||||||
class MasterEquipmentTree(Base, DefaultMixin):
|
class MasterEquipmentTree(Base, DefaultMixin):
|
||||||
__tablename__ = "ms_equipment_tree"
|
__tablename__ = 'ms_equipment_tree'
|
||||||
|
|
||||||
level_no = Column(Integer)
|
level_no = Column(Integer)
|
||||||
|
|||||||
@ -1,92 +1,31 @@
|
|||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
|
||||||
from src.database.core import DbSession, CollectorDbSession
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
from src.database.service import CommonParameters, search_filter_sort_paginate
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
|
from .schema import MasterEquipmentPagination, ScopeEquipmentCreate, ScopeEquipmentPagination
|
||||||
from .schema import (MasterEquipmentPagination, ScopeEquipmentCreate,
|
from .service import create, get_all, get_all_master_equipment, get_history_standard_scope_wo_service
|
||||||
ScopeEquipmentPagination, ScopeEquipmentRead,
|
|
||||||
ScopeEquipmentUpdate)
|
|
||||||
from .service import (create, delete, get_all, get_all_master_equipment, update, get_history_standard_scope_wo_service)
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[ScopeEquipmentPagination])
|
||||||
@router.get("", response_model=StandardResponse[ScopeEquipmentPagination])
|
async def get_scope_equipments(common: CommonParameters, scope_name: str=Query(None)):
|
||||||
async def get_scope_equipments(common: CommonParameters, scope_name: str = Query(None)):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
# return
|
|
||||||
data = await get_all(common=common, oh_scope=scope_name)
|
data = await get_all(common=common, oh_scope=scope_name)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/available/{scope_name}', response_model=StandardResponse[MasterEquipmentPagination])
|
||||||
data=data,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(data=results, message="Data retrieved successfully")
|
@router.post('', response_model=StandardResponse[List[str]])
|
||||||
|
async def create_scope_equipment(db_session: DbSession, scope_equipment_in: ScopeEquipmentCreate):
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[List[str]])
|
|
||||||
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.get('/history/{oh_session_id}', response_model=StandardResponse[List[dict]])
|
||||||
|
async def get_history_standard_scope_wo(db_session: DbSession, collector_db_session: CollectorDbSession, oh_session_id: UUID):
|
||||||
|
|
||||||
@router.get("/history/{oh_session_id}", response_model=StandardResponse[List[dict]])
|
|
||||||
async def get_history_standard_scope_wo(
|
|
||||||
db_session: DbSession, collector_db_session:CollectorDbSession, oh_session_id:UUID):
|
|
||||||
results = await get_history_standard_scope_wo_service(db_session=db_session, collector_db_session=collector_db_session, oh_session_id=oh_session_id)
|
results = await get_history_standard_scope_wo_service(db_session=db_session, collector_db_session=collector_db_session, oh_session_id=oh_session_id)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
return StandardResponse(data=results, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
# @router.put("/{assetnum}", response_model=StandardResponse[ScopeEquipmentRead])
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# if not scope_equipment:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# 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",
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
# @router.delete("/{assetnum}", response_model=StandardResponse[None])
|
|
||||||
# async def delete_scope_equipment(db_session: DbSession, assetnum: str):
|
|
||||||
# scope_equipment = await get_by_assetnum(db_session=db_session, assetnum=assetnum)
|
|
||||||
|
|
||||||
# if not scope_equipment:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
# detail=[{"msg": "A data with this id does not exist."}],
|
|
||||||
# )
|
|
||||||
|
|
||||||
# await delete(db_session=db_session, assetnum=assetnum)
|
|
||||||
|
|
||||||
# return StandardResponse(message="Data deleted successfully", data=None)
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue