Compare commits
11 Commits
oh_securit
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
bc05b0b9db | 1 month ago |
|
|
73bc6481c9 | 2 months ago |
|
|
6f50cb9852 | 2 months ago |
|
|
d910ef80d6 | 2 months ago |
|
|
474d76ff05 | 2 months ago |
|
|
8eb04ab699 | 2 months ago |
|
|
4380cfa134 | 2 months ago |
|
|
d51ba8dd8d | 2 months ago |
|
|
07711f9876 | 2 months ago |
|
|
a0fedd83f1 | 2 months ago |
|
|
7c36b7d276 | 3 months ago |
@ -1,2 +0,0 @@
|
|||||||
tests/
|
|
||||||
.venv/
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
env/
|
env/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
venv/
|
venv/
|
||||||
tests/
|
|
||||||
@ -0,0 +1,208 @@
|
|||||||
|
# DigitalTwin - Optimum Overhaul App (be-optimumoh) API Documentation
|
||||||
|
|
||||||
|
This document maps all the API endpoints, route handlers, database models, and external service layers inside the `be-optimumoh` microservice. It is a FastAPI-based backend engine managing Overhaul schedules, standard scopes, spare parts readiness, Temporal workflow executions, and optimization/constraint calculations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Summary of Services
|
||||||
|
|
||||||
|
| Service / Module | Purpose | Endpoints Count | Primary Database Tables |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| **Overhauls** (`overhauls`) | Core overhaul session status, critical parts, schedules, and systems overview. | 4 | `oh_ms_overhaul` |
|
||||||
|
| **Standard Scope** (`scope-equipments`) | Fleet scopes configuration, master equipment availability, and historical overhaul logs. | 4 | `oh_ms_standard_scope`, `oh_ms_equipment_oh_history` |
|
||||||
|
| **Overhaul Activities** (`overhaul-activity`) | Operations/activities scheduling and batch additions to active overhaul sessions. | 4 | `oh_tr_overhaul_activity` |
|
||||||
|
| **Workscope Groups** (`workscopes`) | Maintenance activity groups, tasks, and overhaul scheduling. | 5 | `oh_ms_workscope_group`, `oh_ms_workscope_task` |
|
||||||
|
| **Spare Parts** (`spareparts`) | Global inventory levels, procurement planning status, and planning remarks. | 2 | `oh_ms_sparepart`, `oh_ms_sparepart_remark` |
|
||||||
|
| **Equipment Workscopes** (`equipment-workscopes`) | Equipment-specific maintenance work scope groups mapping and jobs tracking. | 3 | `oh_tr_equipment_workscope_group` |
|
||||||
|
| **Equipment Spare Parts** (`equipment-spareparts`) | Material part catalog tracking per asset location tag. | 1 | `oh_ms_scope_equipment_part` |
|
||||||
|
| **Monitoring Gantt** (`overhaul-gantt`) | Google Sheets Gantt performance charts extraction and sync triggers. | 3 | `oh_ms_monitoring_spreadsheet` |
|
||||||
|
| **Overhaul Schedules** (`overhaul-schedules`) | Detailed calendar session creation, history tracking, updates, and deletes. | 6 | `oh_ms_overhaul` |
|
||||||
|
| **Time Constraints** (`time-constraint`) | Scheduling simulations under a rigid time window restriction. | 9 | `oh_ms_calculation_param`, `oh_tr_calculation_result` |
|
||||||
|
| **Optimized Time Constraints** | Simulations for ideal schedules under a variable optimal calendar logic. | 9 | `oh_tr_calculation_data`, `oh_tr_calculation_equipment_result` |
|
||||||
|
| **Target Reliability** | Background RBD Monte Carlo simulations executed via Temporal workflows. | 2 | None (Leverages Temporal Client SDK) |
|
||||||
|
| **Budget Constraints** | Financial threshold filtering and consequence calculation matrix. | 1 | `oh_tr_calculation_result` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ App Middlewares & Framework Architecture
|
||||||
|
|
||||||
|
### 1. Security Headers Middleware
|
||||||
|
FastAPI adds rigid HTTP security headers to all incoming connections inside `src/main.py`. In production, it enforces `Strict-Transport-Security`, `X-Frame-Options` (DENY), and full CSP blockades. In development mode, it supports relaxed parameters.
|
||||||
|
|
||||||
|
### 2. Request Sanitization & Slowapi Limiting
|
||||||
|
All JSON and path structures pass through `RequestValidationMiddleware` (`slowapi` integration) to enforce rate limiting thresholds globally, returning standard payload error formatting on breaches.
|
||||||
|
|
||||||
|
### 3. Multi-Session Context Scoping
|
||||||
|
Scoped database sessions are generated per request context dynamically utilizing ContextVars in `src/context.py` and SQLAlchemy's async connection pool engine:
|
||||||
|
* `db` session: Optimum overhaul local PostgreSQL dataset (`default`).
|
||||||
|
* `collector_db` session: Asset metadata and equipment configurations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Complete API Endpoints Catalog
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
UI[Frontend Client Application] -->|JWTBearer token| G[FastAPI Gateway Router]
|
||||||
|
G -->|Overhaul Master| OH[Overhaul Service]
|
||||||
|
G -->|Standard Scopes| SS[Standard Scope Service]
|
||||||
|
G -->|Spare Parts| SP[Spare Parts Service]
|
||||||
|
G -->|Gantt Tracking| GT[Gantt Chart Service]
|
||||||
|
G -->|Simulations API| TC[Constraint Calculations Service]
|
||||||
|
TC -->|Simulation Worker| TMP[Temporalio Cluster Engine]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. 📂 Overhauls Service (`overhauls`)
|
||||||
|
Prefix: `/overhauls` — Exposes master overhaul dashboard summaries, schedules, critical parts status, and system maps.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Overhauls** | `/overhauls` | `GET` | Algorithm | `oh_ms_overhaul` | Frontend | ~28 | **Y** | JWTBearer |
|
||||||
|
| **Overhauls** | `/overhauls/schedules` | `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
| **Overhauls** | `/overhauls/critical-parts` | `GET` | CRUD | `oh_ms_sparepart`, `oh_ms_sparepart_procurement` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
| **Overhauls** | `/overhauls/system-components` | `GET` | CRUD | None (Static mapping) | Frontend | ~10 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 📋 Standard Scope Service (`scope-equipments`)
|
||||||
|
Prefix: `/scope-equipments` — Manages fleet-wide equipment checklists and standard scopes configurations.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Standard Scope** | `/scope-equipments` | `GET` | CRUD | `oh_ms_standard_scope` | Frontend | ~10 | **Y** | JWTBearer |
|
||||||
|
| **Standard Scope** | `/scope-equipments/available/{scope_name}` | `GET` | CRUD | `ms_equipment_master`, `oh_ms_standard_scope` | Frontend | ~9 | **Y** | JWTBearer |
|
||||||
|
| **Standard Scope** | `/scope-equipments` | `POST` | CRUD | `oh_ms_standard_scope` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
| **Standard Scope** | `/scope-equipments/history/{oh_session_id}`| `GET` | CRUD | `oh_ms_equipment_oh_history`, `ms_equipment_master` | Frontend | ~10 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 🏃♂️ Overhaul Activity Service (`overhaul-activity`)
|
||||||
|
Prefix: `/overhaul-activity` — Controls tasks assigned to individual overhaul sessions.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Overhaul Activity** | `/overhaul-activity/{overhaul_session}` | `GET` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~25 | **Y** | JWTBearer |
|
||||||
|
| **Overhaul Activity** | `/overhaul-activity/{overhaul_session_id}`| `POST` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~15 | **Y** | JWTBearer |
|
||||||
|
| **Overhaul Activity** | `/overhaul-activity/{overhaul_session}/{assetnum}`| `GET` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~17 | **Y** | JWTBearer |
|
||||||
|
| **Overhaul Activity** | `/overhaul-activity/delete/{overhaul_session}/{location_tag}`| `POST` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 🔨 Workscope Group Service (`workscopes`)
|
||||||
|
Prefix: `/workscopes` — Defines tasks and maintenance groupings.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Workscope Group** | `/workscopes` | `GET` | CRUD | `oh_ms_workscope_group` | Frontend | ~12 | **Y** | JWTBearer |
|
||||||
|
| **Workscope Group** | `/workscopes` | `POST` | CRUD | `oh_ms_workscope_group` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
| **Workscope Group** | `/workscopes/{scope_equipment_activity_id}`| `GET` | CRUD | `oh_ms_workscope_task` | Frontend | ~12 | **Y** | JWTBearer |
|
||||||
|
| **Workscope Group** | `/workscopes/update/{scope_equipment_activity_id}`| `POST` | CRUD | `oh_ms_workscope_task` | Frontend | ~17 | **Y** | JWTBearer |
|
||||||
|
| **Workscope Group** | `/workscopes/delete/{scope_equipment_activity_id}`| `POST` | CRUD | `oh_ms_workscope_task` | Frontend | ~14 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 📦 Spare Parts Service (`spareparts`)
|
||||||
|
Prefix: `/spareparts` — Manages part catalogs and remark notations.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Spare Parts** | `/spareparts` | `GET` | CRUD | `oh_ms_sparepart`, `oh_ms_sparepart_procurement` | Frontend | ~12 | **Y** | JWTBearer |
|
||||||
|
| **Spare Parts** | `/spareparts` | `POST` | CRUD | `oh_ms_sparepart_remark` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 🔌 Equipment Workscopes Service (`equipment-workscopes`)
|
||||||
|
Prefix: `/equipment-workscopes` — Maps equipment directly to defined work scopes.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Equipment Workscopes**| `/equipment-workscopes/{location_tag}`| `GET` | CRUD | `oh_tr_equipment_workscope_group` | Frontend | ~12 | **Y** | JWTBearer |
|
||||||
|
| **Equipment Workscopes**| `/equipment-workscopes/{assetnum}` | `POST` | CRUD | `oh_tr_equipment_workscope_group` | Frontend | ~11 | **Y** | JWTBearer |
|
||||||
|
| **Equipment Workscopes**| `/equipment-workscopes/delete/{scope_job_id}`| `POST` | CRUD | `oh_tr_equipment_workscope_group` | Frontend | ~9 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 🔩 Equipment Spare Parts Service (`equipment-spareparts`)
|
||||||
|
Prefix: `/equipment-spareparts` — Lists active spare parts mapping per equipment tag.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Equipment Spareparts**| `/equipment-spareparts/{location_tag}`| `GET` | CRUD | `oh_ms_scope_equipment_part` | Frontend | ~10 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 📊 Overhaul Gantt Chart Service (`overhaul-gantt`)
|
||||||
|
Prefix: `/overhaul-gantt` — Coordinates Google Sheets sync actions to build overhaul performance monitoring dashboards.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Overhaul Gantt** | `/overhaul-gantt` | `GET` | Integration | `oh_ms_monitoring_spreadsheet` | Frontend | ~19 | **Y** | Google Sheets APIs, cell parsers |
|
||||||
|
| **Overhaul Gantt** | `/overhaul-gantt/spreadsheet`| `GET` | CRUD | `oh_ms_monitoring_spreadsheet` | Frontend | ~22 | **Y** | JWTBearer |
|
||||||
|
| **Overhaul Gantt** | `/overhaul-gantt/spreadsheet`| `POST` | CRUD | `oh_ms_monitoring_spreadsheet` | Frontend | ~36 | **Y** | Google Sheets URL validator |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 📅 Overhaul Session Schedules Service (`overhaul-schedules`)
|
||||||
|
Prefix: `/overhaul-schedules` — Controls scheduler calendar timelines.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Schedules** | `/overhaul-schedules` | `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~10 | **Y** | JWTBearer |
|
||||||
|
| **Schedules** | `/overhaul-schedules/history`| `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~4 | **Y** | JWTBearer |
|
||||||
|
| **Schedules** | `/overhaul-schedules/{overhaul_session_id}`| `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~10 | **Y** | JWTBearer |
|
||||||
|
| **Schedules** | `/overhaul-schedules` | `POST` | CRUD | `oh_ms_overhaul` | Frontend | ~5 | **Y** | JWTBearer |
|
||||||
|
| **Schedules** | `/overhaul-schedules/update/{scope_id}`| `POST` | CRUD | `oh_ms_overhaul` | Frontend | ~12 | **Y** | JWTBearer |
|
||||||
|
| **Schedules** | `/overhaul-schedules/delete/{scope_id}`| `POST` | CRUD | `oh_ms_overhaul` | Frontend | ~13 | **Y** | JWTBearer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. ⏱️ Time Constraints Calculation Service (`time-constraint`)
|
||||||
|
Prefix: `/calculation/time-constraint` — Simulates failure counts and risks under strict overhaul downtime constraints.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint` | `POST` | Algorithm | `oh_ms_calculation_param` | Frontend | ~36 | **Y** | RBD risk simulation logic |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint` | `GET` | CRUD | `oh_tr_calculation_data` | Frontend | ~18 | **Y** | JWTBearer |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/parameters`| `GET` | CRUD | `oh_ms_calculation_param` | Frontend | ~13 | **Y** | JWTBearer |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}`| `GET` | Algorithm | `oh_tr_calculation_result` | Frontend | ~18 | **N** | Internal Auth Key (Unprotected endpoint) |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/{assetnum}`| `GET` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~9 | **Y** | JWTBearer |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/simulation`| `POST` | Algorithm | `oh_tr_calculation_result` | Frontend | ~13 | **Y** | Temporal timelines calculator |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/update/{calculation_id}`| `POST` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~18 | **Y** | Bulk parameters updates |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/refresh-spareparts`| `POST` | Integration | `oh_ms_sparepart`, `oh_ms_calculation_param` | Frontend | ~15 | **Y** | Inventory sync parameters |
|
||||||
|
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/recalculate`| `POST` | Algorithm / Int | `oh_tr_calculation_result` | Frontend | ~17 | **Y** | Temporal workflows trigger |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. ⚙️ Optimized Time Constraints Service (`optimum-time-constraint`)
|
||||||
|
Prefix: `/calculation/optimum-time-constraint` — Solves for optimized downtime intervals.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint` | `POST` | Algorithm | `oh_ms_calculation_param` | Frontend | ~29 | **Y** | Optimum solver iterations |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint` | `GET` | CRUD | `oh_tr_calculation_data` | Frontend | ~15 | **Y** | JWTBearer |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/parameters`| `GET` | CRUD | `oh_ms_calculation_param` | Frontend | ~13 | **Y** | JWTBearer |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}`| `GET` | Algorithm | `oh_tr_calculation_result` | Frontend | ~19 | **N** | Internal Auth Key (Unprotected endpoint) |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/{assetnum}`| `GET` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~8 | **Y** | JWTBearer |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/simulation`| `POST` | Algorithm | `oh_tr_calculation_result` | Frontend | ~13 | **Y** | Optimization simulation matrix |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/update/{calculation_id}`| `POST` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~18 | **Y** | JWTBearer |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/refresh-spareparts`| `POST` | Integration | `oh_ms_sparepart` | Frontend | ~15 | **Y** | Inventory sync parameters |
|
||||||
|
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/recalculate`| `POST` | Algorithm / Int | `oh_tr_calculation_result` | Frontend | ~17 | **Y** | Temporal workflows trigger |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. ⚡ Target Reliability Simulation Service (`target-reliability`)
|
||||||
|
Prefix: `/calculation/target-reliability` — Leverages Temporal client triggers to dispatch heavy RBD Monte Carlo workflows.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Target Reliability** | `/calculation/target-reliability/simulate`| `POST` | Integration | None (Triggers Temporal workflow) | Frontend | ~18 | **Y** | Temporal IO client runner |
|
||||||
|
| **Target Reliability** | `/calculation/target-reliability` | `GET` | Algorithm | `oh_ms_overhaul` | Frontend | ~88 | **Y** | Temporal client status tracking, EAF math |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. 💰 Budget Constraints Service (`budget-constraint`)
|
||||||
|
Prefix: `/calculation/budget-constraint` — Computes maintenance strategies according to cost limit thresholds.
|
||||||
|
|
||||||
|
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||||
|
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||||
|
| **Budget Constraints** | `/calculation/budget-constraint/{session_id}`| `GET` | Algorithm | `oh_ms_overhaul`, `oh_tr_calculation_result` | Frontend | ~25 | **Y** | Risk-cost matrix calculator |
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
# Equipment Level Overhaul Optimization
|
||||||
|
|
||||||
|
This document explains the mathematical theory and implementation used to determine the optimal overhaul interval for individual assets.
|
||||||
|
|
||||||
|
## 1. Theoretical Foundation
|
||||||
|
The optimization model is based on the **Expected Total Cost per Unit Time (CPUT)** theory, a standard approach in reliability engineering for age-replacement and block-replacement policies.
|
||||||
|
|
||||||
|
### Objective Function
|
||||||
|
The goal is to find an overhaul interval ($T$) that minimizes the total expected cost amortized over time:
|
||||||
|
|
||||||
|
$$C(T) = \frac{C_{Preventive} + C_{Corrective} \cdot E[N(T)]}{T}$$
|
||||||
|
|
||||||
|
Where:
|
||||||
|
* **$T$**: The overhaul interval (e.g., month 12, 24, etc.).
|
||||||
|
* **$C_{Preventive}$**: The cost of a planned overhaul (materials, labor, and procurement).
|
||||||
|
* **$C_{Corrective}$**: The cost of an unplanned failure (repairs + risk cost of downtime).
|
||||||
|
* **$E[N(T)]$**: The expected number of failures occurring in the interval $(0, T]$. This is derived from the **NHPP (Non-Homogeneous Poisson Process)** reliability model.
|
||||||
|
|
||||||
|
## 2. The Cost Balance (The "U-Curve")
|
||||||
|
Optimization works by balancing two competing costs:
|
||||||
|
1. **Overhaul Cost ($C_{PM}/T$)**: As the interval $T$ increases, the amortized cost of the overhaul decreases (economy of waiting).
|
||||||
|
2. **Failure Risk ($C_{CM} \cdot E[N(T)]/T$)**: As the interval $T$ increases, the probability and expected frequency of failure increase (cost of wear-out).
|
||||||
|
|
||||||
|
The point where these two lines intersect typically represents the **Global Minimum** of the total cost curve, known as the **Optimum Overhaul Month**.
|
||||||
|
|
||||||
|
## 3. Implementation Logic
|
||||||
|
In the `service.py` engine, the search is performed as follows:
|
||||||
|
|
||||||
|
1. **Failure Projection**: The system fetches reliability prediction data (cumulative failures) for the analysis window.
|
||||||
|
2. **Sparepart Simulation**: For every potential month $T$, the system simulates the procurement process to calculate the real $C_{Preventive}$ (including any shortage penalties).
|
||||||
|
3. **Cost Amortization**:
|
||||||
|
```python
|
||||||
|
total_cycle_cost = total_expected_failure_cost + total_preventive_cost
|
||||||
|
cput = total_cycle_cost / month_index
|
||||||
|
```
|
||||||
|
4. **Grid Search**: The system iterates through all months in the analysis window and identifies the index with the lowest `cput` value.
|
||||||
|
|
||||||
|
## 4. Interpretation in UI
|
||||||
|
* **Analysis Window Card**: Shows the CPUT value at your currently selected month.
|
||||||
|
* **Optimum Target Card**: Shows the month $T$ where the curve is at its lowest point.
|
||||||
|
* **Potential Benefit**: Calculated as $CPUT(current) - CPUT(optimum)$. This represents the real monthly cash-flow saving if the maintenance interval is adjusted.
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# Plant Level Overhaul Optimization
|
||||||
|
|
||||||
|
This document explains how individual asset optimizations are aggregated to find the best economic strategy for the entire fleet or plant.
|
||||||
|
|
||||||
|
## 1. Fleet Aggregation Theory
|
||||||
|
At the plant level, the objective is to minimize the **Total Fleet Cost per Unit Time**. This assumes a **Uniform Interval Policy** or a **Synchronized Overhaul Strategy** where the plant looks for a common maintenance rhythm.
|
||||||
|
|
||||||
|
### Plant Objective Function
|
||||||
|
The plant-level cost function is the summation of individual equipment cost functions ($C_i$):
|
||||||
|
|
||||||
|
$$C_{Plant}(T) = \sum_{i=1}^{N} C_i(T) = \frac{\sum_{i=1}^{N} (C_{p,i} + C_{f,i} \cdot E[N_i(T)])}{T}$$
|
||||||
|
|
||||||
|
Where:
|
||||||
|
* **$N$**: Total number of equipments in the scope.
|
||||||
|
* **$C_{p,i}$**: Preventive cost for equipment $i$.
|
||||||
|
* **$C_{f,i}$**: Failure cost for equipment $i$.
|
||||||
|
* **$E[N_i(T)]$**: Expected failures for equipment $i$ until time $T$.
|
||||||
|
|
||||||
|
## 2. Searching for the Fleet Optimum
|
||||||
|
The "actual theory" used by the engine involves a two-phase search:
|
||||||
|
|
||||||
|
### Phase 1: Unconstrained Summation
|
||||||
|
The system calculates the CPUT curve for every piece of equipment independently. It then sums these curves to create a "Fleet U-Curve." The minimum of this sum represents the **Theoretical Fleet Optimum**.
|
||||||
|
|
||||||
|
### Phase 2: Sparepart Interaction & Constraints
|
||||||
|
Unlike a simple sum, the real plant optimum must account for **shared resources** (e.g., a limited budget or limited spare parts).
|
||||||
|
1. **Sparepart Conflicts**: If multiple equipments reach their optimal interval at the same time, the `SparepartManager` checks if there are enough parts in the warehouse.
|
||||||
|
2. **Constraint Penalty**: If parts are missing, a "Procurement Penalty" is added to the $C_{p,i}$ for that specific month, effectively shifting the "U-curve" to the right (delaying) or left (earlier) depending on availability.
|
||||||
|
3. **Final Selection**: The system chooses the Month $T$ that minimizes the *constrained* total fleet cost.
|
||||||
|
|
||||||
|
## 3. Implementation in `service.py`
|
||||||
|
The code uses `numpy` to perform vector addition of the cost curves:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Aggregate amortized costs for fleet analysis
|
||||||
|
total_corrective_costs += np.array(corrective_costs)
|
||||||
|
total_preventive_costs += np.array(preventive_costs)
|
||||||
|
total_costs += np.array(total_costs_equipment)
|
||||||
|
|
||||||
|
# Find the month T that minimizes the sum
|
||||||
|
fleet_optimal_index = np.argmin(total_costs)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Key Metrics for Decision Makers
|
||||||
|
* **Fleet CPUT**: The average monthly budget required to maintain the plant at the chosen interval.
|
||||||
|
* **Accumulation Control**: By using amortized costs (Rp/Month) instead of total cumulative costs, the plant chart remains stable and allows for direct comparison between intervals regardless of the number of assets.
|
||||||
|
* **Risk vs. Cost**: The plant chart shows the trade-off between the *Fleet Failure Risk* (increasing line) and *Fixed Overhaul Costs* (decreasing line).
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import asyncio
|
||||||
|
from src.database.core import engine
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async def alter_table():
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
try:
|
||||||
|
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN plant_results JSONB"))
|
||||||
|
print("Added plant_results")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"plant_results exists: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN fleet_statistics JSONB"))
|
||||||
|
print("Added fleet_statistics")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"fleet_statistics exists: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN analysis_metadata JSONB"))
|
||||||
|
print("Added analysis_metadata")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"analysis_metadata exists: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(alter_table())
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open('/home/atra/Development/be-optimumoh/src/calculation_time_constrains/service.py', 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Find the point where it broke
|
||||||
|
# Line 159 is ' \'month_index\': i + 1,\n'
|
||||||
|
# We want to keep up to line 159 (index 158)
|
||||||
|
|
||||||
|
new_lines = lines[:159]
|
||||||
|
new_lines.append(" 'source': source\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" return monthly_data\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" async def get_simulation_results(self, simulation_id: str = \"default\"):\n")
|
||||||
|
new_lines.append(" \"\"\"Get simulation results for Birnbaum importance calculations\"\"\"\n")
|
||||||
|
new_lines.append(" headers = {\n")
|
||||||
|
new_lines.append(" \"Authorization\": f\"Bearer {self.token}\",\n")
|
||||||
|
new_lines.append(" \"Content-Type\": \"application/json\"\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_result_url = f\"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode\"\n")
|
||||||
|
new_lines.append(" plant_result_url = f\"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant\"\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" async with httpx.AsyncClient(timeout=300.0) as client:\n")
|
||||||
|
new_lines.append(" calc_task = client.get(calc_result_url, headers=headers)\n")
|
||||||
|
new_lines.append(" plant_task = client.get(plant_result_url, headers=headers)\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_response, plant_response = await asyncio.gather(calc_task, plant_task)\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_response.raise_for_status()\n")
|
||||||
|
new_lines.append(" plant_response.raise_for_status()\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_data = calc_response.json()[\"data\"]\n")
|
||||||
|
new_lines.append(" plant_data = plant_response.json()[\"data\"]\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" return {\n")
|
||||||
|
new_lines.append(" \"calc_result\": calc_data,\n")
|
||||||
|
new_lines.append(" \"plant_result\": plant_data\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" def _calculate_equipment_costs_with_spareparts(self, failures_prediction: Dict, birnbaum_importance: float,\n")
|
||||||
|
new_lines.append(" preventive_cost: float, failure_replacement_cost: float, ecs,\n")
|
||||||
|
new_lines.append(" location_tag: str, planned_overhauls: List = None, loss_production_permonth=0) -> List[Dict]:\n")
|
||||||
|
new_lines.append(" \"\"\"Calculate costs for each month including sparepart costs and availability\"\"\"\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" if not failures_prediction:\n")
|
||||||
|
new_lines.append(" self.logger.warning(f\"No failure prediction data for {location_tag}\")\n")
|
||||||
|
new_lines.append(" return []\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" results = []\n")
|
||||||
|
new_lines.append(" months = list(failures_prediction.keys())\n")
|
||||||
|
new_lines.append(" num_months = len(months)\n")
|
||||||
|
new_lines.append(" failure_counts = []\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" monthly_risk_cost_per_failure = 0\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" if ecs:\n")
|
||||||
|
new_lines.append(" is_trip = 1 if ecs.get(\"Diskripsi Operasional Akibat Equip. Failure\") == \"Trip\" else 0\n")
|
||||||
|
new_lines.append(" is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance)\n")
|
||||||
|
new_lines.append(" if is_trip:\n")
|
||||||
|
new_lines.append(" downtime = ecs.get(\"Estimasi Waktu Maint. / Downtime / Gangguan (Jam)\")\n")
|
||||||
|
new_lines.append(" monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" for month_key in months:\n")
|
||||||
|
new_lines.append(" data = failures_prediction[month_key]\n")
|
||||||
|
new_lines.append(" failure_counts.append(data['cumulative_failures'])\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" for i in range(num_months):\n")
|
||||||
|
new_lines.append(" month_index = i + 1\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Use only months within the analysis window\n")
|
||||||
|
new_lines.append(" if month_index > self.time_window_months:\n")
|
||||||
|
new_lines.append(" continue\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" # Check sparepart availability for this month\n")
|
||||||
|
new_lines.append(" sparepart_analysis = self._analyze_sparepart_availability(\n")
|
||||||
|
new_lines.append(" location_tag, month_index - 1, planned_overhauls or []\n")
|
||||||
|
new_lines.append(" )\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # THEORY: Total Expected Cost per Unit Time (CPUT)\n")
|
||||||
|
new_lines.append(" # Reference: Maintenance Optimization Models (Age/Block Replacement)\n")
|
||||||
|
new_lines.append(" # C(T) = [Total Preventive Cost + Total Expected Corrective Cost(T)] / T\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 1. Total Expected Corrective Cost until month_index (Expected number of failures * cost per failure)\n")
|
||||||
|
new_lines.append(" # In NHPP model, Expected Failures E[N(T)] = Cumulative Failures\n")
|
||||||
|
new_lines.append(" total_expected_failure_cost = failure_counts[i] * (failure_replacement_cost + monthly_risk_cost_per_failure)\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 2. Total Preventive Cost (One-time cost at month_index)\n")
|
||||||
|
new_lines.append(" # Includes labor, materials, and procurement delays\n")
|
||||||
|
new_lines.append(" procurement_cost = sparepart_analysis['total_procurement_cost']\n")
|
||||||
|
new_lines.append(" total_preventive_cost = preventive_cost + procurement_cost\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 3. Expected Total Cycle Cost\n")
|
||||||
|
new_lines.append(" total_cycle_cost = total_expected_failure_cost + total_preventive_cost\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 4. Expected Cost Per Unit Time (Optimization Target)\n")
|
||||||
|
new_lines.append(" # This value forms the U-shaped curve\n")
|
||||||
|
new_lines.append(" cput = total_cycle_cost / month_index\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Store both absolute and amortized components for visualization\n")
|
||||||
|
new_lines.append(" results.append({\n")
|
||||||
|
new_lines.append(" 'month': month_index,\n")
|
||||||
|
new_lines.append(" 'number_of_failures': failure_counts[i],\n")
|
||||||
|
new_lines.append(" 'is_actual': failures_prediction[months[i]].get('source') == 'actual',\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Amortized components (for the \"U\" chart)\n")
|
||||||
|
new_lines.append(" 'failure_cost': total_expected_failure_cost / month_index,\n")
|
||||||
|
new_lines.append(" 'preventive_cost': preventive_cost / month_index,\n")
|
||||||
|
new_lines.append(" 'procurement_cost': procurement_cost / month_index,\n")
|
||||||
|
new_lines.append(" 'total_cost': cput,\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Absolute values (for breakdown analysis)\n")
|
||||||
|
new_lines.append(" 'absolute_failure_cost': total_expected_failure_cost,\n")
|
||||||
|
new_lines.append(" 'absolute_overhaul_cost': preventive_cost,\n")
|
||||||
|
new_lines.append(" 'absolute_procurement_cost': procurement_cost,\n")
|
||||||
|
new_lines.append(" 'total_cycle_cost': total_cycle_cost,\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" 'is_after_planned_oh': month_index > self.planned_oh_months,\n")
|
||||||
|
new_lines.append(" 'delay_months': max(0, month_index - self.planned_oh_months),\n")
|
||||||
|
new_lines.append(" 'procurement_details': sparepart_analysis,\n")
|
||||||
|
new_lines.append(" 'sparepart_available': sparepart_analysis['available'],\n")
|
||||||
|
new_lines.append(" 'sparepart_status': sparepart_analysis['message'],\n")
|
||||||
|
new_lines.append(" 'can_proceed': sparepart_analysis['can_proceed_with_delays']\n")
|
||||||
|
new_lines.append(" })\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" return results\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" def _analyze_sparepart_availability(self, equipment_tag: str, target_month: int, \n")
|
||||||
|
new_lines.append(" planned_overhauls: List) -> Dict:\n")
|
||||||
|
new_lines.append(" \"\"\"Analyze sparepart availability for equipment at target month\"\"\"\n")
|
||||||
|
new_lines.append(" if not self.sparepart_manager:\n")
|
||||||
|
new_lines.append(" return {\n")
|
||||||
|
new_lines.append(" 'available': True,\n")
|
||||||
|
new_lines.append(" 'message': 'Sparepart manager not initialized',\n")
|
||||||
|
new_lines.append(" 'total_procurement_cost': 0,\n")
|
||||||
|
new_lines.append(" 'procurement_needed': [],\n")
|
||||||
|
new_lines.append(" 'can_proceed_with_delays': True\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Convert planned overhauls to format expected by sparepart manager\n")
|
||||||
|
new_lines.append(" other_overhauls = [(eq_tag, month) for eq_tag, month in planned_overhauls\n")
|
||||||
|
new_lines.append(" if eq_tag != equipment_tag and month <= target_month]\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" return self.sparepart_manager.check_sparepart_availability(\n")
|
||||||
|
new_lines.append(" equipment_tag, target_month, other_overhauls\n")
|
||||||
|
new_lines.append(" )\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
|
||||||
|
new_lines.extend(lines[159:])
|
||||||
|
|
||||||
|
with open('/home/atra/Development/be-optimumoh/src/calculation_time_constrains/service.py', 'w') as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
@ -0,0 +1,263 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('src/calculation_time_constrains/service.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Replacement 1: run_simulation_with_spareparts
|
||||||
|
replacement_1 = """ finally:
|
||||||
|
await optimum_oh_model._close_session()
|
||||||
|
|
||||||
|
# Re-fetch calculation_data with equipment_results to ensure they are loaded
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.equipment_results), selectinload(CalculationData.parameter))
|
||||||
|
.where(CalculationData.id == calculation.id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
data_num = scope_calculation.max_interval
|
||||||
|
all_equipment = scope_calculation.equipment_results
|
||||||
|
included_equipment = [eq for eq in all_equipment if eq.is_included]
|
||||||
|
|
||||||
|
calculation_results = []
|
||||||
|
fleet_statistics = {
|
||||||
|
'total_equipment': len(all_equipment),
|
||||||
|
'included_equipment': len(included_equipment),
|
||||||
|
'excluded_equipment': len(all_equipment) - len(included_equipment),
|
||||||
|
'equipment_with_sparepart_constraints': 0,
|
||||||
|
'total_procurement_items': 0,
|
||||||
|
'critical_procurement_items': 0,
|
||||||
|
'total_spareparts': 745
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_failure_cost = sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment) if all_equipment else 0
|
||||||
|
|
||||||
|
rbd_marginal_fails = [0] * data_num
|
||||||
|
try:
|
||||||
|
if scope_calculation.rbd_simulation_id:
|
||||||
|
from src.config import RBD_SERVICE_API
|
||||||
|
import httpx
|
||||||
|
plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant"
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
plant_result_url,
|
||||||
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
plant_data = response.json().get("data", {})
|
||||||
|
timestamp_outs = plant_data.get("timestamp_outs", [])
|
||||||
|
if timestamp_outs:
|
||||||
|
from src.calculation_time_constrains.utils import create_time_series_data, calculate_failures_per_month
|
||||||
|
hourly_data = create_time_series_data(timestamp_outs, max_hours=data_num * 720)
|
||||||
|
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
||||||
|
rbd_fails_map = {m['month']: m['failures'] for m in cumulative_rbd_fails}
|
||||||
|
prev_fail = 0
|
||||||
|
for m in range(1, data_num + 1):
|
||||||
|
curr_fail = rbd_fails_map.get(m, prev_fail)
|
||||||
|
rbd_marginal_fails[m-1] = curr_fail - prev_fail
|
||||||
|
prev_fail = curr_fail
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning(f"Failed to fetch plant simulation: {e}")
|
||||||
|
|
||||||
|
cumulative_plant_failures = 0
|
||||||
|
import numpy as np
|
||||||
|
from .schema import CalculationResultsRead
|
||||||
|
for month_index in range(data_num):
|
||||||
|
historical_marginal_fail = 0
|
||||||
|
for eq in all_equipment:
|
||||||
|
if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]:
|
||||||
|
curr_fail = eq.daily_failures[month_index] if month_index < len(eq.daily_failures) else 0
|
||||||
|
prev_fail = eq.daily_failures[month_index-1] if month_index > 0 and (month_index - 1) < len(eq.daily_failures) else 0
|
||||||
|
historical_marginal_fail += max(0, curr_fail - prev_fail)
|
||||||
|
|
||||||
|
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
||||||
|
cumulative_plant_failures += marginal_fail
|
||||||
|
|
||||||
|
month_result = {
|
||||||
|
"overhaul_cost": 0.0,
|
||||||
|
"corrective_cost": 0.0,
|
||||||
|
"procurement_cost": 0.0,
|
||||||
|
"num_failures": cumulative_plant_failures,
|
||||||
|
"day": month_index + 1,
|
||||||
|
"month": month_index + 1,
|
||||||
|
"procurement_details": {},
|
||||||
|
"sparepart_summary": {
|
||||||
|
"total_procurement_cost": 0.0,
|
||||||
|
"equipment_requiring_procurement": 0,
|
||||||
|
"critical_shortages": 0,
|
||||||
|
"existing_orders_value": 0.0,
|
||||||
|
"new_orders_required": 0,
|
||||||
|
"urgent_procurements": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
equipment_requiring_procurement = 0
|
||||||
|
total_existing_orders_value = 0.0
|
||||||
|
total_new_orders_value = 0.0
|
||||||
|
critical_shortages = 0
|
||||||
|
urgent_procurements = 0
|
||||||
|
|
||||||
|
for eq in all_equipment:
|
||||||
|
if month_index >= len(eq.procurement_details):
|
||||||
|
continue
|
||||||
|
procurement_detail = eq.procurement_details[month_index]
|
||||||
|
if (procurement_detail and isinstance(procurement_detail, dict) and procurement_detail.get("procurement_needed")):
|
||||||
|
equipment_requiring_procurement += 1
|
||||||
|
pr_po_summary = procurement_detail.get("pr_po_summary", {})
|
||||||
|
existing_orders_value = pr_po_summary.get("total_existing_value", 0)
|
||||||
|
total_existing_orders_value += existing_orders_value
|
||||||
|
new_orders_value = pr_po_summary.get("total_new_orders_value", 0)
|
||||||
|
total_new_orders_value += new_orders_value
|
||||||
|
critical_missing = procurement_detail.get("critical_missing_parts", 0)
|
||||||
|
if critical_missing > 0:
|
||||||
|
critical_shortages += 1
|
||||||
|
recommendations = procurement_detail.get("recommendations", [])
|
||||||
|
urgent_items = [r for r in recommendations if r.get("priority") == "CRITICAL"]
|
||||||
|
if urgent_items:
|
||||||
|
urgent_procurements += 1
|
||||||
|
is_included_eq = False if eq.is_initial else eq.is_included
|
||||||
|
month_result["procurement_details"][eq.location_tag] = {
|
||||||
|
"is_included": is_included_eq,
|
||||||
|
"location_tag": eq.location_tag,
|
||||||
|
"details": procurement_detail.get("procurement_needed", []),
|
||||||
|
"detailed_message": procurement_detail.get("detailed_message", ""),
|
||||||
|
"pr_po_summary": pr_po_summary,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"sparepart_available": procurement_detail.get("sparepart_available", True),
|
||||||
|
"can_proceed": procurement_detail.get("can_proceed_with_delays", True),
|
||||||
|
"critical_missing_parts": critical_missing,
|
||||||
|
"existing_orders_value": existing_orders_value,
|
||||||
|
"new_orders_value": new_orders_value
|
||||||
|
}
|
||||||
|
if eq.is_included:
|
||||||
|
if (month_index < len(eq.overhaul_costs) and month_index < len(eq.procurement_costs)):
|
||||||
|
month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index])
|
||||||
|
month_result["procurement_cost"] += float(eq.procurement_costs[month_index])
|
||||||
|
|
||||||
|
month_result["corrective_cost"] = (cumulative_plant_failures * avg_failure_cost) / (month_index + 1)
|
||||||
|
month_result["sparepart_summary"].update({
|
||||||
|
"total_procurement_cost": month_result["procurement_cost"],
|
||||||
|
"equipment_requiring_procurement": equipment_requiring_procurement,
|
||||||
|
"critical_shortages": critical_shortages,
|
||||||
|
"existing_orders_value": total_existing_orders_value,
|
||||||
|
"new_orders_required": len([eq for eq in all_equipment if month_index < len(eq.procurement_details) and eq.procurement_details[month_index] and eq.procurement_details[month_index].get("procurement_needed")]),
|
||||||
|
"urgent_procurements": urgent_procurements
|
||||||
|
})
|
||||||
|
month_result["total_cost"] = month_result["corrective_cost"] + month_result["overhaul_cost"] + month_result["procurement_cost"]
|
||||||
|
calculation_results.append(month_result)
|
||||||
|
|
||||||
|
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
||||||
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
||||||
|
|
||||||
|
fleet_statistics['equipment_with_sparepart_constraints'] = len([
|
||||||
|
eq for eq in all_equipment if any(detail and detail.get("procurement_needed") for detail in eq.procurement_details if detail)
|
||||||
|
])
|
||||||
|
fleet_statistics['total_procurement_items'] = sum([
|
||||||
|
len(detail.get("procurement_needed", [])) for eq in all_equipment for detail in eq.procurement_details if detail and isinstance(detail, dict)
|
||||||
|
])
|
||||||
|
|
||||||
|
analysis_metadata = {
|
||||||
|
"planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12 + (scope.start_date.month - prev_oh_scope.end_date.month) if prev_oh_scope and scope else 0,
|
||||||
|
"total_fleet_procurement_cost": sum([eq.procurement_costs[int(scope_calculation.optimum_oh_day)] for eq in all_equipment if eq.procurement_costs]),
|
||||||
|
"last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None,
|
||||||
|
"next_oh_date": scope.start_date.isoformat() if scope else None,
|
||||||
|
"optimal_stat": None
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_results_read = [CalculationResultsRead(**r) for r in calculation_results]
|
||||||
|
optimal_analysis = _analyze_optimal_timing(
|
||||||
|
calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_calculation.plant_results = calculation_results
|
||||||
|
scope_calculation.fleet_statistics = fleet_statistics
|
||||||
|
scope_calculation.analysis_metadata = analysis_metadata
|
||||||
|
scope_calculation.optimum_analysis = optimal_analysis
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": calculation.id,
|
||||||
|
"optimum": optimal_analysis
|
||||||
|
}"""
|
||||||
|
|
||||||
|
pattern_1 = re.compile(r" finally:\n await optimum_oh_model\._close_session\(\).*?return \{\n \"id\": calculation_data\.id,\n \"optimum\": stats\n \}", re.DOTALL)
|
||||||
|
if pattern_1.search(content):
|
||||||
|
content = pattern_1.sub(replacement_1, content)
|
||||||
|
else:
|
||||||
|
print("Could not find Replacement 1 target")
|
||||||
|
|
||||||
|
# Replacement 2: get_calculation_result
|
||||||
|
replacement_2 = """async def get_calculation_result(db_session: DbSession, calculation_id: str, token, include_risk_cost):
|
||||||
|
\"\"\"
|
||||||
|
Get calculation results from DB, returning pre-calculated plant and equipment results.
|
||||||
|
\"\"\"
|
||||||
|
try:
|
||||||
|
# Get calculation data with equipment results
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.equipment_results))
|
||||||
|
.where(CalculationData.id == calculation_id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not scope_calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Calculation with id {calculation_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_overhaul = await get_scope(
|
||||||
|
db_session=db_session,
|
||||||
|
overhaul_session_id=scope_calculation.overhaul_session_id
|
||||||
|
)
|
||||||
|
if not scope_overhaul:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Overhaul scope for session {scope_calculation.overhaul_session_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse pre-calculated plant results
|
||||||
|
plant_results_raw = scope_calculation.plant_results or []
|
||||||
|
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
||||||
|
|
||||||
|
# Return comprehensive result
|
||||||
|
return CalculationTimeConstrainsRead(
|
||||||
|
id=scope_calculation.id,
|
||||||
|
reference=scope_calculation.overhaul_session_id,
|
||||||
|
scope=scope_overhaul.maintenance_type.name,
|
||||||
|
results=calculation_results,
|
||||||
|
optimum_oh=scope_calculation.optimum_oh_day,
|
||||||
|
optimum_oh_month=scope_calculation.optimum_oh_day + 1 if scope_calculation.optimum_oh_day is not None else None,
|
||||||
|
equipment_results=scope_calculation.equipment_results,
|
||||||
|
fleet_statistics=scope_calculation.fleet_statistics or {},
|
||||||
|
optimal_analysis=scope_calculation.optimum_analysis or {},
|
||||||
|
analysis_metadata=scope_calculation.analysis_metadata or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f"Error in get_calculation_result for calculation_id {calculation_id}: {str(e)}")
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Internal error processing calculation results: {str(e)}",
|
||||||
|
)"""
|
||||||
|
|
||||||
|
pattern_2 = re.compile(r"async def get_calculation_result\(db_session: DbSession, calculation_id: str, token, include_risk_cost\):.*?raise HTTPException\(\n status_code=status\.HTTP_500_INTERNAL_SERVER_ERROR,\n detail=f\"Internal error processing calculation results: \{str\(e\)\}\",\n \)", re.DOTALL)
|
||||||
|
if pattern_2.search(content):
|
||||||
|
content = pattern_2.sub(replacement_2, content)
|
||||||
|
else:
|
||||||
|
print("Could not find Replacement 2 target")
|
||||||
|
|
||||||
|
with open('src/calculation_time_constrains/service.py', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("Patch applied.")
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@ -1,5 +1,6 @@
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from src.config import HOST, PORT, ENV
|
|
||||||
if __name__ == '__main__':
|
from src.config import HOST, PORT
|
||||||
is_dev = str(ENV).lower() == 'development'
|
|
||||||
uvicorn.run('src.main:app', host=HOST, port=PORT, reload=is_dev)
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run("src.main:app", host=HOST, port=PORT, reload=True)
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from temporalio.client import Client
|
||||||
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
|
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||||
|
from temporal.temporal_workflows import create_optimum_oh_calculation, request_rbd_simulation, run_optimum_oh_calculation
|
||||||
|
|
||||||
|
TEMPORAL_URL = os.environ.get("TEMPORAL_URL", "http://192.168.1.86:7233")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
client = await Client.connect(TEMPORAL_URL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
worker = Worker(
|
||||||
|
client,
|
||||||
|
task_queue="oh-sim-queue",
|
||||||
|
workflows=[OptimumOHCalculationWorkflow],
|
||||||
|
activities=[
|
||||||
|
create_optimum_oh_calculation,
|
||||||
|
request_rbd_simulation,
|
||||||
|
run_optimum_oh_calculation
|
||||||
|
],
|
||||||
|
max_concurrent_workflow_tasks=50,
|
||||||
|
max_concurrent_activities=12
|
||||||
|
)
|
||||||
|
await worker.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Worker failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,45 +1,197 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from src.auth.service import JWTBearer
|
from src.auth.service import JWTBearer
|
||||||
from src.calculation_budget_constrains.router import router as calculation_budget_constraint
|
from src.calculation_budget_constrains.router import \
|
||||||
from src.calculation_target_reliability.router import router as calculation_target_reliability
|
router as calculation_budget_constraint
|
||||||
from src.calculation_time_constrains.router import router as calculation_time_constrains_router, get_calculation
|
from src.calculation_target_reliability.router import \
|
||||||
|
router as calculation_target_reliability
|
||||||
|
from src.calculation_time_constrains.router import \
|
||||||
|
router as calculation_time_constrains_router, get_calculation
|
||||||
|
from src.optimum_time_constraint.router import router as optimum_time_constraint_router
|
||||||
|
from src.optimum_time_constraint.router import get_calculation as optimum_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.include_router(standard_scope_router, prefix='/scope-equipments', tags=['scope_equipment'])
|
authenticated_api_router = APIRouter(
|
||||||
authenticated_api_router.include_router(overhaul_activity_router, prefix='/overhaul-activity', tags=['activity'])
|
dependencies=[Depends(JWTBearer())],
|
||||||
authenticated_api_router.include_router(workscope_group_router, prefix='/workscopes', tags=['workscope_groups'])
|
)
|
||||||
authenticated_api_router.include_router(sparepart_router, prefix='/spareparts', tags=['sparepart'])
|
# overhaul data
|
||||||
authenticated_api_router.include_router(equipment_workscope_group_router, prefix='/equipment-workscopes', tags=['equipment_workscope_groups'])
|
authenticated_api_router.include_router(
|
||||||
authenticated_api_router.include_router(equipment_sparepart_router, prefix='/equipment-spareparts', tags=['equipment_workscope_sparepart_router'])
|
overhaul_router, prefix="/overhauls", tags=["overhaul"]
|
||||||
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'])
|
|
||||||
calculation_router.include_router(calculation_time_constrains_router, prefix='/time-constraint', tags=['calculation', 'time_constraint'])
|
# authenticated_api_router.include_router(job_router, prefix="/jobs", tags=["job"])
|
||||||
calculation_router.include_router(calculation_target_reliability, prefix='/target-reliability', tags=['calculation', 'target_reliability'])
|
|
||||||
calculation_router.include_router(calculation_budget_constraint, prefix='/budget-constraint', tags=['calculation', 'budget_constraint'])
|
# # # Overhaul session data
|
||||||
|
# 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"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optimized Time constrains
|
||||||
|
calculation_router.include_router(
|
||||||
|
optimum_time_constraint_router,
|
||||||
|
prefix="/optimum-time-constraint",
|
||||||
|
tags=["calculation", "optimum_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'])
|
|
||||||
|
api_router.include_router(
|
||||||
|
get_calculation,
|
||||||
|
prefix="/calculation/time-constraint",
|
||||||
|
tags=["calculation", "time_constraint"],
|
||||||
|
)
|
||||||
|
|
||||||
|
api_router.include_router(
|
||||||
|
optimum_get_calculation,
|
||||||
|
prefix="/calculation/optimum-time-constraint",
|
||||||
|
tags=["calculation", "optimum_time_constraint"],
|
||||||
|
)
|
||||||
|
|
||||||
api_router.include_router(authenticated_api_router)
|
api_router.include_router(authenticated_api_router)
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
import logging
|
|
||||||
import logging.handlers
|
|
||||||
import json
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
except ImportError:
|
|
||||||
from backports.zoneinfo import ZoneInfo
|
|
||||||
from src.config import LOG_LEVEL, SERVICE_NAME
|
|
||||||
from src.enums import OptimumOHEnum
|
|
||||||
_TZ_LOCAL = ZoneInfo('Asia/Jakarta')
|
|
||||||
RESET = '\x1b[0m'
|
|
||||||
COLORS = {'DEBUG': '\x1b[36m', 'INFO': '\x1b[32m', 'WARNING': '\x1b[33m', 'WARN': '\x1b[33m', 'ERROR': '\x1b[31m', 'CRITICAL': '\x1b[1;31m'}
|
|
||||||
_REQUEST_FIELDS = frozenset({'method', 'path', 'status_code', 'duration_ms', 'request_id', 'user_id', 'user_ip', 'error_id', 'result', 'error_type', 'error_message'})
|
|
||||||
|
|
||||||
class LogLevels(OptimumOHEnum):
|
|
||||||
info = 'INFO'
|
|
||||||
warn = 'WARN'
|
|
||||||
error = 'ERROR'
|
|
||||||
debug = 'DEBUG'
|
|
||||||
|
|
||||||
class JSONFormatter(logging.Formatter):
|
|
||||||
|
|
||||||
def format(self, record: logging.LogRecord) -> str:
|
|
||||||
dt = datetime.datetime.fromtimestamp(record.created, tz=_TZ_LOCAL)
|
|
||||||
timestamp = dt.isoformat(timespec='microseconds')
|
|
||||||
status_code = getattr(record, 'status_code', None)
|
|
||||||
result = getattr(record, 'result', None)
|
|
||||||
if result is None and status_code is not None:
|
|
||||||
result = 'Request completed' if int(status_code) < 400 else 'Request failed'
|
|
||||||
log_record = {'timestamp': timestamp, 'service_name': SERVICE_NAME, 'level': record.levelname, 'method': getattr(record, 'method', None), 'path': getattr(record, 'path', None), 'status_code': status_code, 'duration_ms': getattr(record, 'duration_ms', None), 'request_id': getattr(record, 'request_id', None), 'user_id': getattr(record, 'user_id', None), 'user_ip': getattr(record, 'user_ip', None), 'error_id': getattr(record, 'error_id', None), 'result': result, 'error_type': getattr(record, 'error_type', None), 'error_message': getattr(record, 'error_message', None)}
|
|
||||||
log_json = json.dumps(log_record, default=str)
|
|
||||||
if sys.stdout.isatty():
|
|
||||||
color = COLORS.get(record.levelname, '')
|
|
||||||
return f'{color}{log_json}{RESET}'
|
|
||||||
return log_json
|
|
||||||
|
|
||||||
def configure_logging() -> None:
|
|
||||||
log_level = str(LOG_LEVEL).upper()
|
|
||||||
if log_level not in list(LogLevels):
|
|
||||||
log_level = LogLevels.error
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.setLevel(log_level)
|
|
||||||
if root_logger.hasHandlers():
|
|
||||||
root_logger.handlers.clear()
|
|
||||||
formatter = JSONFormatter()
|
|
||||||
stream_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
stream_handler.setFormatter(formatter)
|
|
||||||
root_logger.addHandler(stream_handler)
|
|
||||||
os.makedirs('logs', exist_ok=True)
|
|
||||||
file_handler = logging.handlers.RotatingFileHandler('logs/app.log', maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8')
|
|
||||||
file_handler.setFormatter(formatter)
|
|
||||||
root_logger.addHandler(file_handler)
|
|
||||||
for logger_name in ['uvicorn', 'uvicorn.access', 'uvicorn.error', 'fastapi']:
|
|
||||||
uvicorn_logger = logging.getLogger(logger_name)
|
|
||||||
uvicorn_logger.handlers = []
|
|
||||||
uvicorn_logger.propagate = True
|
|
||||||
logging.getLogger('uvicorn.access').setLevel(logging.WARNING)
|
|
||||||
slowapi_logger = logging.getLogger('slowapi')
|
|
||||||
slowapi_logger.setLevel(logging.ERROR)
|
|
||||||
slowapi_logger.propagate = False
|
|
||||||
logging.getLogger('slack_sdk.web.base_client').setLevel(logging.CRITICAL)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, List, Union
|
|
||||||
from fastapi import Request, HTTPException, status
|
|
||||||
Allow: str = 'allow'
|
|
||||||
Deny: str = 'deny'
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Principal:
|
|
||||||
key: str
|
|
||||||
value: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f'{self.key}:{self.value}'
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SystemPrincipal(Principal):
|
|
||||||
|
|
||||||
def __init__(self, value: str):
|
|
||||||
super().__init__(key='system', value=value)
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class RolePrincipal(Principal):
|
|
||||||
|
|
||||||
def __init__(self, value: str):
|
|
||||||
super().__init__(key='role', value=value)
|
|
||||||
Everyone = SystemPrincipal(value='everyone')
|
|
||||||
Authenticated = SystemPrincipal(value='authenticated')
|
|
||||||
|
|
||||||
class OHPermission(Enum):
|
|
||||||
CREATE = 'create'
|
|
||||||
READ = 'read'
|
|
||||||
EDIT = 'edit'
|
|
||||||
DELETE = 'delete'
|
|
||||||
|
|
||||||
class AccessControl:
|
|
||||||
|
|
||||||
def __init__(self, permission_exception: Any=None):
|
|
||||||
self.permission_exception = permission_exception or HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Permission denied')
|
|
||||||
|
|
||||||
def _acl(self, resource):
|
|
||||||
acl = getattr(resource, '__acl__', [])
|
|
||||||
if callable(acl):
|
|
||||||
return acl()
|
|
||||||
return acl
|
|
||||||
|
|
||||||
def has_permission(self, principals: List[Principal], required_permissions: List[OHPermission], resource: Any) -> bool:
|
|
||||||
if not isinstance(resource, list):
|
|
||||||
resource = [resource]
|
|
||||||
permits = []
|
|
||||||
for res in resource:
|
|
||||||
granted = False
|
|
||||||
acl = self._acl(res)
|
|
||||||
for action, principal, permissions in acl:
|
|
||||||
is_permitted = any((p in permissions for p in required_permissions))
|
|
||||||
if (action == Allow and is_permitted) and (principal in principals or principal == Everyone):
|
|
||||||
granted = True
|
|
||||||
break
|
|
||||||
if (action == Deny and is_permitted) and (principal in principals or principal == Everyone):
|
|
||||||
granted = False
|
|
||||||
break
|
|
||||||
permits.append(granted)
|
|
||||||
return all(permits)
|
|
||||||
|
|
||||||
def assert_access(self, principals: List[Principal], required_permissions: List[OHPermission], resource: Any):
|
|
||||||
if not self.has_permission(principals, required_permissions, resource):
|
|
||||||
raise self.permission_exception
|
|
||||||
ALLOWED_ROLES = {'Admin', 'Super Admin', 'Engineer', 'Application Administrator'}
|
|
||||||
|
|
||||||
def require_any_role(*allowed_roles: str):
|
|
||||||
normalized = {r.lower() for r in allowed_roles}
|
|
||||||
|
|
||||||
async def dependency(request: Request):
|
|
||||||
user = getattr(request.state, 'user', None)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Not authenticated')
|
|
||||||
user_role = user.get('role') if isinstance(user, dict) else getattr(user, 'role', None)
|
|
||||||
if not user_role or user_role.lower() not in normalized:
|
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Your role does not have permission to access this resource.')
|
|
||||||
return True
|
|
||||||
return dependency
|
|
||||||
|
|
||||||
def required_permission(permissions: Union[OHPermission, List[OHPermission]], resources: Any):
|
|
||||||
if isinstance(permissions, OHPermission):
|
|
||||||
permissions = [permissions]
|
|
||||||
|
|
||||||
async def dependency(request: Request):
|
|
||||||
user = getattr(request.state, 'user', None)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Not authenticated')
|
|
||||||
user_role = None
|
|
||||||
if isinstance(user, dict):
|
|
||||||
user_role = user.get('role')
|
|
||||||
else:
|
|
||||||
user_role = getattr(user, 'role', None)
|
|
||||||
if not user_role:
|
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='No user role found')
|
|
||||||
user_principals = [Authenticated, RolePrincipal(user_role)]
|
|
||||||
ac = AccessControl()
|
|
||||||
ac.assert_access(user_principals, permissions, resources)
|
|
||||||
return True
|
|
||||||
return dependency
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
name: Optional[str] = None
|
name: str
|
||||||
role: str
|
role: str
|
||||||
user_id: str
|
user_id: str
|
||||||
|
|||||||
@ -1,135 +1,235 @@
|
|||||||
|
# app/auth/auth_bearer.py
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Annotated
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
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):
|
||||||
def __init__(self, auto_error: bool=True):
|
super(JWTBearer, self).__init__(auto_error=auto_error)
|
||||||
super(JWTBearer, self).__init__(auto_error=False)
|
|
||||||
|
|
||||||
async def __call__(self, request: Request):
|
async def __call__(self, request: Request):
|
||||||
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
|
credentials: HTTPAuthorizationCredentials = await super(
|
||||||
|
JWTBearer, self
|
||||||
|
).__call__(request)
|
||||||
if credentials:
|
if credentials:
|
||||||
if not credentials.scheme == 'Bearer':
|
if not credentials.scheme == "Bearer":
|
||||||
raise HTTPException(status_code=401, detail='Invalid authentication scheme.')
|
raise HTTPException(
|
||||||
|
status_code=403, detail="Invalid authentication scheme."
|
||||||
|
)
|
||||||
method = request.method
|
method = request.method
|
||||||
if method == 'OPTIONS':
|
|
||||||
return None
|
if method == "OPTIONS":
|
||||||
|
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):
|
message = message.get("message", "Invalid token or expired token.")
|
||||||
message = message.get('message') or message.get('detail') or 'Invalid token or expired token.'
|
raise HTTPException(
|
||||||
else:
|
status_code=403, detail=message
|
||||||
message = str(message) if message else 'Invalid token or expired token.'
|
)
|
||||||
raise HTTPException(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)
|
if hasattr(message, "username"):
|
||||||
if username:
|
set_username(message.username)
|
||||||
set_username(username)
|
elif hasattr(message, "name"):
|
||||||
if hasattr(message, 'role'):
|
set_username(message.name)
|
||||||
|
if hasattr(message, "role"):
|
||||||
set_role(message.role)
|
set_role(message.role)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
raise HTTPException(status_code=401, detail='Invalid authorization code.')
|
else:
|
||||||
|
raise HTTPException(status_code=403, 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'
|
response = requests.get(
|
||||||
response = requests.get(url_to_verify, headers={'Authorization': f'Bearer {jwtoken}'})
|
f"{config.AUTH_SERVICE_API}/verify-token",
|
||||||
|
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 ', '')
|
return token.replace("Bearer ", "") # Menghapus prefix "Bearer "
|
||||||
return request.cookies.get('access_token')
|
else:
|
||||||
return ''
|
return request.cookies.get("access_token") # Fallback ke cookie
|
||||||
|
|
||||||
|
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(status_code=401, detail='Invalid Key.')
|
raise HTTPException(
|
||||||
|
status_code=403, detail="Invalid Key."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {
|
||||||
response = requests.post(f'{config.AUTH_SERVICE_API}/sign-in', headers=headers, data=json.dumps({'username': 'ohuser', 'password': '123456789'}))
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
response = requests.get(
|
||||||
response = requests.get(verify_url, headers={'Authorization': f'{token}'})
|
f"{config.AUTH_SERVICE_API}/verify-token",
|
||||||
|
headers={"Authorization": f"{token}"},
|
||||||
|
)
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise HTTPException(status_code=401, detail='Invalid token.')
|
raise HTTPException(
|
||||||
return token.split(' ')[1]
|
status_code=403, detail="Invalid token."
|
||||||
|
)
|
||||||
|
|
||||||
|
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 httpx
|
||||||
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]:
|
|
||||||
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', ''), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
AUTH_NOTIFY_ENDPOINT = f"{config.AUTH_SERVICE_API}/admin/notify-limit"
|
||||||
token = request.headers.get('Authorization')
|
|
||||||
headers = {'Authorization': token} if token else {}
|
|
||||||
|
async def notify_admin_on_rate_limit(
|
||||||
|
endpoint_name: str,
|
||||||
|
ip_address: str,
|
||||||
|
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": endpoint_name,
|
||||||
|
"ip_address": ip_address,
|
||||||
|
"method": method,
|
||||||
|
"cooldown": cooldown,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.to_thread(requests.post, AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
|
response = await client.post(AUTH_NOTIFY_ENDPOINT, json=payload)
|
||||||
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,
|
||||||
|
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": endpoint_name,
|
||||||
|
"ip_address": ip_address,
|
||||||
|
"method": method,
|
||||||
|
"cooldown": cooldown,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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 = httpx.post(AUTH_NOTIFY_ENDPOINT, json=payload, 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,6 +1,9 @@
|
|||||||
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
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,21 +1,43 @@
|
|||||||
from typing import Annotated, Dict
|
from typing import Annotated, Dict, List, Optional
|
||||||
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.overhaul_scope.model import OverhaulScope
|
|
||||||
from fastapi import Depends
|
|
||||||
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 = APIRouter()
|
||||||
async def get_target_reliability(db_session: DbSession, token: Token, session_id: str, collector_db: CollectorDbSession, params: Annotated[BudgetContraintQuery, Query()]):
|
|
||||||
|
|
||||||
|
@router.get("/{session_id}", response_model=StandardResponse[Dict])
|
||||||
|
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(simulation_id=TC_RBD_ID, token=token)
|
results = await get_simulation_results(
|
||||||
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)
|
simulation_id = TC_RBD_ID,
|
||||||
return StandardResponse(data={'results': results, 'consequence': consequence}, message='Data retrieved successfully')
|
token=token
|
||||||
|
)
|
||||||
|
|
||||||
|
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 +0,0 @@
|
|||||||
|
|
||||||
@ -1,45 +1,150 @@
|
|||||||
|
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 pydantic import BaseModel, Field
|
||||||
|
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.overhaul_scope.model import OverhaulScope
|
|
||||||
from fastapi import Depends
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get('', response_model=StandardResponse[OptimizationResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
class SimulateRequest(BaseModel):
|
||||||
async def get_target_reliability(db_session: DbSession, token: Token, collector_db: CollectorDbSession, params: Annotated[TargetReliabiltiyQuery, Query()]):
|
duration: int = Field(17520, description="Simulation duration in hours")
|
||||||
|
oh_duration: int = Field(1200, description="Overhaul duration in hours")
|
||||||
|
oh_session_id: str = Field(..., description="Overhaul session ID")
|
||||||
|
|
||||||
|
@router.post("/simulate", response_model=StandardResponse)
|
||||||
|
async def start_simulation(
|
||||||
|
token: Token,
|
||||||
|
db_session: DbSession,
|
||||||
|
payload: SimulateRequest
|
||||||
|
):
|
||||||
|
"""Start RBD simulation for target reliability."""
|
||||||
|
token_str = token.token if hasattr(token, 'token') else str(token)
|
||||||
|
result = await run_rbd_simulation(
|
||||||
|
sim_hours=payload.duration,
|
||||||
|
oh_duration=payload.oh_duration,
|
||||||
|
oh_session_id=payload.oh_session_id,
|
||||||
|
db_session=db_session,
|
||||||
|
token=token_str
|
||||||
|
)
|
||||||
|
return StandardResponse(
|
||||||
|
data={"simulation_id": result.get("data")},
|
||||||
|
message=result.get("message", "Simulation started successfully")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# @router.get("", response_model=StandardResponse[List[Dict]])
|
||||||
|
# async def get_target_reliability(
|
||||||
|
# db_session: DbSession,
|
||||||
|
# scope_name: Optional[str] = Query(None),
|
||||||
|
# eaf_threshold: float = Query(100),
|
||||||
|
# ):
|
||||||
|
# """Get all scope pagination."""
|
||||||
|
# results = await get_all_target_reliability(
|
||||||
|
# db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold
|
||||||
|
# )
|
||||||
|
|
||||||
|
# return StandardResponse(
|
||||||
|
# data=results,
|
||||||
|
# message="Data retrieved successfully",
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=StandardResponse[OptimizationResult])
|
||||||
|
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
|
||||||
|
oh_duration = params.oh_duration
|
||||||
|
|
||||||
if not oh_session_id:
|
if not oh_session_id:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='oh_session_id is required')
|
raise HTTPException(
|
||||||
if duration != 17520:
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
if not simulation_id:
|
detail="oh_session_id is required",
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Simulation ID is required for non-default duration. Please run simulation first.')
|
)
|
||||||
|
|
||||||
|
# results = await get_eaf_timeline(
|
||||||
|
# db_session=db_session,
|
||||||
|
# oh_session_id=oh_session_id,
|
||||||
|
# eaf_input=eaf_input,
|
||||||
|
# oh_duration=duration
|
||||||
|
# )
|
||||||
|
|
||||||
|
if simulation_id:
|
||||||
try:
|
try:
|
||||||
temporal_client = await Client.connect(TEMPORAL_URL)
|
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||||
handle = temporal_client.get_workflow_handle(f'simulation-{simulation_id}')
|
handle = temporal_client.get_workflow_handle(f"simulation-{simulation_id}")
|
||||||
desc = await handle.describe()
|
desc = await handle.describe()
|
||||||
status_name = desc.status.name
|
status_name = desc.status.name
|
||||||
if status_name in ['RUNNING', 'CONTINUED_AS_NEW']:
|
|
||||||
raise HTTPException(status_code=status.HTTP_425_TOO_EARLY, detail='Simulation is still running.')
|
if status_name in ["RUNNING", "CONTINUED_AS_NEW"]:
|
||||||
if status_name != 'COMPLETED':
|
raise HTTPException(
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f'Simulation failed with status: {status_name}')
|
status_code=status.HTTP_425_TOO_EARLY,
|
||||||
|
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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'Simulation not found or error checking status: {str(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:
|
||||||
|
if duration != 17520 or oh_duration != 1200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Simulation ID is required for non-default duration or OH duration. Please run simulation first.",
|
||||||
|
)
|
||||||
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))
|
|
||||||
return StandardResponse(data=optimize_result, message='Data retrieved successfully')
|
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=oh_duration,
|
||||||
|
cut_hours=float(cut_hours)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=optimize_result,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|||||||
@ -1,49 +1,91 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import random
|
import random
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
from src.config import TEMPORAL_URL
|
|
||||||
|
|
||||||
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]]:
|
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]]:
|
||||||
|
"""
|
||||||
|
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}'
|
workflow_id = f"simulation-{simulation_id}" # use returned 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']:
|
|
||||||
print(f'✅ Workflow {workflow_id} finished with status: {status}')
|
if status not in ["RUNNING", "CONTINUED_AS_NEW"]:
|
||||||
|
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
|
||||||
print(f'⚠️ Workflow {workflow_id} not found (retry {retries}/{max_retries}), waiting 10s before retry...')
|
else:
|
||||||
|
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
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,36 +1,216 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import Select, func, select
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
from src.auth.service import Token
|
||||||
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.overhaul_scope.service import get_all
|
from src.overhaul_scope.service import get_all
|
||||||
from src.standard_scope.model import StandardScope
|
from src.standard_scope.model import StandardScope
|
||||||
from src.workorder.model import MasterWorkOrder
|
from src.workorder.model import MasterWorkOrder
|
||||||
from .schema import CalculationTimeConstrainsParametersCreate, CalculationTimeConstrainsParametersRead, CalculationTimeConstrainsParametersRetrive
|
|
||||||
from .service import create_param_and_data, get_calculation_data_by_id, run_simulation_with_spareparts
|
from .schema import (CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
CalculationTimeConstrainsRead)
|
||||||
|
from .service import (create_calculation_result_service, create_param_and_data,
|
||||||
|
get_avg_cost_by_asset,
|
||||||
|
get_calculation_by_reference_and_parameter,
|
||||||
|
get_calculation_data_by_id, get_calculation_result,
|
||||||
|
run_simulation_with_spareparts)
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
|
|
||||||
async def get_create_calculation_parameters(*, db_session: DbSession, calculation_id: Optional[str]=None):
|
|
||||||
|
async def get_create_calculation_parameters(
|
||||||
|
*, db_session: DbSession, calculation_id: Optional[str] = None
|
||||||
|
):
|
||||||
|
|
||||||
|
|
||||||
if calculation_id is not None:
|
if calculation_id is not None:
|
||||||
calculation = await get_calculation_data_by_id(calculation_id=calculation_id, db_session=db_session)
|
calculation = await get_calculation_data_by_id(
|
||||||
|
calculation_id=calculation_id, db_session=db_session
|
||||||
|
)
|
||||||
|
|
||||||
if not calculation:
|
if not calculation:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
raise HTTPException(
|
||||||
return CalculationTimeConstrainsParametersRead(costPerFailure=calculation.parameter.avg_failure_cost, overhaulCost=calculation.parameter.overhaul_cost, reference=calculation)
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
stmt = select(StandardScope, func.avg(MasterWorkOrder.total_cost_max).label('average_cost')).outerjoin(MasterWorkOrder, StandardScope.location_tag == MasterWorkOrder.location_tag).group_by(StandardScope.id)
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return CalculationTimeConstrainsParametersRead(
|
||||||
|
costPerFailure=calculation.parameter.avg_failure_cost,
|
||||||
|
overhaulCost=calculation.parameter.overhaul_cost,
|
||||||
|
reference=calculation,
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
StandardScope,
|
||||||
|
func.avg(MasterWorkOrder.total_cost_max).label("average_cost"),
|
||||||
|
)
|
||||||
|
.outerjoin(MasterWorkOrder, StandardScope.location_tag == MasterWorkOrder.location_tag)
|
||||||
|
.group_by(StandardScope.id)
|
||||||
|
)
|
||||||
|
|
||||||
results = await db_session.execute(stmt)
|
results = await db_session.execute(stmt)
|
||||||
costFailure = results.all()
|
costFailure = results.all()
|
||||||
scopes = await get_all(db_session=db_session)
|
scopes = await get_all(db_session=db_session)
|
||||||
avaiableScopes = {scope.id: scope.scope_name for scope in scopes}
|
avaiableScopes = {scope.id: scope.scope_name for scope in scopes}
|
||||||
costFailurePerScope = {avaiableScopes.get(costPerFailure[0]): costPerFailure[1] for costPerFailure in costFailure}
|
costFailurePerScope = {
|
||||||
return CalculationTimeConstrainsParametersRetrive(costPerFailure=costFailurePerScope, availableScopes=avaiableScopes.values(), recommendedScope='A')
|
avaiableScopes.get(costPerFailure[0]): costPerFailure[1]
|
||||||
|
for costPerFailure in costFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return CalculationTimeConstrainsParametersRetrive(
|
||||||
|
costPerFailure=costFailurePerScope,
|
||||||
|
availableScopes=avaiableScopes.values(),
|
||||||
|
recommendedScope="A",
|
||||||
|
# historicalData={
|
||||||
|
# "averageOverhaulCost": 10000000,
|
||||||
|
# "lastCalculation": {
|
||||||
|
# "id": "calc_122",
|
||||||
|
# "date": "2024-10-15",
|
||||||
|
# "scope": "B",
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_calculation(
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||||
|
created_by: str,
|
||||||
|
simulation_id
|
||||||
|
):
|
||||||
|
from temporalio.client import Client
|
||||||
|
from src.config import TEMPORAL_URL
|
||||||
|
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
if simulation_id is None:
|
||||||
|
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||||
|
workflow_id = f"optimum-oh-calc-{uuid.uuid4()}"
|
||||||
|
args = {
|
||||||
|
"token": token.token if hasattr(token, 'token') else str(token),
|
||||||
|
"calculation_in": calculation_time_constrains_in.model_dump(mode="json"),
|
||||||
|
"created_by": created_by,
|
||||||
|
"callback_workflow_id": workflow_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = await temporal_client.start_workflow(
|
||||||
|
OptimumOHCalculationWorkflow.run,
|
||||||
|
args,
|
||||||
|
id=workflow_id,
|
||||||
|
task_queue="oh-sim-queue" # or whatever task queue they use
|
||||||
|
)
|
||||||
|
|
||||||
async def create_calculation(*, token: str, db_session: DbSession, collector_db_session: CollectorDbSession, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, created_by: str, simulation_id):
|
return {
|
||||||
calculation_data = await create_param_and_data(db_session=db_session, calculation_param_in=calculation_time_constrains_in, created_by=created_by)
|
"data": workflow_id,
|
||||||
rbd_simulation_id = simulation_id or TC_RBD_ID
|
"status": "success",
|
||||||
return await run_simulation_with_spareparts(db_session=db_session, calculation=calculation_data, token=token, collector_db_session=collector_db_session, simulation_id=rbd_simulation_id)
|
"message": "Calculation workflow started successfully"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
calculation_data = await create_param_and_data(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_param_in=calculation_time_constrains_in,
|
||||||
|
created_by=created_by
|
||||||
|
)
|
||||||
|
|
||||||
|
await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calculation_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
simulation_id=simulation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return await get_calculation_result(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=str(calculation_data.id),
|
||||||
|
token=token,
|
||||||
|
include_risk_cost=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_calculation(
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
simulation_id: Optional[str] = None
|
||||||
|
):
|
||||||
|
calculation_data = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not calculation_data:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Calculation not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
rbd_simulation_id = simulation_id or calculation_data.rbd_simulation_id or TC_RBD_ID
|
||||||
|
|
||||||
|
# Delete old results to avoid duplicates
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from .model import CalculationEquipmentResult, CalculationResult
|
||||||
|
|
||||||
|
await db_session.execute(
|
||||||
|
delete(CalculationResult).where(CalculationResult.calculation_data_id == calculation_data.id)
|
||||||
|
)
|
||||||
|
await db_session.execute(
|
||||||
|
delete(CalculationEquipmentResult).where(CalculationEquipmentResult.calculation_data_id == calculation_data.id)
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calculation_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
simulation_id=rbd_simulation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
from .service import get_calculation_result
|
||||||
|
return await get_calculation_result(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
token=token,
|
||||||
|
include_risk_cost=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_or_create_scope_equipment_calculation(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
scope_calculation_id,
|
||||||
|
calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]
|
||||||
|
):
|
||||||
|
scope_calculation = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=scope_calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
async def get_or_create_scope_equipment_calculation(*, db_session: DbSession, scope_calculation_id, calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]):
|
|
||||||
scope_calculation = await get_calculation_data_by_id(db_session=db_session, calculation_id=scope_calculation_id)
|
|
||||||
if not scope_calculation:
|
if not scope_calculation:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
return scope_calculation.id
|
return scope_calculation.id
|
||||||
|
|
||||||
|
# Check if calculation already exist
|
||||||
|
# return CalculationTimeConstrainsRead(
|
||||||
|
# id=scope_calculation.id,
|
||||||
|
# reference=scope_calculation.overhaul_session_id,
|
||||||
|
# results=scope_calculation.results,
|
||||||
|
# optimum_oh=scope_calculation.optimum_oh_day,
|
||||||
|
# equipment_results=scope_calculation.equipment_results,
|
||||||
|
# )
|
||||||
|
|||||||
@ -1,70 +1,172 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import List, Optional, Union
|
||||||
from sqlalchemy import JSON, UUID, Boolean, Column, Float, ForeignKey, Integer, String
|
|
||||||
|
from sqlalchemy import (JSON, UUID, Boolean, Column, Float, ForeignKey,
|
||||||
|
Integer, Numeric, String)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base, DbSession
|
from src.database.core import Base, DbSession
|
||||||
from src.models import DefaultMixin, IdentityMixin
|
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
class OverhaulReferenceType(str, Enum):
|
class OverhaulReferenceType(str, Enum):
|
||||||
SCOPE = 'SCOPE'
|
SCOPE = "SCOPE"
|
||||||
ASSET = 'ASSET'
|
ASSET = "ASSET"
|
||||||
|
|
||||||
|
|
||||||
class CalculationParam(Base, DefaultMixin, IdentityMixin):
|
class CalculationParam(Base, DefaultMixin, IdentityMixin):
|
||||||
__tablename__ = 'oh_ms_calculation_param'
|
__tablename__ = "oh_ms_calculation_param"
|
||||||
|
|
||||||
avg_failure_cost = Column(Float, nullable=False)
|
avg_failure_cost = Column(Float, nullable=False)
|
||||||
overhaul_cost = Column(Float, nullable=False)
|
overhaul_cost = Column(Float, nullable=False)
|
||||||
calculation_data = relationship('CalculationData', back_populates='parameter')
|
|
||||||
results = relationship('CalculationResult', back_populates='parameter')
|
# Relationships
|
||||||
|
calculation_data = relationship("CalculationData", back_populates="parameter")
|
||||||
|
results = relationship("CalculationResult", back_populates="parameter")
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# async def create_with_references(
|
||||||
|
# cls,
|
||||||
|
# db: DbSession,
|
||||||
|
# avg_failure_cost: float,
|
||||||
|
# overhaul_cost: float,
|
||||||
|
# created_by: str,
|
||||||
|
# # list of {"reference_type": OverhaulReferenceType, "reference_id": str}
|
||||||
|
# references: List[dict]
|
||||||
|
# ):
|
||||||
|
# # Create parameter
|
||||||
|
# param = cls(
|
||||||
|
# avg_failure_cost=avg_failure_cost,
|
||||||
|
# overhaul_cost=overhaul_cost,
|
||||||
|
# created_by=created_by
|
||||||
|
# )
|
||||||
|
# db.add(param)
|
||||||
|
# await db.flush() # Flush to get the param.id
|
||||||
|
|
||||||
|
# # Create reference links
|
||||||
|
# for ref in references:
|
||||||
|
# reference_link = ReferenceLink(
|
||||||
|
# parameter_id=param.id,
|
||||||
|
# overhaul_reference_type=ref["reference_type"],
|
||||||
|
# reference_id=ref["reference_id"]
|
||||||
|
# )
|
||||||
|
# db.add(reference_link)
|
||||||
|
|
||||||
|
# await db.commit()
|
||||||
|
# await db.refresh(param)
|
||||||
|
# return param
|
||||||
|
|
||||||
|
|
||||||
class CalculationData(Base, DefaultMixin, IdentityMixin):
|
class CalculationData(Base, DefaultMixin, IdentityMixin):
|
||||||
__tablename__ = 'oh_tr_calculation_data'
|
__tablename__ = "oh_tr_calculation_data"
|
||||||
parameter_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_calculation_param.id'), nullable=True)
|
|
||||||
overhaul_session_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_overhaul.id'))
|
parameter_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("oh_ms_calculation_param.id"), nullable=True
|
||||||
|
)
|
||||||
|
overhaul_session_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("oh_ms_overhaul.id")
|
||||||
|
)
|
||||||
optimum_oh_day = Column(Integer, nullable=True)
|
optimum_oh_day = Column(Integer, nullable=True)
|
||||||
|
|
||||||
max_interval = Column(Integer, nullable=True)
|
max_interval = Column(Integer, nullable=True)
|
||||||
|
|
||||||
rbd_simulation_id = Column(UUID(as_uuid=True), nullable=True)
|
rbd_simulation_id = Column(UUID(as_uuid=True), nullable=True)
|
||||||
|
|
||||||
optimum_analysis = Column(JSON, nullable=True)
|
optimum_analysis = Column(JSON, nullable=True)
|
||||||
session = relationship('OverhaulScope', lazy='raise')
|
plant_results = Column(JSON, nullable=True)
|
||||||
parameter = relationship('CalculationParam', back_populates='calculation_data')
|
fleet_statistics = Column(JSON, nullable=True)
|
||||||
equipment_results = relationship('CalculationEquipmentResult', lazy='raise', viewonly=True)
|
analysis_metadata = Column(JSON, nullable=True)
|
||||||
results = relationship('CalculationResult', lazy='raise', viewonly=True)
|
|
||||||
|
session = relationship("OverhaulScope", lazy="raise")
|
||||||
|
|
||||||
|
parameter = relationship("CalculationParam", back_populates="calculation_data")
|
||||||
|
|
||||||
|
equipment_results = relationship(
|
||||||
|
"CalculationEquipmentResult", lazy="raise", viewonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
results = relationship("CalculationResult", lazy="raise", viewonly=True)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_with_param(cls, overhaul_session_id: str, db: DbSession, avg_failure_cost: Optional[float], overhaul_cost: Optional[float], created_by: str, params_id: Optional[UUID]):
|
async def create_with_param(
|
||||||
|
cls,
|
||||||
|
overhaul_session_id: str,
|
||||||
|
db: DbSession,
|
||||||
|
avg_failure_cost: Optional[float],
|
||||||
|
overhaul_cost: Optional[float],
|
||||||
|
created_by: str,
|
||||||
|
params_id: Optional[UUID],
|
||||||
|
):
|
||||||
if not params_id:
|
if not params_id:
|
||||||
params = CalculationParam(avg_failure_cost=avg_failure_cost, overhaul_cost=overhaul_cost, created_by=created_by)
|
# Create Params
|
||||||
|
params = CalculationParam(
|
||||||
|
avg_failure_cost=avg_failure_cost,
|
||||||
|
overhaul_cost=overhaul_cost,
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
|
||||||
db.add(params)
|
db.add(params)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
params_id = params.id
|
params_id = params.id
|
||||||
calculation_data = cls(overhaul_session_id=overhaul_session_id, created_by=created_by, parameter_id=params_id)
|
|
||||||
|
calculation_data = cls(
|
||||||
|
overhaul_session_id=overhaul_session_id,
|
||||||
|
created_by=created_by,
|
||||||
|
parameter_id=params_id,
|
||||||
|
)
|
||||||
|
|
||||||
db.add(calculation_data)
|
db.add(calculation_data)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(calculation_data)
|
await db.refresh(calculation_data)
|
||||||
|
|
||||||
return calculation_data
|
return calculation_data
|
||||||
|
|
||||||
|
|
||||||
class CalculationResult(Base, DefaultMixin):
|
class CalculationResult(Base, DefaultMixin):
|
||||||
__tablename__ = 'oh_tr_calculation_result'
|
|
||||||
parameter_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_calculation_param.id'), nullable=False)
|
__tablename__ = "oh_tr_calculation_result"
|
||||||
calculation_data_id = Column(UUID(as_uuid=True), ForeignKey('oh_tr_calculation_data.id'), nullable=False)
|
|
||||||
|
parameter_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("oh_ms_calculation_param.id"), nullable=False
|
||||||
|
)
|
||||||
|
calculation_data_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("oh_tr_calculation_data.id"), nullable=False
|
||||||
|
)
|
||||||
day = Column(Integer, nullable=False)
|
day = Column(Integer, nullable=False)
|
||||||
corrective_cost = Column(Float, nullable=False)
|
corrective_cost = Column(Float, nullable=False)
|
||||||
overhaul_cost = Column(Float, nullable=False)
|
overhaul_cost = Column(Float, nullable=False)
|
||||||
num_failures = Column(Integer, nullable=False)
|
num_failures = Column(Integer, nullable=False)
|
||||||
parameter = relationship('CalculationParam', back_populates='results')
|
|
||||||
reference_link = relationship('CalculationData')
|
parameter = relationship("CalculationParam", back_populates="results")
|
||||||
|
reference_link = relationship("CalculationData")
|
||||||
|
|
||||||
|
|
||||||
class CalculationEquipmentResult(Base, DefaultMixin):
|
class CalculationEquipmentResult(Base, DefaultMixin):
|
||||||
__tablename__ = 'oh_tr_calculation_equipment_result'
|
|
||||||
|
__tablename__ = "oh_tr_calculation_equipment_result"
|
||||||
|
|
||||||
corrective_costs = Column(JSON, nullable=False)
|
corrective_costs = Column(JSON, nullable=False)
|
||||||
overhaul_costs = Column(JSON, nullable=False)
|
overhaul_costs = Column(JSON, nullable=False)
|
||||||
daily_failures = Column(JSON, nullable=False)
|
daily_failures = Column(JSON, nullable=False)
|
||||||
|
is_actual = Column(JSON, nullable=True) # List of booleans
|
||||||
procurement_costs = Column(JSON, nullable=False)
|
procurement_costs = Column(JSON, nullable=False)
|
||||||
location_tag = Column(String(255), nullable=False)
|
location_tag = Column(String(255), nullable=False)
|
||||||
material_cost = Column(Float, nullable=False)
|
material_cost = Column(Float, nullable=False)
|
||||||
service_cost = Column(Float, nullable=False)
|
service_cost = Column(Float, nullable=False)
|
||||||
calculation_data_id = Column(UUID(as_uuid=True), ForeignKey('oh_tr_calculation_data.id'), nullable=True)
|
calculation_data_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("oh_tr_calculation_data.id"), nullable=True
|
||||||
|
)
|
||||||
optimum_day = Column(Integer, default=1)
|
optimum_day = Column(Integer, default=1)
|
||||||
is_included = Column(Boolean, default=True)
|
is_included = Column(Boolean, default=True)
|
||||||
procurement_details = Column(JSON, nullable=True)
|
procurement_details = Column(JSON, nullable=True)
|
||||||
is_initial = Column(Boolean, default=True)
|
is_initial = Column(Boolean, default=True)
|
||||||
master_equipment = relationship('MasterEquipment', lazy='joined', primaryjoin='and_(CalculationEquipmentResult.location_tag == foreign(MasterEquipment.location_tag))', uselist=False)
|
|
||||||
|
master_equipment = relationship(
|
||||||
|
"MasterEquipment",
|
||||||
|
lazy="joined",
|
||||||
|
primaryjoin="and_(CalculationEquipmentResult.location_tag == foreign(MasterEquipment.location_tag))",
|
||||||
|
uselist=False, # Add this if it's a one-to-one relationship
|
||||||
|
)
|
||||||
|
|||||||
@ -1,61 +1,230 @@
|
|||||||
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 .schema import CalculationResultsRead, CalculationSelectedEquipmentUpdate, CalculationTimeConstrainsCreate, CalculationTimeConstrainsParametersCreate, CalculationTimeConstrainsParametersRead, CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult
|
from .flows import (create_calculation, get_create_calculation_parameters,
|
||||||
from .service import bulk_update_equipment, get_calculation_result, get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations
|
get_or_create_scope_equipment_calculation)
|
||||||
|
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.overhaul_scope.model import OverhaulScope
|
|
||||||
from fastapi import Depends
|
|
||||||
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))])
|
|
||||||
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()]):
|
@router.post(
|
||||||
|
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]]
|
||||||
|
)
|
||||||
|
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
|
||||||
simulation_id = params.simulation_id
|
with_results = params.with_results
|
||||||
|
simulation_id = calculation_time_constrains_in.simulationRBDId
|
||||||
|
|
||||||
|
|
||||||
if scope_calculation_id:
|
if scope_calculation_id:
|
||||||
results = await get_or_create_scope_equipment_calculation(db_session=db_session, scope_calculation_id=scope_calculation_id, calculation_time_constrains_in=calculation_time_constrains_in)
|
results = await get_or_create_scope_equipment_calculation(
|
||||||
|
db_session=db_session,
|
||||||
|
scope_calculation_id=scope_calculation_id,
|
||||||
|
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
results = await create_calculation(token=token, db_session=db_session, collector_db_session=collector_db_session, calculation_time_constrains_in=calculation_time_constrains_in, created_by=current_user.name, simulation_id=simulation_id)
|
results = await create_calculation(
|
||||||
return StandardResponse(data=results, message='Data created successfully')
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=results, message="Data created successfully")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]]
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
"/parameters",
|
||||||
|
response_model=StandardResponse[
|
||||||
|
Union[
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def get_calculation_parameters(
|
||||||
|
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
|
||||||
|
):
|
||||||
|
"""Get all calculation parameter."""
|
||||||
|
|
||||||
|
parameters = await get_create_calculation_parameters(
|
||||||
|
db_session=db_session, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
@router.get('', response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
return StandardResponse(
|
||||||
async def get_all_simulation_calculations(db_session: DbSession, token: Token, current_user: CurrentUser):
|
data=parameters,
|
||||||
calculations = await get_all_calculations(db_session=db_session)
|
message="Data retrieved successfully",
|
||||||
return StandardResponse(data=calculations, message='Data retrieved successfully')
|
)
|
||||||
|
|
||||||
@router.get('/parameters', response_model=StandardResponse[Union[CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsParametersRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
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')
|
|
||||||
|
|
||||||
@get_calculation.get('/{calculation_id}', response_model=StandardResponse[CalculationTimeConstrainsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
@get_calculation.get(
|
||||||
async def get_calculation_results(db_session: DbSession, calculation_id, token: InternalKey, include_risk_cost: int=Query(1, alias='risk_cost')):
|
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
|
||||||
|
)
|
||||||
|
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')
|
|
||||||
|
|
||||||
@router.get('/{calculation_id}/{assetnum}', response_model=StandardResponse[EquipmentResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
results = await get_calculation_result(
|
||||||
|
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]
|
||||||
|
)
|
||||||
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')
|
|
||||||
|
|
||||||
@router.post('/{calculation_id}/simulation', response_model=StandardResponse[CalculationResultsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
results = await get_calculation_by_assetnum(
|
||||||
async def get_simulation_result(db_session: DbSession, calculation_id, calculation_simuation_in: CalculationTimeConstrainsCreate):
|
db_session=db_session, assetnum=assetnum, calculation_id=calculation_id
|
||||||
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(
|
||||||
|
"/{calculation_id}/simulation",
|
||||||
|
response_model=StandardResponse[CalculationResultsRead],
|
||||||
|
)
|
||||||
|
async def get_simulation_result(
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_id,
|
||||||
|
calculation_simuation_in: CalculationTimeConstrainsCreate,
|
||||||
|
):
|
||||||
|
simulation_result = await get_calculation_result_by_day(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
simulation_day=calculation_simuation_in.intervalDays,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=simulation_result, message="Data retrieved successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/update/{calculation_id}', response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope)), Depends(csrf_protect)])
|
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]])
|
||||||
async def update_selected_equipment(db_session: DbSession, calculation_id, calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate]):
|
async def update_selected_equipment(
|
||||||
|
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)
|
|
||||||
return StandardResponse(data=results, message='Data retrieved successfully')
|
results = await bulk_update_equipment(
|
||||||
|
db=db_session,
|
||||||
|
selected_equipments=calculation_time_constrains_in,
|
||||||
|
calculation_data_id=calculation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=results,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/{calculation_id}/refresh-spareparts", response_model=StandardResponse[dict])
|
||||||
|
async def refresh_spareparts(
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Refresh sparepart availability for an existing calculation"""
|
||||||
|
from .service import refresh_spareparts_service
|
||||||
|
|
||||||
|
await refresh_spareparts_service(
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data={}, message="Spareparts refreshed successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{calculation_id}/recalculate", response_model=StandardResponse[CalculationTimeConstrainsRead])
|
||||||
|
async def recalculate_calculation_api(
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
token: Token,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Recalculate an existing simulation with fresh data"""
|
||||||
|
from .flows import recalculate_calculation
|
||||||
|
|
||||||
|
results = await recalculate_calculation(
|
||||||
|
token=token,
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=results, message="Calculation updated with fresh data")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,134 +1,345 @@
|
|||||||
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):
|
||||||
filtered_data = [d for d in chart_data if d['currentEvent'] != 'ON_OH']
|
# Filter out ON_OH
|
||||||
sorted_data = sorted(filtered_data, key=lambda x: x['cumulativeTime'])
|
filtered_data = [d for d in chart_data if d["currentEvent"] != "ON_OH"]
|
||||||
|
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):
|
|
||||||
while current_state_index < len(sorted_data) - 1 and hour >= sorted_data[current_state_index + 1]['cumulativeTime']:
|
for hour in range(0, max_hours + 1): # start from 0
|
||||||
|
# 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['cumulativeTime']
|
||||||
current_eq_status = data_point['currentEQStatus']
|
current_eq_status = data_point['currentEQStatus']
|
||||||
month = (hour - 1) // 720 + 1
|
|
||||||
if current_eq_status == 'OoS' and previous_eq_status is not None and (previous_eq_status != 'OoS'):
|
# Calculate which month this hour belongs to (1-based)
|
||||||
|
# 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
|
||||||
elif current_eq_status == 'OoS' and previous_eq_status is None:
|
|
||||||
|
# Special case: if the very first data point is a failure
|
||||||
|
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 = []
|
||||||
|
if monthly_data:
|
||||||
|
max_month = max(monthly_data.keys())
|
||||||
|
for month in range(1, max_month + 1):
|
||||||
|
result.append({
|
||||||
|
'month': month,
|
||||||
|
'failures': monthly_data.get(month, monthly_data.get(month-1, 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_oos_per_month(hourly_data):
|
||||||
|
"""
|
||||||
|
Calculate the discrete OOS hours for each month from hourly data.
|
||||||
|
A failure is defined as when currentEQStatus = "OoS".
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hourly_data: List of dicts with 'cumulativeTime', 'flowRate', and 'currentEQStatus' keys
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with 'month' and 'oos_hours' keys (discrete monthly count)
|
||||||
|
"""
|
||||||
|
monthly_data = {}
|
||||||
|
|
||||||
|
for data_point in hourly_data:
|
||||||
|
hour = data_point['cumulativeTime']
|
||||||
|
current_eq_status = data_point['currentEQStatus']
|
||||||
|
|
||||||
|
# Calculate which month this hour belongs to (1-based)
|
||||||
|
# Assuming 30 days per month = 720 hours per month
|
||||||
|
month = ((hour - 1) // 720) + 1
|
||||||
|
|
||||||
|
if month not in monthly_data:
|
||||||
|
monthly_data[month] = 0
|
||||||
|
|
||||||
|
if current_eq_status == "OoS":
|
||||||
|
monthly_data[month] += 1
|
||||||
|
|
||||||
|
# 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({'month': month, 'failures': monthly_data.get(month, monthly_data.get(month - 1, 0))})
|
result.append({
|
||||||
|
'month': month,
|
||||||
|
'oos_hours': monthly_data.get(month, 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):
|
||||||
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}'
|
"""Get failure predictions for equipment from simulation service"""
|
||||||
|
calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(calc_result_url, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'}, timeout=30)
|
response = requests.get(
|
||||||
|
calc_result_url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
prediction_data = response.json()['data']
|
prediction_data = response.json()['data']
|
||||||
except (requests.RequestException, ValueError) as e:
|
except (requests.RequestException, ValueError) as e:
|
||||||
raise Exception(str(e))
|
raise Exception(str(e))
|
||||||
|
|
||||||
|
|
||||||
return prediction_data
|
return prediction_data
|
||||||
|
|
||||||
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float=550):
|
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(service_hours / total_time * 100 if total_time > 0 else 0)
|
monthly_results[month_str]['availability_percentage'] = float(
|
||||||
|
(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
|
availability = data['availability_percentage'] / 100 # Convert to decimal
|
||||||
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] = {**monthly_metrics[month], **risk_analysis['monthly_details'][month]}
|
combined_results[month] = {
|
||||||
return {'monthly_data': combined_results, 'risk_cost_array': risk_analysis['risk_cost_per_failure_array']}
|
**monthly_metrics[month],
|
||||||
|
**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,64 +1,97 @@
|
|||||||
|
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:
|
||||||
config = Config('.env')
|
# Try to load from .env file first
|
||||||
|
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')
|
|
||||||
ENV = config('ENV')
|
LOG_LEVEL = config("LOG_LEVEL", default="INFO")
|
||||||
PORT = config('PORT', cast=int)
|
ENV = (os.getenv("ENV") or os.getenv("ENVIRONMENT") or os.getenv("ENVIRONEMENT") or "production").strip()
|
||||||
HOST = config('HOST')
|
DEBUG = 1 if (ENV.upper() in ("DEV", "DEVELOPMENT", "LOCAL") and "PROD" not in ENV.upper()) else 0
|
||||||
DATABASE_HOSTNAME = config('DATABASE_HOSTNAME')
|
PORT = config("PORT", cast=int, default=8000)
|
||||||
_DATABASE_CREDENTIAL_USER = config('DATABASE_CREDENTIAL_USER')
|
HOST = config("HOST", default="localhost")
|
||||||
_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", default="digital_twin")
|
||||||
DATABASE_PORT = config('DATABASE_PORT')
|
DATABASE_PORT = config("DATABASE_PORT", default="5432")
|
||||||
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_POOL_SIZE = config("DATABASE_ENGINE_POOL_SIZE", cast=int, default=20)
|
||||||
COLLECTOR_HOSTNAME = config('COLLECTOR_HOSTNAME')
|
DATABASE_ENGINE_MAX_OVERFLOW = config(
|
||||||
COLLECTOR_PORT = config('COLLECTOR_PORT', cast=int)
|
"DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0
|
||||||
COLLECTOR_CREDENTIAL_USER = config('COLLECTOR_CREDENTIAL_USER')
|
)
|
||||||
COLLECTOR_CREDENTIAL_PASSWORD = config('COLLECTOR_CREDENTIAL_PASSWORD')
|
|
||||||
|
COLLECTOR_HOSTNAME = config("COLLECTOR_HOSTNAME")
|
||||||
|
COLLECTOR_PORT = config("COLLECTOR_PORT", default="5432")
|
||||||
|
COLLECTOR_CREDENTIAL_USER = config("COLLECTOR_CREDENTIAL_USER")
|
||||||
|
COLLECTOR_CREDENTIAL_PASSWORD = config("COLLECTOR_CREDENTIAL_PASSWORD")
|
||||||
QUOTED_COLLECTOR_CREDENTIAL_PASSWORD = parse.quote(str(COLLECTOR_CREDENTIAL_PASSWORD))
|
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}'
|
|
||||||
SQLALCHEMY_COLLECTOR_URI = f'postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}'
|
# Deal w
|
||||||
TIMEZONE = config('TIMEZONE')
|
SQLALCHEMY_DATABASE_URI = f"postgresql+asyncpg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||||
MAXIMO_BASE_URL = config('MAXIMO_BASE_URL')
|
SQLALCHEMY_COLLECTOR_URI = f"postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}"
|
||||||
MAXIMO_API_KEY = config('MAXIMO_API_KEY')
|
|
||||||
AUTH_SERVICE_API = config('AUTH_SERVICE_API')
|
|
||||||
REALIBILITY_SERVICE_API = config('REALIBILITY_SERVICE_API')
|
TIMEZONE = "Asia/Jakarta"
|
||||||
RBD_SERVICE_API = config('RBD_SERVICE_API')
|
|
||||||
TEMPORAL_URL = config('TEMPORAL_URL')
|
MAXIMO_BASE_URL = config("MAXIMO_BASE_URL", default="http://example.com")
|
||||||
BASE_URL = config('BASE_URL')
|
MAXIMO_API_KEY = config("MAXIMO_API_KEY", default="keys")
|
||||||
API_KEY = config('API_KEY')
|
|
||||||
TR_RBD_ID = config('TR_RBD_ID')
|
AUTH_SERVICE_API = config("AUTH_SERVICE_API", default="http://192.168.1.82:8000/auth")
|
||||||
TC_RBD_ID = config('TC_RBD_ID')
|
REALIBILITY_SERVICE_API = config("REALIBILITY_SERVICE_API", default="http://192.168.1.82:8000/reliability")
|
||||||
DEFAULT_TC_ID = config('DEFAULT_TC_ID')
|
RBD_SERVICE_API = config("RBD_SERVICE_API", default="http://192.168.1.82:8000/rbd")
|
||||||
REDIS_HOST = config('REDIS_HOST')
|
TEMPORAL_URL = config("TEMPORAL_URL", default="http://192.168.1.86:7233")
|
||||||
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", default="0KFvcB7zWENyKVjoma9FKZNofVSViEshYr59zEQNGaYjyUP34gCJKDuqHuk9VfvE")
|
||||||
|
|
||||||
|
TR_RBD_ID = config("TR_RBD_ID", default="f04f365e-25d8-4036-87c2-ba1bfe1f9229")
|
||||||
|
TC_RBD_ID = config("TC_RBD_ID", default="f8523cb0-dc3c-4edb-bcf1-eea7b62582f1")
|
||||||
|
DEFAULT_TC_ID = config("DEFAULT_TC_ID", default="44f483f3-bfe4-4094-a59f-b97a10f2fea6")
|
||||||
|
|
||||||
|
TEMPORAL_URL
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import secrets
|
|
||||||
import redis
|
|
||||||
from fastapi import HTTPException, Request
|
|
||||||
import src.config as config
|
|
||||||
from src.context import get_user_id
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
_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:
|
|
||||||
user = getattr(request.state, 'user', None)
|
|
||||||
user_id = str(user.user_id) if user else get_user_id()
|
|
||||||
if not user_id:
|
|
||||||
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')
|
|
||||||
if not csrf_token_header:
|
|
||||||
log.warning('[CSRF] Missing X-CSRF-Token header | user_id=%s | IP: %s', 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()
|
|
||||||
current_path = request.url.path
|
|
||||||
path_hash = hashlib.sha256(current_path.encode()).hexdigest()[:16]
|
|
||||||
redis_key = f'csrf:{user_id}:{path_hash}'
|
|
||||||
stored_token_hash = _redis_auth.get(redis_key)
|
|
||||||
if not stored_token_hash:
|
|
||||||
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')
|
|
||||||
raise HTTPException(status_code=403, detail='CSRF token expired or invalid')
|
|
||||||
if isinstance(stored_token_hash, bytes):
|
|
||||||
stored_token_hash = stored_token_hash.decode('utf-8')
|
|
||||||
incoming_hash = hashlib.sha256(csrf_token_header.encode()).hexdigest()
|
|
||||||
if not secrets.compare_digest(incoming_hash, stored_token_hash):
|
|
||||||
log.warning('[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)
|
|
||||||
raise HTTPException(status_code=403, detail='CSRF token verification failed')
|
|
||||||
_redis_auth.delete(redis_key)
|
|
||||||
log.info('[CSRF] Verified & deleted | user_id=%s | path=%s', user_id, current_path)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,18 +1,22 @@
|
|||||||
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):
|
||||||
current_user: Optional[str] = Field(None, alias='currentUser')
|
# This ensures no extra query params are allowed
|
||||||
page: int = Field(1, gt=0, le=1000000)
|
current_user: Optional[str] = Field(None, alias="currentUser")
|
||||||
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias='itemsPerPage')
|
page: int = Field(1, gt=0, lt=2147483647)
|
||||||
query_str: Optional[str] = Field(None, alias='q')
|
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias="itemsPerPage")
|
||||||
filter_spec: Optional[str] = Field(None, alias='filter')
|
query_str: Optional[str] = Field(None, alias="q")
|
||||||
sort_by: List[str] = Field(default_factory=list, alias='sortBy[]')
|
filter_spec: Optional[str] = Field(None, alias="filter")
|
||||||
descending: List[bool] = Field(default_factory=list, alias='descending[]')
|
sort_by: List[str] = Field(default_factory=list, alias="sortBy[]")
|
||||||
exclude: List[str] = Field(default_factory=list, alias='exclude[]')
|
descending: List[bool] = Field(default_factory=list, alias="descending[]")
|
||||||
all_params: int = Field(0, alias='all', ge=0, le=1000000)
|
exclude: List[str] = Field(default_factory=list, alias="exclude[]")
|
||||||
|
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,58 +1,149 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
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)):
|
# allows only printable characters
|
||||||
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)}
|
QueryStr = constr(pattern=r"^[ -~]+$", min_length=1)
|
||||||
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,
|
||||||
|
}
|
||||||
|
|
||||||
def get_params_factory(model_type: Type[T]):
|
|
||||||
|
|
||||||
async def wrapper(db_session: DbSession, params: Annotated[model_type, Query()]):
|
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]):
|
||||||
|
async def wrapper(
|
||||||
|
db_session: DbSession,
|
||||||
|
params: Annotated[model_type, Query()] # type: ignore
|
||||||
|
):
|
||||||
res = params.model_dump()
|
res = params.model_dump()
|
||||||
return {'db_session': db_session, 'all': params.is_all, **res}
|
return {
|
||||||
|
"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'):
|
|
||||||
search.append(search_model.name.ilike(f'%{query_str}%'))
|
if hasattr(search_model, "name"):
|
||||||
|
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,12 +1,24 @@
|
|||||||
import sys
|
from enum import StrEnum
|
||||||
if sys.version_info >= (3, 11):
|
|
||||||
from enum import StrEnum
|
|
||||||
else:
|
|
||||||
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"
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,12 +1,21 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, 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
|
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||||
|
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,12 +1,27 @@
|
|||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
from fastapi import APIRouter, 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 .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.get('/{location_tag}', response_model=StandardResponse[List[Dict]])
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@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,21 +1,69 @@
|
|||||||
from typing import List, Optional
|
from datetime import datetime
|
||||||
from pydantic import Field
|
from typing import Any, Dict, List, Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from src.models import DefultBase, Pagination
|
from src.models import DefultBase, Pagination
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentActivityBase(DefultBase):
|
class ScopeEquipmentActivityBase(DefultBase):
|
||||||
assetnum: str = Field(..., description='Assetnum is required')
|
assetnum: str = Field(..., description="Assetnum is required")
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentActivityCreate(ScopeEquipmentActivityBase):
|
class ScopeEquipmentActivityCreate(ScopeEquipmentActivityBase):
|
||||||
name: str
|
name: str
|
||||||
cost: Optional[float] = Field(0, ge=0, le=1000000000000000)
|
cost: Optional[float] = Field(0)
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentActivityUpdate(ScopeEquipmentActivityBase):
|
class ScopeEquipmentActivityUpdate(ScopeEquipmentActivityBase):
|
||||||
name: Optional[str] = Field(None)
|
name: Optional[str] = Field(None)
|
||||||
cost: Optional[str] = Field(0)
|
cost: Optional[str] = Field(0)
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentActivityRead(ScopeEquipmentActivityBase):
|
class ScopeEquipmentActivityRead(ScopeEquipmentActivityBase):
|
||||||
name: str
|
name: str
|
||||||
cost: float = Field(..., ge=0, le=1000000000000000)
|
cost: float
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentActivityPagination(Pagination):
|
class ScopeEquipmentActivityPagination(Pagination):
|
||||||
items: List[ScopeEquipmentActivityRead] = []
|
items: List[ScopeEquipmentActivityRead] = []
|
||||||
|
|
||||||
|
|
||||||
|
# {
|
||||||
|
# "overview": {
|
||||||
|
# "totalEquipment": 30,
|
||||||
|
# "nextSchedule": {
|
||||||
|
# "date": "2025-01-12",
|
||||||
|
# "Overhaul": "B",
|
||||||
|
# "equipmentCount": 30
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "criticalParts": [
|
||||||
|
# "Boiler feed pump",
|
||||||
|
# "Boiler reheater system",
|
||||||
|
# "Drum Level (Right) Root Valve A",
|
||||||
|
# "BCP A Discharge Valve",
|
||||||
|
# "BFPT A EXH Press HI Root VLV"
|
||||||
|
# ],
|
||||||
|
# "schedules": [
|
||||||
|
# {
|
||||||
|
# "date": "2025-01-12",
|
||||||
|
# "Overhaul": "B",
|
||||||
|
# "status": "upcoming"
|
||||||
|
# }
|
||||||
|
# // ... other scheduled overhauls
|
||||||
|
# ],
|
||||||
|
# "systemComponents": {
|
||||||
|
# "boiler": {
|
||||||
|
# "status": "operational",
|
||||||
|
# "lastOverhaul": "2024-06-15"
|
||||||
|
# },
|
||||||
|
# "turbine": {
|
||||||
|
# "hpt": { "status": "operational" },
|
||||||
|
# "ipt": { "status": "operational" },
|
||||||
|
# "lpt": { "status": "operational" }
|
||||||
|
# }
|
||||||
|
# // ... other major components
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|||||||
@ -1,29 +1,218 @@
|
|||||||
|
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({'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)})
|
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),
|
||||||
|
})
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,11 +1,22 @@
|
|||||||
from sqlalchemy import UUID, Column, ForeignKey, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, 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
|
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||||
|
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')
|
|
||||||
equipment = relationship('StandardScope', lazy='raise', primaryjoin='and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))', uselist=False)
|
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, # Add this if it's a one-to-one relationship
|
||||||
|
)
|
||||||
|
|||||||
@ -1,22 +1,51 @@
|
|||||||
from typing import Optional
|
from typing import Dict, List, Optional
|
||||||
from fastapi import APIRouter, Query
|
|
||||||
from src.database.service import CommonParameters, DbSession
|
from fastapi import APIRouter, HTTPException, Query, status
|
||||||
|
|
||||||
|
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])
|
|
||||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
@router.get(
|
||||||
|
"/{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')
|
|
||||||
|
|
||||||
@router.post('/{assetnum}', response_model=StandardResponse[None])
|
return StandardResponse(
|
||||||
async def create_scope_equipment_jobs(db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
data=results,
|
||||||
|
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')
|
|
||||||
|
|
||||||
@router.post('/delete/{scope_job_id}', response_model=StandardResponse[None])
|
return StandardResponse(
|
||||||
|
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,32 +1,80 @@
|
|||||||
from typing import List, Optional
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pydantic import Field
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from src.workscope_group.schema import ActivityMasterRead
|
from src.workscope_group.schema import ActivityMasterRead
|
||||||
from src.models import DefultBase, Pagination
|
from src.models import DefultBase, Pagination
|
||||||
from src.overhaul_scope.schema import ScopeRead
|
from src.overhaul_scope.schema import ScopeRead
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentJobBase(DefultBase):
|
class ScopeEquipmentJobBase(DefultBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ScopeEquipmentJobCreate(ScopeEquipmentJobBase):
|
class ScopeEquipmentJobCreate(ScopeEquipmentJobBase):
|
||||||
job_ids: Optional[List[UUID]] = []
|
job_ids: Optional[List[UUID]] = []
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentJobUpdate(ScopeEquipmentJobBase):
|
class ScopeEquipmentJobUpdate(ScopeEquipmentJobBase):
|
||||||
name: Optional[str] = Field(None)
|
name: Optional[str] = Field(None)
|
||||||
cost: Optional[str] = Field(0)
|
cost: Optional[str] = Field(0)
|
||||||
|
|
||||||
|
|
||||||
class OverhaulActivity(DefultBase):
|
class OverhaulActivity(DefultBase):
|
||||||
id: UUID
|
id: UUID
|
||||||
overhaul_scope: ScopeRead
|
overhaul_scope: ScopeRead
|
||||||
|
|
||||||
|
|
||||||
class OverhaulJob(DefultBase):
|
class OverhaulJob(DefultBase):
|
||||||
id: UUID
|
id: UUID
|
||||||
overhaul_activity: OverhaulActivity
|
overhaul_activity: OverhaulActivity
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentJobRead(ScopeEquipmentJobBase):
|
class ScopeEquipmentJobRead(ScopeEquipmentJobBase):
|
||||||
id: UUID
|
id: UUID
|
||||||
workscope_group: Optional[ActivityMasterRead]
|
workscope_group: Optional[ActivityMasterRead]
|
||||||
location_tag: str
|
location_tag: str
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentJobPagination(Pagination):
|
class ScopeEquipmentJobPagination(Pagination):
|
||||||
items: List[ScopeEquipmentJobRead] = []
|
items: List[ScopeEquipmentJobRead] = []
|
||||||
|
|
||||||
|
|
||||||
|
# {
|
||||||
|
# "overview": {
|
||||||
|
# "totalEquipment": 30,
|
||||||
|
# "nextSchedule": {
|
||||||
|
# "date": "2025-01-12",
|
||||||
|
# "Overhaul": "B",
|
||||||
|
# "equipmentCount": 30
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "criticalParts": [
|
||||||
|
# "Boiler feed pump",
|
||||||
|
# "Boiler reheater system",
|
||||||
|
# "Drum Level (Right) Root Valve A",
|
||||||
|
# "BCP A Discharge Valve",
|
||||||
|
# "BFPT A EXH Press HI Root VLV"
|
||||||
|
# ],
|
||||||
|
# "schedules": [
|
||||||
|
# {
|
||||||
|
# "date": "2025-01-12",
|
||||||
|
# "Overhaul": "B",
|
||||||
|
# "status": "upcoming"
|
||||||
|
# }
|
||||||
|
# // ... other scheduled overhauls
|
||||||
|
# ],
|
||||||
|
# "systemComponents": {
|
||||||
|
# "boiler": {
|
||||||
|
# "status": "operational",
|
||||||
|
# "lastOverhaul": "2024-06-15"
|
||||||
|
# },
|
||||||
|
# "turbine": {
|
||||||
|
# "hpt": { "status": "operational" },
|
||||||
|
# "ipt": { "status": "operational" },
|
||||||
|
# "lpt": { "status": "operational" }
|
||||||
|
# }
|
||||||
|
# // ... other major components
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|||||||
@ -1,46 +1,151 @@
|
|||||||
|
import random
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Delete, Select, and_
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from src.auth.service import CurrentUser
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import search_filter_sort_paginate
|
from src.database.service import CommonParameters, 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
|
|
||||||
|
|
||||||
async def get_all(*, common, location_tag: str, scope: Optional[str]=None):
|
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
||||||
query = Select(EquipmentWorkscopeGroup).where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
# """Returns a document based on the given document id."""
|
||||||
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
# 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))
|
||||||
|
)
|
||||||
|
|
||||||
if scope:
|
if scope:
|
||||||
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)
|
query
|
||||||
|
.join(EquipmentWorkscopeGroup.workscope_group)
|
||||||
|
.join(MasterActivity.oh_types)
|
||||||
|
.join(WorkscopeOHType.oh_type)
|
||||||
|
.filter(MaintenanceType.name == scope)
|
||||||
|
)
|
||||||
|
|
||||||
async def create(*, db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
results = await search_filter_sort_paginate(model=query, **common)
|
||||||
|
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(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
raise HTTPException(
|
||||||
|
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,133 @@
|
|||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.config import LOG_LEVEL
|
||||||
|
from src.enums import OptimumOHEnum
|
||||||
|
|
||||||
|
|
||||||
|
LOG_FORMAT_DEBUG = "%(levelname)s:%(message)s:%(pathname)s:%(funcName)s:%(lineno)d"
|
||||||
|
|
||||||
|
# ANSI Color Codes
|
||||||
|
RESET = "\033[0m"
|
||||||
|
COLORS = {
|
||||||
|
"DEBUG": "\033[36m", # Cyan
|
||||||
|
"INFO": "\033[32m", # Green
|
||||||
|
"WARNING": "\033[33m", # Yellow
|
||||||
|
"WARN": "\033[33m", # Yellow
|
||||||
|
"ERROR": "\033[31m", # Red
|
||||||
|
"CRITICAL": "\033[1;31m", # Bold Red
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LogLevels(OptimumOHEnum):
|
||||||
|
info = "INFO"
|
||||||
|
warn = "WARN"
|
||||||
|
error = "ERROR"
|
||||||
|
debug = "DEBUG"
|
||||||
|
|
||||||
|
|
||||||
|
class JSONFormatter(logging.Formatter):
|
||||||
|
"""
|
||||||
|
Custom formatter to output logs in JSON format.
|
||||||
|
"""
|
||||||
|
def format(self, record):
|
||||||
|
from src.context import get_request_id, get_user_id, get_username, get_role
|
||||||
|
|
||||||
|
request_id = None
|
||||||
|
user_id = None
|
||||||
|
username = None
|
||||||
|
role = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
request_id = get_request_id()
|
||||||
|
user_id = get_user_id()
|
||||||
|
username = get_username()
|
||||||
|
role = get_role()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Standard fields from requirements
|
||||||
|
log_record = {
|
||||||
|
"timestamp": datetime.datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"level": record.levelname,
|
||||||
|
"message": record.getMessage(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add Context information if available
|
||||||
|
if request_id:
|
||||||
|
log_record["request_id"] = request_id
|
||||||
|
|
||||||
|
# Add Error context if available
|
||||||
|
if hasattr(record, "error_id"):
|
||||||
|
log_record["error_id"] = record.error_id
|
||||||
|
elif "error_id" in record.__dict__:
|
||||||
|
log_record["error_id"] = record.error_id
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
log_record["user_id"] = user_id
|
||||||
|
|
||||||
|
# Add any extra attributes passed to the log call
|
||||||
|
standard_attrs = {
|
||||||
|
"args", "asctime", "created", "exc_info", "exc_text", "filename",
|
||||||
|
"funcName", "levelname", "levelno", "lineno", "module", "msecs",
|
||||||
|
"message", "msg", "name", "pathname", "process", "processName",
|
||||||
|
"relativeCreated", "stack_info", "thread", "threadName", "error_id"
|
||||||
|
}
|
||||||
|
for key, value in record.__dict__.items():
|
||||||
|
if key not in standard_attrs and not key.startswith("_"):
|
||||||
|
log_record[key] = value
|
||||||
|
|
||||||
|
log_json = json.dumps(log_record)
|
||||||
|
|
||||||
|
# Apply color if the output is a terminal
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
level_color = COLORS.get(record.levelname, "")
|
||||||
|
return f"{level_color}{log_json}{RESET}"
|
||||||
|
|
||||||
|
return log_json
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging():
|
||||||
|
log_level = str(LOG_LEVEL).upper() # cast to string
|
||||||
|
log_levels = list(LogLevels)
|
||||||
|
|
||||||
|
if log_level not in log_levels:
|
||||||
|
log_level = LogLevels.error
|
||||||
|
|
||||||
|
# Get the root logger
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(log_level)
|
||||||
|
|
||||||
|
# Clear existing handlers to avoid duplicate logs
|
||||||
|
if root_logger.hasHandlers():
|
||||||
|
root_logger.handlers.clear()
|
||||||
|
|
||||||
|
# Create a stream handler that outputs to stdout
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
# Use JSONFormatter for all environments, or could be conditional
|
||||||
|
# For now, let's assume the user wants JSON everywhere as requested
|
||||||
|
formatter = JSONFormatter()
|
||||||
|
|
||||||
|
# If debug mode is specifically requested and we want the old format for debug:
|
||||||
|
# if log_level == LogLevels.debug:
|
||||||
|
# formatter = logging.Formatter(LOG_FORMAT_DEBUG)
|
||||||
|
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
|
||||||
|
# Reconfigure uvicorn loggers to use our JSON formatter
|
||||||
|
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]:
|
||||||
|
logger = logging.getLogger(logger_name)
|
||||||
|
logger.handlers = []
|
||||||
|
logger.propagate = True
|
||||||
|
|
||||||
|
# Silence the chatty uvicorn access logs as we have custom middleware logging
|
||||||
|
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# sometimes the slack client can be too verbose
|
||||||
|
logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL)
|
||||||
@ -1,153 +1,247 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
|
||||||
import time
|
import time
|
||||||
from src.context import set_request_id, reset_request_id, get_request_id
|
from src.context import set_request_id, reset_request_id, get_request_id
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from os import path
|
||||||
|
from typing import Final, Optional
|
||||||
from uuid import uuid1
|
from uuid import uuid1
|
||||||
from fastapi import Depends, FastAPI, HTTPException
|
|
||||||
from src.auth.service import JWTBearer
|
from fastapi import FastAPI, HTTPException, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import ValidationError
|
||||||
|
from slowapi import _rate_limit_exceeded_handler
|
||||||
from slowapi.errors import RateLimitExceeded
|
from slowapi.errors import RateLimitExceeded
|
||||||
|
from slowapi.middleware import SlowAPIMiddleware
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.ext.asyncio import async_scoped_session
|
from sqlalchemy.ext.asyncio import async_scoped_session
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from sqlalchemy.orm import scoped_session
|
||||||
|
from starlette.middleware.base import (BaseHTTPMiddleware,
|
||||||
|
RequestResponseEndpoint)
|
||||||
from starlette.middleware.gzip import GZipMiddleware
|
from starlette.middleware.gzip import GZipMiddleware
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import Response
|
from starlette.responses import FileResponse, Response, StreamingResponse
|
||||||
import uuid as _uuid_module
|
from starlette.routing import compile_path
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
|
||||||
from src.api import api_router
|
from src.api import api_router
|
||||||
from src.database.core import async_session, async_collector_session
|
from src.database.core import async_session, engine, async_collector_session
|
||||||
from src.enums import ResponseStatus
|
from src.enums import ResponseStatus
|
||||||
from src.exceptions import handle_exception
|
from src.exceptions import handle_exception
|
||||||
from src.app_logging import configure_logging
|
from src.logging import configure_logging
|
||||||
from src.middleware import RequestValidationMiddleware, RequestTimeoutMiddleware
|
from src.middleware import RequestValidationMiddleware
|
||||||
|
from src.rate_limiter import limiter
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
from src.csrf_protect import csrf_protect
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# we configure the logging level and format
|
||||||
configure_logging()
|
configure_logging()
|
||||||
REQUEST_TIMEOUT_SECONDS = 20
|
|
||||||
app = FastAPI(openapi_url='', title='Optimum OH API', description="Welcome to Optimum OH's API documentation!", version='0.1.0')
|
from src.config import DEBUG
|
||||||
|
|
||||||
def _sync_rate_limit_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
|
# we create the ASGI for the app
|
||||||
error_id = str(_uuid_module.uuid4())
|
app = FastAPI(
|
||||||
request.state.log_status_code = 429
|
openapi_url="",
|
||||||
request.state.log_error_id = error_id
|
title="LCCA API",
|
||||||
request.state.log_error_type = 'Too Many Requests'
|
description="Welcome to LCCA's API documentation!",
|
||||||
request.state.log_error_message = 'Please try again later'
|
version="0.1.0",
|
||||||
return JSONResponse(status_code=429, content={'data': None, 'message': f'Please try again later. Error ID: {error_id}', 'status': ResponseStatus.ERROR})
|
debug=bool(DEBUG),
|
||||||
|
)
|
||||||
|
|
||||||
|
# we define the exception handlers
|
||||||
app.add_exception_handler(Exception, handle_exception)
|
app.add_exception_handler(Exception, handle_exception)
|
||||||
app.add_exception_handler(HTTPException, handle_exception)
|
app.add_exception_handler(HTTPException, handle_exception)
|
||||||
app.add_exception_handler(StarletteHTTPException, handle_exception)
|
app.add_exception_handler(StarletteHTTPException, handle_exception)
|
||||||
app.add_exception_handler(RequestValidationError, handle_exception)
|
app.add_exception_handler(RequestValidationError, handle_exception)
|
||||||
app.add_exception_handler(RateLimitExceeded, _sync_rate_limit_handler)
|
app.add_exception_handler(RateLimitExceeded, handle_exception)
|
||||||
|
|
||||||
|
app.state.limiter = limiter
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
app.add_middleware(RequestTimeoutMiddleware, timeout=REQUEST_TIMEOUT_SECONDS)
|
app.add_middleware(SlowAPIMiddleware)
|
||||||
|
|
||||||
|
# credentials: "include",
|
||||||
|
|
||||||
|
|
||||||
@app.route('/slow', methods=['GET'])
|
|
||||||
async def slow_endpoint(request: Request):
|
|
||||||
await asyncio.sleep(REQUEST_TIMEOUT_SECONDS + 10)
|
|
||||||
return JSONResponse(content={'message': 'This should timeout'}, status_code=200)
|
|
||||||
|
|
||||||
@app.post('/testcsrf', dependencies=[Depends(JWTBearer()), Depends(csrf_protect)])
|
|
||||||
async def test_csrf(request: Request):
|
|
||||||
return JSONResponse(content={'data': None, 'message': 'CSRF token is valid', 'status': ResponseStatus.SUCCESS}, status_code=200)
|
|
||||||
|
|
||||||
def security_headers_middleware(app: FastAPI):
|
def security_headers_middleware(app: FastAPI):
|
||||||
is_production = False
|
is_production = False
|
||||||
csp_policy = {'default-src': "'self'", 'script-src': "'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net", 'style-src': "'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net", 'img-src': "'self' data: https: blob:", 'font-src': "'self' https://fonts.gstatic.com data:", 'connect-src': "'self' https://api.your-domain.com wss://ws.your-domain.com", 'frame-src': "'none'", 'object-src': "'none'", 'base-uri': "'self'", 'form-action': "'self'"}
|
|
||||||
feature_policy = {'geolocation': "'none'", 'midi': "'none'", 'notifications': "'none'", 'push': "'none'", 'sync-xhr': "'none'", 'microphone': "'none'", 'camera': "'none'", 'magnetometer': "'none'", 'gyroscope': "'none'", 'speaker': "'none'", 'vibrate': "'none'", 'fullscreen': "'self'", 'payment': "'none'"}
|
|
||||||
csp_header_value = '; '.join((f'{k} {v}' for k, v in csp_policy.items()))
|
|
||||||
feature_header_value = '; '.join((f'{k}={v}' for k, v in feature_policy.items()))
|
|
||||||
|
|
||||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
# CSP rules
|
||||||
|
csp_policy = {
|
||||||
|
"default-src": "'self'",
|
||||||
|
"script-src": "'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net",
|
||||||
|
"style-src": "'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net",
|
||||||
|
"img-src": "'self' data: https: blob:",
|
||||||
|
"font-src": "'self' https://fonts.gstatic.com data:",
|
||||||
|
"connect-src": "'self' https://api.your-domain.com wss://ws.your-domain.com",
|
||||||
|
"frame-src": "'none'",
|
||||||
|
"object-src": "'none'",
|
||||||
|
"base-uri": "'self'",
|
||||||
|
"form-action": "'self'",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Feature / Permissions Policy
|
||||||
|
feature_policy = {
|
||||||
|
"geolocation": "'none'",
|
||||||
|
"midi": "'none'",
|
||||||
|
"notifications": "'none'",
|
||||||
|
"push": "'none'",
|
||||||
|
"sync-xhr": "'none'",
|
||||||
|
"microphone": "'none'",
|
||||||
|
"camera": "'none'",
|
||||||
|
"magnetometer": "'none'",
|
||||||
|
"gyroscope": "'none'",
|
||||||
|
"speaker": "'none'",
|
||||||
|
"vibrate": "'none'",
|
||||||
|
"fullscreen": "'self'",
|
||||||
|
"payment": "'none'",
|
||||||
|
}
|
||||||
|
|
||||||
|
csp_header_value = "; ".join(f"{k} {v}" for k, v in csp_policy.items())
|
||||||
|
feature_header_value = "; ".join(f"{k}={v}" for k, v in feature_policy.items())
|
||||||
|
|
||||||
|
# Middleware definition
|
||||||
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
response: Response = await call_next(request)
|
response: Response = await call_next(request)
|
||||||
|
|
||||||
if is_production:
|
if is_production:
|
||||||
response.headers['Strict-Transport-Security'] = 'max-age=15724800; includeSubDomains; preload'
|
response.headers["Strict-Transport-Security"] = "max-age=15724800; includeSubDomains; preload"
|
||||||
response.headers['X-Frame-Options'] = 'DENY'
|
response.headers["X-Frame-Options"] = "DENY"
|
||||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||||
response.headers['Content-Security-Policy'] = csp_header_value
|
response.headers["Content-Security-Policy"] = csp_header_value
|
||||||
response.headers['Permissions-Policy'] = feature_header_value
|
response.headers["Permissions-Policy"] = feature_header_value
|
||||||
else:
|
else:
|
||||||
response.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' *"
|
# Relaxed settings for development
|
||||||
|
response.headers["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' *"
|
||||||
|
# You can skip some headers here for local testing
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
app.add_middleware(SecurityHeadersMiddleware)
|
app.add_middleware(SecurityHeadersMiddleware)
|
||||||
|
|
||||||
security_headers_middleware(app)
|
security_headers_middleware(app)
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(RequestValidationMiddleware)
|
app.add_middleware(RequestValidationMiddleware)
|
||||||
|
|
||||||
@app.middleware('http')
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
async def db_session_middleware(request: Request, call_next):
|
async def db_session_middleware(request: Request, call_next):
|
||||||
request_id = str(uuid1())
|
request_id = str(uuid1())
|
||||||
|
|
||||||
|
# we create a per-request id such that we can ensure that our session is scoped for a particular request.
|
||||||
|
# see: https://github.com/tiangolo/fastapi/issues/726
|
||||||
ctx_token = set_request_id(request_id)
|
ctx_token = set_request_id(request_id)
|
||||||
request.state.request_id = request_id
|
|
||||||
try:
|
try:
|
||||||
session = async_scoped_session(async_session, scopefunc=get_request_id)
|
session = async_scoped_session(async_session, scopefunc=get_request_id)
|
||||||
request.state.db = session()
|
request.state.db = session()
|
||||||
|
|
||||||
collector_session = async_scoped_session(async_collector_session, scopefunc=get_request_id)
|
collector_session = async_scoped_session(async_collector_session, scopefunc=get_request_id)
|
||||||
request.state.collector_db = collector_session()
|
request.state.collector_db = collector_session()
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
try:
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
except Exception as exc:
|
process_time = (time.time() - start_time) * 1000
|
||||||
response = await handle_exception(request, exc)
|
|
||||||
duration_ms = round((time.time() - start_time) * 1000, 2)
|
# Skip logging in middleware if it's an error (already logged in handle_exception)
|
||||||
timed_out = request.scope.get('x_timed_out', False)
|
if response.status_code >= 400:
|
||||||
if timed_out:
|
return response
|
||||||
status_code = 408
|
|
||||||
error_id = request.scope.get('x_timeout_error_id', None)
|
from src.context import get_username, get_role, get_user_id, set_user_id, set_username, set_role
|
||||||
error_type = 'Request Timeout'
|
|
||||||
error_message = 'Request timeout over 20 seconds'
|
# Pull from context or fallback to request.state.user
|
||||||
else:
|
username = get_username()
|
||||||
status_code = getattr(request.state, 'log_status_code', None) or response.status_code
|
role = get_role()
|
||||||
error_id = getattr(request.state, 'log_error_id', None)
|
|
||||||
error_type = getattr(request.state, 'log_error_type', None)
|
|
||||||
error_message = getattr(request.state, 'log_error_message', None)
|
|
||||||
_FALLBACK_ERROR_TYPES = {400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 408: 'Request Timeout', 409: 'Conflict', 422: 'Unprocessable Entity', 429: 'Too Many Requests', 500: 'Internal Server Error'}
|
|
||||||
_FALLBACK_ERROR_MESSAGES = {400: 'Bad request', 401: 'Invalid authorization token', 403: 'Forbidden Access', 404: 'Resource not found', 408: 'Request timeout', 409: 'This record already exists', 422: 'Input validation failed', 429: 'Please try again later', 500: 'An unexpected error occurred'}
|
|
||||||
if status_code >= 400 and (not error_type):
|
|
||||||
error_type = _FALLBACK_ERROR_TYPES.get(status_code, f'HTTP {status_code}')
|
|
||||||
if status_code >= 400 and (not error_message):
|
|
||||||
error_message = _FALLBACK_ERROR_MESSAGES.get(status_code, 'Request failed')
|
|
||||||
from src.context import get_user_id, set_user_id
|
|
||||||
user_id = get_user_id()
|
user_id = get_user_id()
|
||||||
user_obj = getattr(request.state, 'user', None)
|
|
||||||
if user_obj and (not user_id):
|
user_obj = getattr(request.state, "user", None)
|
||||||
|
if user_obj:
|
||||||
|
# message is UserBase dict/obj in this project
|
||||||
if isinstance(user_obj, dict):
|
if isinstance(user_obj, dict):
|
||||||
u_id = user_obj.get('user_id')
|
u_id = user_obj.get("user_id")
|
||||||
|
u_name = user_obj.get("name") or user_obj.get("username")
|
||||||
|
u_role = user_obj.get("role")
|
||||||
else:
|
else:
|
||||||
u_id = getattr(user_obj, 'user_id', None)
|
u_id = getattr(user_obj, "user_id", None)
|
||||||
if u_id:
|
u_name = getattr(user_obj, "name", None) or getattr(user_obj, "username", None)
|
||||||
|
u_role = getattr(user_obj, "role", None)
|
||||||
|
|
||||||
|
if not user_id and u_id:
|
||||||
user_id = str(u_id)
|
user_id = str(u_id)
|
||||||
set_user_id(user_id)
|
set_user_id(user_id)
|
||||||
headers = request.headers
|
if not username and u_name:
|
||||||
if 'X-Real-IP' in headers:
|
username = u_name
|
||||||
user_ip = headers['X-Real-IP']
|
set_username(username)
|
||||||
elif 'X-Forwarded-For' in headers:
|
if not role and u_role:
|
||||||
user_ip = headers['X-Forwarded-For'].split(',')[0].strip()
|
role = u_role
|
||||||
else:
|
set_role(role)
|
||||||
user_ip = request.client.host if request.client else None
|
|
||||||
path = request.url.path
|
user_info_str = ""
|
||||||
if request.url.query:
|
if user_id:
|
||||||
path = f'{path}?{request.url.query}'
|
user_info_str = f" | User ID: {user_id}"
|
||||||
result = 'Request completed' if status_code < 400 else 'Request failed'
|
|
||||||
if status_code >= 500:
|
error_id = getattr(request.state, "error_id", None)
|
||||||
log_level = logging.ERROR
|
log_msg = f"HTTP {request.method} {request.url.path} completed in {round(process_time, 2)}ms{user_info_str}"
|
||||||
elif status_code >= 400:
|
if error_id:
|
||||||
log_level = logging.WARNING
|
log_msg += f" | Error ID: {error_id}"
|
||||||
else:
|
|
||||||
log_level = logging.INFO
|
log.info(
|
||||||
log.log(log_level, f'{request.method} {path} {status_code}', extra={'method': request.method, 'path': path, 'status_code': status_code, 'duration_ms': duration_ms, 'request_id': request_id, 'user_id': user_id, 'user_ip': user_ip, 'error_id': error_id, 'result': result, 'error_type': error_type if status_code >= 400 else None, 'error_message': error_message if status_code >= 400 else None})
|
log_msg,
|
||||||
|
extra={
|
||||||
|
"method": request.method,
|
||||||
|
"path": request.url.path,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"duration_ms": round(process_time, 2),
|
||||||
|
"user_id": user_id,
|
||||||
|
"error_id": error_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
await request.state.db.close()
|
await request.state.db.close()
|
||||||
await request.state.collector_db.close()
|
await request.state.collector_db.close()
|
||||||
|
|
||||||
reset_request_id(ctx_token)
|
reset_request_id(ctx_token)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.middleware('http')
|
|
||||||
|
@app.middleware("http")
|
||||||
async def add_security_headers(request: Request, call_next):
|
async def add_security_headers(request: Request, call_next):
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000 ; includeSubDomains'
|
response.headers["Strict-Transport-Security"] = (
|
||||||
|
"max-age=31536000 ; includeSubDomains"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# class MetricsMiddleware(BaseHTTPMiddleware):
|
||||||
|
# async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
||||||
|
# method = request.method
|
||||||
|
# endpoint = request.url.path
|
||||||
|
# tags = {"method": method, "endpoint": endpoint}
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# start = time.perf_counter()
|
||||||
|
# response = await call_next(request)
|
||||||
|
# elapsed_time = time.perf_counter() - start
|
||||||
|
# tags.update({"status_code": response.status_code})
|
||||||
|
# metric_provider.counter("server.call.counter", tags=tags)
|
||||||
|
# metric_provider.timer("server.call.elapsed", value=elapsed_time, tags=tags)
|
||||||
|
# log.debug(f"server.call.elapsed.{endpoint}: {elapsed_time}")
|
||||||
|
# except Exception as e:
|
||||||
|
# metric_provider.counter("server.call.exception.counter", tags=tags)
|
||||||
|
# raise e from None
|
||||||
|
# return response
|
||||||
|
|
||||||
|
|
||||||
|
# app.add_middleware(ExceptionMiddleware)
|
||||||
|
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,37 +1,332 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from sqlalchemy import select, text
|
from sqlalchemy import select, func, cast, Numeric, 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 (\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 ")
|
query = text("""WITH part_costs AS (
|
||||||
|
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({'location_tag': row.asset_location, 'avg_cost': row.avg_cost})
|
data.append({
|
||||||
return {item['location_tag']: item['avg_cost'] for item in data}
|
"location_tag": row.asset_location,
|
||||||
|
"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({'location_tag': row.asset_location, 'avg_cost': row.avg_cost})
|
data.append({
|
||||||
return {item['location_tag']: item['avg_cost'] for item in data}
|
"location_tag": row.asset_location,
|
||||||
|
"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 ")
|
|
||||||
results = await collector_db_session.execute(sql_query, {'parent_wo_num': parent_wo_num})
|
sql_query = text("""
|
||||||
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]
|
WITH target_wos AS (
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1 +1,46 @@
|
|||||||
|
# import logging
|
||||||
|
|
||||||
|
# from dispatch.plugins.base import plugins
|
||||||
|
|
||||||
|
# from .config import METRIC_PROVIDERS
|
||||||
|
|
||||||
|
# log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
# class Metrics(object):
|
||||||
|
# _providers = []
|
||||||
|
|
||||||
|
# def __init__(self):
|
||||||
|
# if not METRIC_PROVIDERS:
|
||||||
|
# log.info(
|
||||||
|
# "No metric providers defined via METRIC_PROVIDERS env var. Metrics will not be sent."
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# self._providers = METRIC_PROVIDERS
|
||||||
|
|
||||||
|
# def gauge(self, name, value, tags=None):
|
||||||
|
# for provider in self._providers:
|
||||||
|
# log.debug(
|
||||||
|
# f"Sending gauge metric {name} to provider {provider}. Value: {value} Tags: {tags}"
|
||||||
|
# )
|
||||||
|
# p = plugins.get(provider)
|
||||||
|
# p.gauge(name, value, tags=tags)
|
||||||
|
|
||||||
|
# def counter(self, name, value=None, tags=None):
|
||||||
|
# for provider in self._providers:
|
||||||
|
# log.debug(
|
||||||
|
# f"Sending counter metric {name} to provider {provider}. Value: {value} Tags: {tags}"
|
||||||
|
# )
|
||||||
|
# p = plugins.get(provider)
|
||||||
|
# p.counter(name, value=value, tags=tags)
|
||||||
|
|
||||||
|
# def timer(self, name, value, tags=None):
|
||||||
|
# for provider in self._providers:
|
||||||
|
# log.debug(
|
||||||
|
# f"Sending timer metric {name} to provider {provider}. Value: {value} Tags: {tags}"
|
||||||
|
# )
|
||||||
|
# p = plugins.get(provider)
|
||||||
|
# p.timer(name, value, tags=tags)
|
||||||
|
|
||||||
|
|
||||||
|
# provider = Metrics()
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
# Empty init file
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
|
from src.auth.service import Token
|
||||||
|
from src.config import TC_RBD_ID
|
||||||
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
|
from src.overhaul_scope.service import get_all
|
||||||
|
from src.standard_scope.model import StandardScope
|
||||||
|
from src.workorder.model import MasterWorkOrder
|
||||||
|
|
||||||
|
from src.calculation_time_constrains.schema import (
|
||||||
|
CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
)
|
||||||
|
from src.optimum_time_constraint.service import (
|
||||||
|
get_calculation_data_by_id,
|
||||||
|
get_calculation_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_create_calculation_parameters(
|
||||||
|
*, db_session: DbSession, calculation_id: Optional[str] = None
|
||||||
|
):
|
||||||
|
if calculation_id is not None:
|
||||||
|
calculation = await get_calculation_data_by_id(
|
||||||
|
calculation_id=calculation_id, db_session=db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
if not calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return CalculationTimeConstrainsParametersRead(
|
||||||
|
costPerFailure=calculation.parameter.avg_failure_cost,
|
||||||
|
overhaulCost=calculation.parameter.overhaul_cost,
|
||||||
|
reference=calculation,
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
StandardScope,
|
||||||
|
func.avg(MasterWorkOrder.total_cost_max).label("average_cost"),
|
||||||
|
)
|
||||||
|
.outerjoin(MasterWorkOrder, StandardScope.location_tag == MasterWorkOrder.location_tag)
|
||||||
|
.group_by(StandardScope.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await db_session.execute(stmt)
|
||||||
|
costFailure = results.all()
|
||||||
|
scopes = await get_all(db_session=db_session)
|
||||||
|
avaiableScopes = {scope.id: scope.scope_name for scope in scopes}
|
||||||
|
costFailurePerScope = {
|
||||||
|
avaiableScopes.get(costPerFailure[0]): costPerFailure[1]
|
||||||
|
for costPerFailure in costFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return CalculationTimeConstrainsParametersRetrive(
|
||||||
|
costPerFailure=costFailurePerScope,
|
||||||
|
availableScopes=avaiableScopes.values(),
|
||||||
|
recommendedScope="A",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_calculation(
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||||
|
created_by: str,
|
||||||
|
simulation_id
|
||||||
|
):
|
||||||
|
from temporalio.client import Client
|
||||||
|
from src.config import TEMPORAL_URL
|
||||||
|
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||||
|
|
||||||
|
workflow_id = f"optimum-oh-calc-{uuid.uuid4()}"
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"token": token.token if hasattr(token, 'token') else str(token),
|
||||||
|
"calculation_in": calculation_time_constrains_in.model_dump(mode="json"),
|
||||||
|
"created_by": created_by
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = await temporal_client.start_workflow(
|
||||||
|
OptimumOHCalculationWorkflow.run,
|
||||||
|
args,
|
||||||
|
id=workflow_id,
|
||||||
|
task_queue="oh-task-queue"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": workflow_id,
|
||||||
|
"status": "success",
|
||||||
|
"message": "Calculation workflow started successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_calculation(
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
simulation_id: Optional[str] = None
|
||||||
|
):
|
||||||
|
calculation_data = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not calculation_data:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Calculation not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
rbd_simulation_id = simulation_id or calculation_data.rbd_simulation_id or TC_RBD_ID
|
||||||
|
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from src.calculation_time_constrains.model import CalculationEquipmentResult, CalculationResult
|
||||||
|
|
||||||
|
await db_session.execute(
|
||||||
|
delete(CalculationResult).where(CalculationResult.calculation_data_id == calculation_data.id)
|
||||||
|
)
|
||||||
|
await db_session.execute(
|
||||||
|
delete(CalculationEquipmentResult).where(CalculationEquipmentResult.calculation_data_id == calculation_data.id)
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||||
|
await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calculation_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
simulation_id=rbd_simulation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return await get_calculation_result(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
token=token,
|
||||||
|
include_risk_cost=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_or_create_scope_equipment_calculation(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
scope_calculation_id,
|
||||||
|
calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]
|
||||||
|
):
|
||||||
|
scope_calculation = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=scope_calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not scope_calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return scope_calculation.id
|
||||||
@ -0,0 +1,917 @@
|
|||||||
|
import asyncio
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import httpx
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from src.config import REALIBILITY_SERVICE_API, RBD_SERVICE_API
|
||||||
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
|
from src.calculation_time_constrains.model import CalculationData, CalculationEquipmentResult
|
||||||
|
from src.calculation_time_constrains.schema import CalculationResultsRead
|
||||||
|
from src.calculation_time_constrains.utils import (
|
||||||
|
calculate_failures_per_month,
|
||||||
|
create_time_series_data,
|
||||||
|
get_months_between,
|
||||||
|
)
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
from src.sparepart.service import load_sparepart_data_from_db
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OptimumCostModelWithSpareparts:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
last_oh_date: date,
|
||||||
|
next_oh_date: date,
|
||||||
|
sparepart_manager,
|
||||||
|
time_window_months: Optional[int] = None,
|
||||||
|
base_url: str = "http://192.168.1.82:8000",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the Optimum Cost Model with sparepart management
|
||||||
|
"""
|
||||||
|
self.api_base_url = base_url
|
||||||
|
self.token = token
|
||||||
|
self.last_oh_date = last_oh_date
|
||||||
|
self.next_oh_date = next_oh_date
|
||||||
|
self.session = None
|
||||||
|
self.sparepart_manager = sparepart_manager
|
||||||
|
|
||||||
|
# Calculate planned overhaul interval in months
|
||||||
|
self.planned_oh_months = get_months_between(last_oh_date, next_oh_date)
|
||||||
|
|
||||||
|
# Set analysis time window: next OH + 6 months
|
||||||
|
self.time_window_months = time_window_months or (self.planned_oh_months + 6)
|
||||||
|
|
||||||
|
# Pre-calculate date range for API calls
|
||||||
|
self.date_range = self._generate_date_range()
|
||||||
|
|
||||||
|
self.logger = log
|
||||||
|
|
||||||
|
def _generate_date_range(self) -> List[datetime.datetime]:
|
||||||
|
"""Generate date range for analysis based on time window"""
|
||||||
|
dates = []
|
||||||
|
current_date = datetime.datetime.combine(self.last_oh_date, datetime.datetime.min.time())
|
||||||
|
end_date = current_date + timedelta(days=self.time_window_months * 30)
|
||||||
|
|
||||||
|
while current_date <= end_date:
|
||||||
|
dates.append(current_date)
|
||||||
|
current_date += timedelta(days=31)
|
||||||
|
|
||||||
|
return dates
|
||||||
|
|
||||||
|
async def _create_session(self):
|
||||||
|
"""Create aiohttp session with connection pooling"""
|
||||||
|
if self.session is None:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=300)
|
||||||
|
connector = aiohttp.TCPConnector(
|
||||||
|
limit=500,
|
||||||
|
limit_per_host=200,
|
||||||
|
ttl_dns_cache=300,
|
||||||
|
use_dns_cache=True,
|
||||||
|
force_close=False,
|
||||||
|
enable_cleanup_closed=True,
|
||||||
|
)
|
||||||
|
self.session = aiohttp.ClientSession(
|
||||||
|
timeout=timeout,
|
||||||
|
connector=connector,
|
||||||
|
headers={"Authorization": f"Bearer {self.token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _close_session(self):
|
||||||
|
"""Close aiohttp session"""
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
async def get_failures_prediction(self, location_tag: str):
|
||||||
|
"""Get failure predictions for equipment from Reliability Predict service"""
|
||||||
|
|
||||||
|
start_date = self.last_oh_date.strftime("%Y-%m-%d")
|
||||||
|
end_date_val = self.next_oh_date + timedelta(days=6 * 30)
|
||||||
|
end_date = end_date_val.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
predict_url = f"{REALIBILITY_SERVICE_API}/main/predict/{location_tag}/{start_date}/{end_date}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
predict_url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
prediction_data = response.json()
|
||||||
|
except (requests.RequestException, ValueError) as e:
|
||||||
|
self.logger.error(f"Failed to fetch prediction data for {location_tag}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
predictions = (
|
||||||
|
prediction_data.get("data", {}).get("predictions")
|
||||||
|
if prediction_data.get("data")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not predictions:
|
||||||
|
self.logger.warning(f"No prediction data available for {location_tag}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
monthly_data = {}
|
||||||
|
cumulative = 0.0
|
||||||
|
for i, pred in enumerate(predictions):
|
||||||
|
month_key = pred["month"]
|
||||||
|
monthly_fail = pred["predicted_failures"]
|
||||||
|
source = pred.get("source", "predicted")
|
||||||
|
cumulative += monthly_fail
|
||||||
|
monthly_data[month_key] = {
|
||||||
|
"cumulative_failures": cumulative,
|
||||||
|
"monthly_failures": monthly_fail,
|
||||||
|
"month_index": i + 1,
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
|
||||||
|
return monthly_data
|
||||||
|
|
||||||
|
async def get_simulation_results(self, simulation_id: str = "default"):
|
||||||
|
"""Get simulation results for Birnbaum importance calculations"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode"
|
||||||
|
plant_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||||
|
calc_task = client.get(calc_result_url, headers=headers)
|
||||||
|
plant_task = client.get(plant_result_url, headers=headers)
|
||||||
|
|
||||||
|
calc_response, plant_response = await asyncio.gather(calc_task, plant_task)
|
||||||
|
|
||||||
|
calc_response.raise_for_status()
|
||||||
|
plant_response.raise_for_status()
|
||||||
|
|
||||||
|
calc_data = calc_response.json()["data"]
|
||||||
|
plant_data = plant_response.json()["data"]
|
||||||
|
|
||||||
|
return {"calc_result": calc_data, "plant_result": plant_data}
|
||||||
|
|
||||||
|
def _calculate_equipment_costs_with_spareparts(
|
||||||
|
self,
|
||||||
|
failures_prediction: Dict,
|
||||||
|
birnbaum_importance: float,
|
||||||
|
preventive_cost: float,
|
||||||
|
failure_replacement_cost: float,
|
||||||
|
ecs,
|
||||||
|
location_tag: str,
|
||||||
|
planned_overhauls: List = None,
|
||||||
|
loss_production_permonth=0,
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Calculate costs for each month including sparepart costs and availability"""
|
||||||
|
|
||||||
|
if not failures_prediction:
|
||||||
|
self.logger.warning(f"No failure prediction data for {location_tag}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
results = []
|
||||||
|
months = list(failures_prediction.keys())
|
||||||
|
num_months = len(months)
|
||||||
|
failure_counts = []
|
||||||
|
|
||||||
|
monthly_risk_cost_per_failure = 0
|
||||||
|
|
||||||
|
if ecs:
|
||||||
|
is_trip = 1 if ecs.get("Diskripsi Operasional Akibat Equip. Failure") == "Trip" else 0
|
||||||
|
is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance)
|
||||||
|
if is_trip:
|
||||||
|
downtime = ecs.get("Estimasi Waktu Maint. / Downtime / Gangguan (Jam)")
|
||||||
|
monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series
|
||||||
|
|
||||||
|
for month_key in months:
|
||||||
|
data = failures_prediction[month_key]
|
||||||
|
failure_counts.append(data["cumulative_failures"])
|
||||||
|
|
||||||
|
for i in range(num_months):
|
||||||
|
month_index = i + 1
|
||||||
|
|
||||||
|
if month_index > self.time_window_months:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sparepart_analysis = self._analyze_sparepart_availability(
|
||||||
|
location_tag, month_index - 1, planned_overhauls or []
|
||||||
|
)
|
||||||
|
|
||||||
|
total_expected_failure_cost = failure_counts[i] * (
|
||||||
|
failure_replacement_cost + monthly_risk_cost_per_failure
|
||||||
|
)
|
||||||
|
|
||||||
|
procurement_cost = sparepart_analysis["total_procurement_cost"]
|
||||||
|
total_preventive_cost = preventive_cost + procurement_cost
|
||||||
|
|
||||||
|
total_cycle_cost = total_expected_failure_cost + total_preventive_cost
|
||||||
|
|
||||||
|
cput = total_cycle_cost / month_index
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"month": month_index,
|
||||||
|
"number_of_failures": failure_counts[i],
|
||||||
|
"is_actual": failures_prediction[months[i]].get("source") == "actual",
|
||||||
|
"failure_cost": total_expected_failure_cost / month_index,
|
||||||
|
"preventive_cost": preventive_cost / month_index,
|
||||||
|
"procurement_cost": procurement_cost / month_index,
|
||||||
|
"total_cost": cput,
|
||||||
|
"absolute_failure_cost": total_expected_failure_cost,
|
||||||
|
"absolute_overhaul_cost": preventive_cost,
|
||||||
|
"absolute_procurement_cost": procurement_cost,
|
||||||
|
"total_cycle_cost": total_cycle_cost,
|
||||||
|
"is_after_planned_oh": month_index > self.planned_oh_months,
|
||||||
|
"delay_months": max(0, month_index - self.planned_oh_months),
|
||||||
|
"procurement_details": sparepart_analysis,
|
||||||
|
"sparepart_available": sparepart_analysis["available"],
|
||||||
|
"sparepart_status": sparepart_analysis["message"],
|
||||||
|
"can_proceed": sparepart_analysis["can_proceed_with_delays"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _analyze_sparepart_availability(
|
||||||
|
self, equipment_tag: str, target_month: int, planned_overhauls: List
|
||||||
|
) -> Dict:
|
||||||
|
"""Analyze sparepart availability for equipment at target month"""
|
||||||
|
if not self.sparepart_manager:
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"message": "Sparepart manager not initialized",
|
||||||
|
"total_procurement_cost": 0,
|
||||||
|
"procurement_needed": [],
|
||||||
|
"can_proceed_with_delays": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
other_overhauls = [
|
||||||
|
(eq_tag, month)
|
||||||
|
for eq_tag, month in planned_overhauls
|
||||||
|
if eq_tag != equipment_tag and month <= target_month
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.sparepart_manager.check_sparepart_availability(
|
||||||
|
equipment_tag, target_month, other_overhauls
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_optimal_timing_with_spareparts(
|
||||||
|
self, cost_results: List[Dict], location_tag: str
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""Find optimal timing considering sparepart constraints"""
|
||||||
|
if not cost_results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
feasible_results = [r for r in cost_results if r["can_proceed"]]
|
||||||
|
|
||||||
|
min_cost = float("inf")
|
||||||
|
optimal_result = None
|
||||||
|
|
||||||
|
for i, result in enumerate(cost_results):
|
||||||
|
if result in feasible_results and result["total_cost"] < min_cost:
|
||||||
|
min_cost = result["total_cost"]
|
||||||
|
optimal_result = result
|
||||||
|
|
||||||
|
if optimal_result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._create_optimal_result(optimal_result, location_tag, "OPTIMAL")
|
||||||
|
|
||||||
|
def _create_optimal_result(
|
||||||
|
self, optimal_result: Dict, location_tag: str, status: str
|
||||||
|
) -> Dict:
|
||||||
|
"""Create standardized optimal result dictionary"""
|
||||||
|
return {
|
||||||
|
"location_tag": location_tag,
|
||||||
|
"optimal_month": optimal_result["month"],
|
||||||
|
"optimal_index": optimal_result["month"] - 1,
|
||||||
|
"optimal_cost": optimal_result["total_cost"],
|
||||||
|
"failure_cost": optimal_result["failure_cost"],
|
||||||
|
"preventive_cost": optimal_result["preventive_cost"],
|
||||||
|
"procurement_cost": optimal_result["procurement_cost"],
|
||||||
|
"number_of_failures": optimal_result["number_of_failures"],
|
||||||
|
"is_delayed": optimal_result["is_after_planned_oh"],
|
||||||
|
"delay_months": optimal_result["delay_months"],
|
||||||
|
"planned_oh_month": self.planned_oh_months,
|
||||||
|
"planned_cost": None,
|
||||||
|
"cost_vs_planned": None,
|
||||||
|
"savings_from_delay": 0,
|
||||||
|
"cost_of_delay": 0,
|
||||||
|
"sparepart_available": optimal_result["sparepart_available"],
|
||||||
|
"sparepart_status": optimal_result["sparepart_status"],
|
||||||
|
"procurement_details": optimal_result["procurement_details"],
|
||||||
|
"optimization_status": status,
|
||||||
|
"all_monthly_costs": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def calculate_cost_all_equipment_with_spareparts(
|
||||||
|
self,
|
||||||
|
db_session,
|
||||||
|
collector_db_session,
|
||||||
|
equipments: List,
|
||||||
|
calculation,
|
||||||
|
preventive_cost: float,
|
||||||
|
simulation_id: str = "default",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Calculate optimal overhaul timing for entire fleet considering sparepart constraints
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"Starting fleet optimization with reliability prediction for {len(equipments)} equipment"
|
||||||
|
)
|
||||||
|
max_interval = self.time_window_months
|
||||||
|
|
||||||
|
# Phase 1: Calculate individual optimal timings without considering interactions
|
||||||
|
individual_results = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("src/calculation_time_constrains/full_equipment_with_downtime_opdesc.json", "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
ecs_tags = {eq["Location"]: eq for eq in data}
|
||||||
|
except FileNotFoundError:
|
||||||
|
ecs_tags = {}
|
||||||
|
|
||||||
|
for equipment in equipments:
|
||||||
|
location_tag = equipment.location_tag
|
||||||
|
contribution_factor = 1.0
|
||||||
|
ecs = ecs_tags.get(location_tag, None)
|
||||||
|
|
||||||
|
monthly_data = await self.get_failures_prediction(location_tag)
|
||||||
|
|
||||||
|
if not monthly_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
equipment_preventive_cost = equipment.overhaul_cost + equipment.service_cost
|
||||||
|
failure_replacement_cost = equipment.material_cost + (3 * 111000 * 3)
|
||||||
|
|
||||||
|
cost_results = self._calculate_equipment_costs_with_spareparts(
|
||||||
|
failures_prediction=monthly_data,
|
||||||
|
birnbaum_importance=contribution_factor,
|
||||||
|
preventive_cost=equipment_preventive_cost,
|
||||||
|
failure_replacement_cost=failure_replacement_cost,
|
||||||
|
location_tag=location_tag,
|
||||||
|
planned_overhauls=[],
|
||||||
|
ecs=ecs,
|
||||||
|
loss_production_permonth=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not cost_results:
|
||||||
|
continue
|
||||||
|
|
||||||
|
optimal_timing = self._find_optimal_timing_with_spareparts(cost_results, location_tag)
|
||||||
|
|
||||||
|
if optimal_timing:
|
||||||
|
optimal_timing["all_monthly_costs"] = cost_results
|
||||||
|
individual_results[location_tag] = optimal_timing
|
||||||
|
self.logger.info(
|
||||||
|
f"Individual optimal for {location_tag}: Month {optimal_timing['optimal_month']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 2: Optimize considering sparepart interactions
|
||||||
|
self.logger.info("Phase 2: Optimizing with sparepart interactions...")
|
||||||
|
|
||||||
|
improved_plan = self._optimize_fleet_with_sparepart_constraints(
|
||||||
|
individual_results, equipments, simulation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 3: Generate final results and database objects
|
||||||
|
fleet_results = []
|
||||||
|
total_corrective_costs = np.zeros(max_interval)
|
||||||
|
total_preventive_costs = np.zeros(max_interval)
|
||||||
|
total_procurement_costs = np.zeros(max_interval)
|
||||||
|
total_costs = np.zeros(max_interval)
|
||||||
|
|
||||||
|
total_fleet_procurement_cost = 0
|
||||||
|
|
||||||
|
for equipment in equipments:
|
||||||
|
location_tag = equipment.location_tag
|
||||||
|
|
||||||
|
if location_tag not in individual_results:
|
||||||
|
continue
|
||||||
|
|
||||||
|
equipment_timing = next(
|
||||||
|
(month for tag, month in improved_plan if tag == location_tag),
|
||||||
|
individual_results[location_tag]["optimal_month"],
|
||||||
|
)
|
||||||
|
|
||||||
|
cost_data = individual_results[location_tag]["all_monthly_costs"][equipment_timing - 1]
|
||||||
|
|
||||||
|
all_costs = individual_results[location_tag]["all_monthly_costs"]
|
||||||
|
|
||||||
|
corrective_costs = [r["failure_cost"] for r in all_costs]
|
||||||
|
preventive_costs = [r["preventive_cost"] for r in all_costs]
|
||||||
|
procurement_costs = [r["procurement_cost"] for r in all_costs]
|
||||||
|
failures = [r["number_of_failures"] for r in all_costs]
|
||||||
|
total_costs_equipment = [r["total_cost"] for r in all_costs]
|
||||||
|
procurement_details = [r["procurement_details"] for r in all_costs]
|
||||||
|
|
||||||
|
def pad_array(arr, target_length):
|
||||||
|
if len(arr) < target_length:
|
||||||
|
return arr + [arr[-1]] * (target_length - len(arr))
|
||||||
|
return arr[:target_length]
|
||||||
|
|
||||||
|
corrective_costs = pad_array(corrective_costs, max_interval)
|
||||||
|
preventive_costs = pad_array(preventive_costs, max_interval)
|
||||||
|
procurement_costs = pad_array(procurement_costs, max_interval)
|
||||||
|
failures = pad_array(failures, max_interval)
|
||||||
|
total_costs_equipment = pad_array(total_costs_equipment, max_interval)
|
||||||
|
procurement_details = pad_array(procurement_details, max_interval)
|
||||||
|
|
||||||
|
is_actual_list = [r.get("is_actual", False) for r in all_costs]
|
||||||
|
is_actual_list = pad_array(is_actual_list, max_interval)
|
||||||
|
|
||||||
|
equipment_result = CalculationEquipmentResult(
|
||||||
|
corrective_costs=corrective_costs,
|
||||||
|
overhaul_costs=preventive_costs,
|
||||||
|
procurement_costs=procurement_costs,
|
||||||
|
daily_failures=failures,
|
||||||
|
is_actual=is_actual_list,
|
||||||
|
location_tag=equipment.location_tag,
|
||||||
|
material_cost=equipment.material_cost,
|
||||||
|
service_cost=equipment.service_cost,
|
||||||
|
optimum_day=equipment_timing - 1,
|
||||||
|
calculation_data_id=calculation.id,
|
||||||
|
procurement_details=procurement_details,
|
||||||
|
)
|
||||||
|
|
||||||
|
fleet_results.append(equipment_result)
|
||||||
|
|
||||||
|
total_corrective_costs += np.array(corrective_costs)
|
||||||
|
total_preventive_costs += np.array(preventive_costs)
|
||||||
|
total_procurement_costs += np.array(procurement_costs)
|
||||||
|
total_costs += np.array(total_costs_equipment)
|
||||||
|
|
||||||
|
total_fleet_procurement_cost += cost_data["procurement_cost"]
|
||||||
|
|
||||||
|
fleet_optimal_index = np.argmin(total_costs)
|
||||||
|
|
||||||
|
calculation.optimum_oh_day = int(fleet_optimal_index)
|
||||||
|
calculation.max_interval = max_interval
|
||||||
|
calculation.rbd_simulation_id = simulation_id
|
||||||
|
|
||||||
|
db_session.add_all(fleet_results)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return int(fleet_optimal_index)
|
||||||
|
|
||||||
|
def _optimize_fleet_with_sparepart_constraints(
|
||||||
|
self, individual_results: Dict, equipments: List, simulation_id: str
|
||||||
|
) -> List[Tuple[str, int]]:
|
||||||
|
"""
|
||||||
|
Optimize fleet overhaul timing considering sparepart sharing constraints
|
||||||
|
"""
|
||||||
|
current_plan = [(tag, result["optimal_month"]) for tag, result in individual_results.items()]
|
||||||
|
current_plan.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
|
improved_plan = []
|
||||||
|
processed_equipment = []
|
||||||
|
|
||||||
|
for equipment_tag, optimal_month in current_plan:
|
||||||
|
sparepart_analysis = self.sparepart_manager.check_sparepart_availability(
|
||||||
|
equipment_tag, optimal_month - 1, processed_equipment
|
||||||
|
)
|
||||||
|
|
||||||
|
if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]:
|
||||||
|
improved_plan.append((equipment_tag, optimal_month))
|
||||||
|
processed_equipment.append((equipment_tag, optimal_month))
|
||||||
|
else:
|
||||||
|
alternative_month = self._find_alternative_timing(
|
||||||
|
equipment_tag,
|
||||||
|
optimal_month,
|
||||||
|
individual_results[equipment_tag]["all_monthly_costs"],
|
||||||
|
processed_equipment,
|
||||||
|
)
|
||||||
|
|
||||||
|
if alternative_month:
|
||||||
|
improved_plan.append((equipment_tag, alternative_month))
|
||||||
|
processed_equipment.append((equipment_tag, alternative_month))
|
||||||
|
else:
|
||||||
|
improved_plan.append((equipment_tag, optimal_month))
|
||||||
|
processed_equipment.append((equipment_tag, optimal_month))
|
||||||
|
|
||||||
|
return improved_plan
|
||||||
|
|
||||||
|
def _find_alternative_timing(
|
||||||
|
self,
|
||||||
|
equipment_tag: str,
|
||||||
|
preferred_month: int,
|
||||||
|
cost_results: List[Dict],
|
||||||
|
processed_equipment: List[Tuple[str, int]],
|
||||||
|
) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Find alternative timing when preferred month has sparepart constraints
|
||||||
|
"""
|
||||||
|
search_range = 6
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for offset in range(-search_range // 2, search_range // 2 + 1):
|
||||||
|
candidate_month = preferred_month + offset
|
||||||
|
|
||||||
|
if candidate_month <= 0 or candidate_month > len(cost_results):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if candidate_month == preferred_month:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sparepart_analysis = self.sparepart_manager.check_sparepart_availability(
|
||||||
|
equipment_tag, candidate_month - 1, processed_equipment
|
||||||
|
)
|
||||||
|
|
||||||
|
if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]:
|
||||||
|
cost_data = cost_results[candidate_month - 1]
|
||||||
|
candidates.append((candidate_month, cost_data["total_cost"]))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates.sort(key=lambda x: x[1])
|
||||||
|
return candidates[0][0]
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
await self._create_session()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self._close_session()
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_optimal_timing(
|
||||||
|
calculation_results: List, optimum_oh_day: int, prev_oh_scope, scope_overhaul
|
||||||
|
) -> Dict:
|
||||||
|
"""Analyze optimal timing and provide recommendations"""
|
||||||
|
|
||||||
|
if not calculation_results:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
optimal_result = None
|
||||||
|
if 0 <= optimum_oh_day < len(calculation_results):
|
||||||
|
optimal_result = calculation_results[optimum_oh_day]
|
||||||
|
|
||||||
|
planned_oh_months = None
|
||||||
|
if prev_oh_scope and scope_overhaul:
|
||||||
|
planned_oh_months = get_months_between(prev_oh_scope.end_date, scope_overhaul.start_date)
|
||||||
|
|
||||||
|
timing_recommendation = "OPTIMAL"
|
||||||
|
if planned_oh_months:
|
||||||
|
if optimum_oh_day + 1 < planned_oh_months:
|
||||||
|
timing_recommendation = "EARLY"
|
||||||
|
elif optimum_oh_day + 1 > planned_oh_months:
|
||||||
|
timing_recommendation = "DELAYED"
|
||||||
|
else:
|
||||||
|
timing_recommendation = "ON_SCHEDULE"
|
||||||
|
|
||||||
|
cost_trend = "STABLE"
|
||||||
|
if len(calculation_results) > 1:
|
||||||
|
early_costs = [r.total_cost for r in calculation_results[: len(calculation_results) // 3]]
|
||||||
|
late_costs = [r.total_cost for r in calculation_results[-len(calculation_results) // 3 :]]
|
||||||
|
|
||||||
|
avg_early = sum(early_costs) / len(early_costs) if early_costs else 0
|
||||||
|
avg_late = sum(late_costs) / len(late_costs) if late_costs else 0
|
||||||
|
|
||||||
|
if avg_late > avg_early * 1.2:
|
||||||
|
cost_trend = "INCREASING"
|
||||||
|
elif avg_late < avg_early * 0.8:
|
||||||
|
cost_trend = "DECREASING"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"optimal_month": optimum_oh_day + 1,
|
||||||
|
"planned_month": planned_oh_months,
|
||||||
|
"timing_recommendation": timing_recommendation,
|
||||||
|
"optimal_total_cost": optimal_result.total_cost if optimal_result else 0,
|
||||||
|
"optimal_breakdown": {
|
||||||
|
"corrective_cost": optimal_result.corrective_cost if optimal_result else 0,
|
||||||
|
"overhaul_cost": optimal_result.overhaul_cost if optimal_result else 0,
|
||||||
|
"procurement_cost": optimal_result.procurement_cost if optimal_result else 0,
|
||||||
|
"num_failures": optimal_result.num_failures if optimal_result else 0,
|
||||||
|
},
|
||||||
|
"cost_trend": cost_trend,
|
||||||
|
"months_from_planned": (optimum_oh_day + 1 - planned_oh_months)
|
||||||
|
if planned_oh_months
|
||||||
|
else None,
|
||||||
|
"cost_savings_vs_planned": None,
|
||||||
|
"sparepart_impact": {
|
||||||
|
"equipment_with_constraints": optimal_result.sparepart_summary["equipment_requiring_procurement"]
|
||||||
|
if optimal_result
|
||||||
|
else 0,
|
||||||
|
"critical_shortages": optimal_result.sparepart_summary["critical_shortages"]
|
||||||
|
if optimal_result
|
||||||
|
else 0,
|
||||||
|
"procurement_investment": optimal_result.sparepart_summary["total_procurement_cost"]
|
||||||
|
if optimal_result
|
||||||
|
else 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_simulation_with_spareparts(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation,
|
||||||
|
token: str,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
time_window_months: Optional[int] = None,
|
||||||
|
simulation_id: str = "default",
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Run complete overhaul optimization simulation with sparepart management
|
||||||
|
"""
|
||||||
|
from src.optimum_time_constraint.service import get_calculation_data_by_id
|
||||||
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||||
|
|
||||||
|
equipments = await get_standard_scope_by_session_id(
|
||||||
|
db_session=db_session,
|
||||||
|
overhaul_session_id=calculation.overhaul_session_id,
|
||||||
|
collector_db=collector_db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
scope = await get_scope(
|
||||||
|
db_session=db_session, overhaul_session_id=calculation.overhaul_session_id
|
||||||
|
)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
calculation_data = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=calculation.id
|
||||||
|
)
|
||||||
|
|
||||||
|
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||||
|
|
||||||
|
sparepart_manager = await load_sparepart_data_from_db(
|
||||||
|
scope=scope,
|
||||||
|
prev_oh_scope=prev_oh_scope,
|
||||||
|
db_session=collector_db_session,
|
||||||
|
app_db_session=db_session,
|
||||||
|
analysis_window_months=time_window_months,
|
||||||
|
)
|
||||||
|
|
||||||
|
optimum_oh_model = OptimumCostModelWithSpareparts(
|
||||||
|
token=token,
|
||||||
|
last_oh_date=prev_oh_scope.end_date,
|
||||||
|
next_oh_date=scope.start_date,
|
||||||
|
base_url=RBD_SERVICE_API,
|
||||||
|
sparepart_manager=sparepart_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fleet_optimal_index = await optimum_oh_model.calculate_cost_all_equipment_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
equipments=equipments,
|
||||||
|
calculation=calculation_data,
|
||||||
|
preventive_cost=calculation_data.parameter.overhaul_cost,
|
||||||
|
simulation_id=simulation_id,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await optimum_oh_model._close_session()
|
||||||
|
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(
|
||||||
|
selectinload(CalculationData.equipment_results),
|
||||||
|
selectinload(CalculationData.parameter),
|
||||||
|
)
|
||||||
|
.where(CalculationData.id == calculation.id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
data_num = scope_calculation.max_interval
|
||||||
|
all_equipment = scope_calculation.equipment_results
|
||||||
|
included_equipment = [eq for eq in all_equipment if eq.is_included]
|
||||||
|
|
||||||
|
calculation_results = []
|
||||||
|
fleet_statistics = {
|
||||||
|
"total_equipment": len(all_equipment),
|
||||||
|
"included_equipment": len(included_equipment),
|
||||||
|
"excluded_equipment": len(all_equipment) - len(included_equipment),
|
||||||
|
"equipment_with_sparepart_constraints": 0,
|
||||||
|
"total_procurement_items": 0,
|
||||||
|
"critical_procurement_items": 0,
|
||||||
|
"total_spareparts": 745,
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_failure_cost = (
|
||||||
|
sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment)
|
||||||
|
if all_equipment
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
rbd_marginal_fails = [0] * data_num
|
||||||
|
try:
|
||||||
|
if scope_calculation.rbd_simulation_id:
|
||||||
|
plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant"
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
plant_result_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
plant_data = response.json().get("data", {})
|
||||||
|
timestamp_outs = plant_data.get("timestamp_outs", [])
|
||||||
|
if timestamp_outs:
|
||||||
|
hourly_data = create_time_series_data(
|
||||||
|
timestamp_outs, max_hours=data_num * 720
|
||||||
|
)
|
||||||
|
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
||||||
|
rbd_fails_map = {m["month"]: m["failures"] for m in cumulative_rbd_fails}
|
||||||
|
prev_fail = 0
|
||||||
|
for m in range(1, data_num + 1):
|
||||||
|
curr_fail = rbd_fails_map.get(m, prev_fail)
|
||||||
|
rbd_marginal_fails[m - 1] = curr_fail - prev_fail
|
||||||
|
prev_fail = curr_fail
|
||||||
|
except Exception as e:
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning(f"Failed to fetch plant simulation: {e}")
|
||||||
|
|
||||||
|
cumulative_plant_failures = 0
|
||||||
|
|
||||||
|
for month_index in range(data_num):
|
||||||
|
historical_marginal_fail = 0
|
||||||
|
for eq in all_equipment:
|
||||||
|
if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]:
|
||||||
|
curr_fail = (
|
||||||
|
eq.daily_failures[month_index]
|
||||||
|
if month_index < len(eq.daily_failures)
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
prev_fail = (
|
||||||
|
eq.daily_failures[month_index - 1]
|
||||||
|
if month_index > 0 and (month_index - 1) < len(eq.daily_failures)
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
historical_marginal_fail += max(0, curr_fail - prev_fail)
|
||||||
|
|
||||||
|
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
||||||
|
cumulative_plant_failures += marginal_fail
|
||||||
|
|
||||||
|
month_result = {
|
||||||
|
"overhaul_cost": 0.0,
|
||||||
|
"corrective_cost": 0.0,
|
||||||
|
"procurement_cost": 0.0,
|
||||||
|
"num_failures": cumulative_plant_failures,
|
||||||
|
"day": month_index + 1,
|
||||||
|
"month": month_index + 1,
|
||||||
|
"procurement_details": {},
|
||||||
|
"sparepart_summary": {
|
||||||
|
"total_procurement_cost": 0.0,
|
||||||
|
"equipment_requiring_procurement": 0,
|
||||||
|
"critical_shortages": 0,
|
||||||
|
"existing_orders_value": 0.0,
|
||||||
|
"new_orders_required": 0,
|
||||||
|
"urgent_procurements": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
equipment_requiring_procurement = 0
|
||||||
|
total_existing_orders_value = 0.0
|
||||||
|
total_new_orders_value = 0.0
|
||||||
|
critical_shortages = 0
|
||||||
|
urgent_procurements = 0
|
||||||
|
|
||||||
|
for eq in all_equipment:
|
||||||
|
if month_index >= len(eq.procurement_details):
|
||||||
|
continue
|
||||||
|
procurement_detail = eq.procurement_details[month_index]
|
||||||
|
if (
|
||||||
|
procurement_detail
|
||||||
|
and isinstance(procurement_detail, dict)
|
||||||
|
and procurement_detail.get("procurement_needed")
|
||||||
|
):
|
||||||
|
equipment_requiring_procurement += 1
|
||||||
|
pr_po_summary = procurement_detail.get("pr_po_summary", {})
|
||||||
|
existing_orders_value = pr_po_summary.get("total_existing_value", 0)
|
||||||
|
total_existing_orders_value += existing_orders_value
|
||||||
|
new_orders_value = pr_po_summary.get("total_new_orders_value", 0)
|
||||||
|
total_new_orders_value += new_orders_value
|
||||||
|
critical_missing = procurement_detail.get("critical_missing_parts", 0)
|
||||||
|
if critical_missing > 0:
|
||||||
|
critical_shortages += 1
|
||||||
|
recommendations = procurement_detail.get("recommendations", [])
|
||||||
|
urgent_items = [
|
||||||
|
r for r in recommendations if r.get("priority") == "CRITICAL"
|
||||||
|
]
|
||||||
|
if urgent_items:
|
||||||
|
urgent_procurements += 1
|
||||||
|
is_included_eq = False if eq.is_initial else eq.is_included
|
||||||
|
month_result["procurement_details"][eq.location_tag] = {
|
||||||
|
"is_included": is_included_eq,
|
||||||
|
"location_tag": eq.location_tag,
|
||||||
|
"details": procurement_detail.get("procurement_needed", []),
|
||||||
|
"detailed_message": procurement_detail.get("detailed_message", ""),
|
||||||
|
"pr_po_summary": pr_po_summary,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"sparepart_available": procurement_detail.get("sparepart_available", True),
|
||||||
|
"can_proceed": procurement_detail.get("can_proceed_with_delays", True),
|
||||||
|
"critical_missing_parts": critical_missing,
|
||||||
|
"existing_orders_value": existing_orders_value,
|
||||||
|
"new_orders_value": new_orders_value,
|
||||||
|
}
|
||||||
|
if eq.is_included:
|
||||||
|
if month_index < len(eq.overhaul_costs) and month_index < len(
|
||||||
|
eq.procurement_costs
|
||||||
|
):
|
||||||
|
month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index])
|
||||||
|
month_result["procurement_cost"] += float(eq.procurement_costs[month_index])
|
||||||
|
|
||||||
|
month_result["corrective_cost"] = (
|
||||||
|
cumulative_plant_failures * avg_failure_cost
|
||||||
|
) / (month_index + 1)
|
||||||
|
month_result["sparepart_summary"].update(
|
||||||
|
{
|
||||||
|
"total_procurement_cost": month_result["procurement_cost"],
|
||||||
|
"equipment_requiring_procurement": equipment_requiring_procurement,
|
||||||
|
"critical_shortages": critical_shortages,
|
||||||
|
"existing_orders_value": total_existing_orders_value,
|
||||||
|
"new_orders_required": len(
|
||||||
|
[
|
||||||
|
eq
|
||||||
|
for eq in all_equipment
|
||||||
|
if month_index < len(eq.procurement_details)
|
||||||
|
and eq.procurement_details[month_index]
|
||||||
|
and eq.procurement_details[month_index].get("procurement_needed")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"urgent_procurements": urgent_procurements,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
month_result["total_cost"] = (
|
||||||
|
month_result["corrective_cost"]
|
||||||
|
+ month_result["overhaul_cost"]
|
||||||
|
+ month_result["procurement_cost"]
|
||||||
|
)
|
||||||
|
calculation_results.append(month_result)
|
||||||
|
|
||||||
|
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
||||||
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
||||||
|
|
||||||
|
fleet_statistics["equipment_with_sparepart_constraints"] = len(
|
||||||
|
[
|
||||||
|
eq
|
||||||
|
for eq in all_equipment
|
||||||
|
if any(
|
||||||
|
detail and detail.get("procurement_needed")
|
||||||
|
for detail in eq.procurement_details
|
||||||
|
if detail
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
fleet_statistics["total_procurement_items"] = sum(
|
||||||
|
[
|
||||||
|
len(detail.get("procurement_needed", []))
|
||||||
|
for eq in all_equipment
|
||||||
|
for detail in eq.procurement_details
|
||||||
|
if detail and isinstance(detail, dict)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
analysis_metadata = {
|
||||||
|
"planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12
|
||||||
|
+ (scope.start_date.month - prev_oh_scope.end_date.month)
|
||||||
|
if prev_oh_scope and scope
|
||||||
|
else 0,
|
||||||
|
"total_fleet_procurement_cost": sum(
|
||||||
|
[
|
||||||
|
eq.procurement_costs[int(scope_calculation.optimum_oh_day)]
|
||||||
|
for eq in all_equipment
|
||||||
|
if eq.procurement_costs
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None,
|
||||||
|
"next_oh_date": scope.start_date.isoformat() if scope else None,
|
||||||
|
"optimal_stat": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_results_read = [CalculationResultsRead(**r) for r in calculation_results]
|
||||||
|
optimal_analysis = _analyze_optimal_timing(
|
||||||
|
calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_calculation.plant_results = calculation_results
|
||||||
|
scope_calculation.fleet_statistics = fleet_statistics
|
||||||
|
scope_calculation.analysis_metadata = analysis_metadata
|
||||||
|
scope_calculation.optimum_analysis = optimal_analysis
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return {"id": calculation.id, "optimum": optimal_analysis}
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
from typing import Annotated, List, Optional, Union
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.params import Query
|
||||||
|
|
||||||
|
from src.auth.service import CurrentUser, InternalKey, Token
|
||||||
|
from src.config import DEFAULT_TC_ID
|
||||||
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
|
from src.models import StandardResponse
|
||||||
|
|
||||||
|
from src.optimum_time_constraint.flows import (
|
||||||
|
create_calculation,
|
||||||
|
get_create_calculation_parameters,
|
||||||
|
get_or_create_scope_equipment_calculation,
|
||||||
|
recalculate_calculation,
|
||||||
|
)
|
||||||
|
from src.calculation_time_constrains.schema import (
|
||||||
|
CalculationResultsRead,
|
||||||
|
CalculationSelectedEquipmentUpdate,
|
||||||
|
CalculationTimeConstrainsCreate,
|
||||||
|
CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
CalculationTimeConstrainsRead,
|
||||||
|
CreateCalculationQuery,
|
||||||
|
EquipmentResult,
|
||||||
|
CalculationTimeConstrainsReadNoResult,
|
||||||
|
)
|
||||||
|
from src.optimum_time_constraint.service import (
|
||||||
|
bulk_update_equipment,
|
||||||
|
get_calculation_result,
|
||||||
|
get_calculation_result_by_day,
|
||||||
|
get_calculation_by_assetnum,
|
||||||
|
get_all_calculations,
|
||||||
|
refresh_spareparts_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
get_calculation = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]]
|
||||||
|
)
|
||||||
|
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()],
|
||||||
|
):
|
||||||
|
"""Save calculation time constrains Here"""
|
||||||
|
scope_calculation_id = params.scope_calculation_id
|
||||||
|
simulation_id = params.simulation_id
|
||||||
|
|
||||||
|
if scope_calculation_id:
|
||||||
|
results = await get_or_create_scope_equipment_calculation(
|
||||||
|
db_session=db_session,
|
||||||
|
scope_calculation_id=scope_calculation_id,
|
||||||
|
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=results, message="Data created successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]]
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
"/parameters",
|
||||||
|
response_model=StandardResponse[
|
||||||
|
Union[
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def get_calculation_parameters(
|
||||||
|
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
|
||||||
|
):
|
||||||
|
"""Get all calculation parameter."""
|
||||||
|
|
||||||
|
parameters = await get_create_calculation_parameters(
|
||||||
|
db_session=db_session, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=parameters,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@get_calculation.get(
|
||||||
|
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
|
||||||
|
)
|
||||||
|
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":
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult]
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{calculation_id}/simulation",
|
||||||
|
response_model=StandardResponse[CalculationResultsRead],
|
||||||
|
)
|
||||||
|
async def get_simulation_result(
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_id,
|
||||||
|
calculation_simuation_in: CalculationTimeConstrainsCreate,
|
||||||
|
):
|
||||||
|
simulation_result = await get_calculation_result_by_day(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
simulation_day=calculation_simuation_in.intervalDays,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=simulation_result, message="Data retrieved successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]])
|
||||||
|
async def update_selected_equipment(
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_id,
|
||||||
|
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
|
||||||
|
):
|
||||||
|
if calculation_id == "default":
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=results,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{calculation_id}/refresh-spareparts", response_model=StandardResponse[dict])
|
||||||
|
async def refresh_spareparts(
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Refresh sparepart availability for an existing calculation"""
|
||||||
|
|
||||||
|
await refresh_spareparts_service(
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data={}, message="Spareparts refreshed successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{calculation_id}/recalculate", response_model=StandardResponse[CalculationTimeConstrainsRead])
|
||||||
|
async def recalculate_calculation_api(
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
token: Token,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Recalculate an existing simulation with fresh data"""
|
||||||
|
|
||||||
|
results = await recalculate_calculation(
|
||||||
|
token=token,
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=results, message="Calculation updated with fresh data")
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Coroutine, List, Optional, Tuple, Dict
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import and_, case, func, select, update
|
||||||
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
|
|
||||||
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
|
from src.workorder.model import MasterWorkOrder
|
||||||
|
from src.calculation_time_constrains.model import (CalculationData, CalculationEquipmentResult, CalculationResult)
|
||||||
|
from src.calculation_time_constrains.schema import (
|
||||||
|
CalculationResultsRead,
|
||||||
|
CalculationSelectedEquipmentUpdate,
|
||||||
|
CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsRead
|
||||||
|
)
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
|
||||||
|
|
||||||
|
async def create_param_and_data(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_param_in: CalculationTimeConstrainsParametersCreate,
|
||||||
|
created_by: str,
|
||||||
|
parameter_id: Optional[UUID] = None,
|
||||||
|
):
|
||||||
|
"""Creates a new document."""
|
||||||
|
if calculation_param_in.ohSessionId is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="overhaul_session_id is required",
|
||||||
|
)
|
||||||
|
|
||||||
|
calculationData = await CalculationData.create_with_param(
|
||||||
|
db=db_session,
|
||||||
|
overhaul_session_id=calculation_param_in.ohSessionId,
|
||||||
|
avg_failure_cost=calculation_param_in.costPerFailure,
|
||||||
|
overhaul_cost=calculation_param_in.overhaulCost,
|
||||||
|
created_by=created_by,
|
||||||
|
params_id=parameter_id,
|
||||||
|
)
|
||||||
|
return calculationData
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_result(db_session: DbSession, calculation_id: str, token, include_risk_cost):
|
||||||
|
"""
|
||||||
|
Get calculation results from DB, returning pre-calculated plant and equipment results.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get calculation data with equipment results
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.equipment_results))
|
||||||
|
.where(CalculationData.id == calculation_id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not scope_calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Calculation with id {calculation_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_overhaul = await get_scope(
|
||||||
|
db_session=db_session,
|
||||||
|
overhaul_session_id=scope_calculation.overhaul_session_id
|
||||||
|
)
|
||||||
|
if not scope_overhaul:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Overhaul scope for session {scope_calculation.overhaul_session_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse pre-calculated plant results
|
||||||
|
plant_results_raw = scope_calculation.plant_results or []
|
||||||
|
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
||||||
|
|
||||||
|
# Return comprehensive result
|
||||||
|
return CalculationTimeConstrainsRead(
|
||||||
|
id=scope_calculation.id,
|
||||||
|
reference=scope_calculation.overhaul_session_id,
|
||||||
|
scope=scope_overhaul.maintenance_type.name,
|
||||||
|
results=calculation_results,
|
||||||
|
optimum_oh=scope_calculation.optimum_oh_day,
|
||||||
|
optimum_oh_month=scope_calculation.optimum_oh_day + 1 if scope_calculation.optimum_oh_day is not None else None,
|
||||||
|
equipment_results=scope_calculation.equipment_results,
|
||||||
|
fleet_statistics=scope_calculation.fleet_statistics or {},
|
||||||
|
optimal_analysis=scope_calculation.optimum_analysis or {},
|
||||||
|
analysis_metadata=scope_calculation.analysis_metadata or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f"Error in get_calculation_result for calculation_id {calculation_id}: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Internal error processing calculation results: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_data_by_id(db_session: DbSession, calculation_id) -> CalculationData:
|
||||||
|
stmt = (
|
||||||
|
select(CalculationData)
|
||||||
|
.filter(CalculationData.id == calculation_id)
|
||||||
|
.options(
|
||||||
|
joinedload(CalculationData.equipment_results),
|
||||||
|
joinedload(CalculationData.parameter),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.unique().scalar()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_calculations(db_session: DbSession) -> List[CalculationData]:
|
||||||
|
stmt = (
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.session))
|
||||||
|
.where(
|
||||||
|
CalculationData.optimum_oh_day.isnot(None),
|
||||||
|
CalculationData.max_interval.isnot(None),
|
||||||
|
CalculationData.optimum_analysis.isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(CalculationData.created_at.desc())
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_by_assetnum(*, db_session: DbSession, assetnum: str, calculation_id: str):
|
||||||
|
stmt = (
|
||||||
|
select(CalculationEquipmentResult)
|
||||||
|
.where(CalculationEquipmentResult.assetnum == assetnum)
|
||||||
|
.where(CalculationEquipmentResult.calculation_data_id == calculation_id)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalar()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_by_reference_and_parameter(
|
||||||
|
*, db_session: DbSession, calculation_reference_id, parameter_id
|
||||||
|
):
|
||||||
|
stmt = select(CalculationData).filter(
|
||||||
|
and_(
|
||||||
|
CalculationData.reference_id == calculation_reference_id,
|
||||||
|
CalculationData.parameter_id == parameter_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalar()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_result_by_day(*, db_session: DbSession, calculation_id, simulation_day):
|
||||||
|
stmt = select(CalculationData).filter(CalculationData.id == calculation_id)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
calculation_data = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not calculation_data or not calculation_data.plant_results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for res in calculation_data.plant_results:
|
||||||
|
if res.get("day") == simulation_day:
|
||||||
|
return res
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_avg_cost_by_asset(*, db_session: DbSession, assetnum: str):
|
||||||
|
stmt = select(func.avg(MasterWorkOrder.total_cost_max).label("average_cost")).where(
|
||||||
|
MasterWorkOrder.assetnum == assetnum
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def bulk_update_equipment(
|
||||||
|
*,
|
||||||
|
db: DbSession,
|
||||||
|
selected_equipments: List[CalculationSelectedEquipmentUpdate],
|
||||||
|
calculation_data_id: UUID,
|
||||||
|
):
|
||||||
|
case_mappings = {asset.location_tag: asset.is_included for asset in selected_equipments}
|
||||||
|
location_tags = list(case_mappings.keys())
|
||||||
|
|
||||||
|
when_clauses = [
|
||||||
|
(CalculationEquipmentResult.location_tag == location_tag, is_included)
|
||||||
|
for location_tag, is_included in case_mappings.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
update(CalculationEquipmentResult)
|
||||||
|
.where(CalculationEquipmentResult.calculation_data_id == calculation_data_id)
|
||||||
|
.where(CalculationEquipmentResult.location_tag.in_(location_tags))
|
||||||
|
.values(
|
||||||
|
{
|
||||||
|
"is_included": case(*when_clauses),
|
||||||
|
"is_initial": False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(stmt)
|
||||||
|
await db.commit()
|
||||||
|
return location_tags
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_spareparts_service(db_session: DbSession, collector_db_session: CollectorDbSession, calculation_id: str):
|
||||||
|
stmt = select(CalculationData).where(CalculationData.id == calculation_id).options(
|
||||||
|
joinedload(CalculationData.equipment_results)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
calculation = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not calculation:
|
||||||
|
raise HTTPException(status_code=404, detail="Calculation not found")
|
||||||
|
|
||||||
|
scope = await get_scope(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
last_oh_date = prev_oh_scope.end_date
|
||||||
|
next_oh_date = scope.start_date
|
||||||
|
time_window_months = ((next_oh_date.year - last_oh_date.year) * 12 +
|
||||||
|
(next_oh_date.month - last_oh_date.month) + 6)
|
||||||
|
|
||||||
|
from src.sparepart.service import load_sparepart_data_from_db
|
||||||
|
sparepart_manager = await load_sparepart_data_from_db(
|
||||||
|
scope=scope,
|
||||||
|
prev_oh_scope=prev_oh_scope,
|
||||||
|
db_session=collector_db_session,
|
||||||
|
app_db_session=db_session,
|
||||||
|
analysis_window_months=time_window_months
|
||||||
|
)
|
||||||
|
|
||||||
|
for eq in calculation.equipment_results:
|
||||||
|
procurement_details = []
|
||||||
|
procurement_costs = []
|
||||||
|
for month_index in range(time_window_months):
|
||||||
|
status = sparepart_manager.check_sparepart_availability(eq.location_tag, month_index)
|
||||||
|
procurement_details.append(status)
|
||||||
|
|
||||||
|
if not status['available']:
|
||||||
|
procurement_costs.append(status['total_procurement_cost'])
|
||||||
|
else:
|
||||||
|
procurement_costs.append(0.0)
|
||||||
|
|
||||||
|
eq.procurement_details = procurement_details
|
||||||
|
eq.procurement_costs = procurement_costs
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import httpx
|
||||||
|
from temporalio import activity, workflow
|
||||||
|
|
||||||
|
from src.config import RBD_SERVICE_API
|
||||||
|
from src.database.core import get_main_session, get_collector_session
|
||||||
|
from src.optimum_time_constraint.service import (
|
||||||
|
create_param_and_data,
|
||||||
|
get_calculation_data_by_id
|
||||||
|
)
|
||||||
|
from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||||
|
from src.calculation_time_constrains.schema import CalculationTimeConstrainsParametersCreate
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
from src.calculation_time_constrains.utils import get_months_between
|
||||||
|
from src.calculation_target_reliability.utils import wait_for_workflow
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def create_optimum_oh_calculation(args: dict) -> str:
|
||||||
|
token = args["token"]
|
||||||
|
calc_in_dict = args["calculation_in"]
|
||||||
|
created_by = args["created_by"]
|
||||||
|
|
||||||
|
calc_in = CalculationTimeConstrainsParametersCreate(**calc_in_dict)
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
calc_data = await create_param_and_data(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_param_in=calc_in,
|
||||||
|
created_by=created_by
|
||||||
|
)
|
||||||
|
return str(calc_data.id)
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def request_rbd_simulation(args: dict) -> str:
|
||||||
|
calc_id = args["calc_id"]
|
||||||
|
token = args["token"]
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||||
|
|
||||||
|
scope = await get_scope(db_session=db_session, overhaul_session_id=calc_data.overhaul_session_id)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||||
|
sim_duration_hours = time_window_months * 720
|
||||||
|
|
||||||
|
sim_input = {
|
||||||
|
"SchematicName": "- TJB - Unit 3 -",
|
||||||
|
"SimulationName": f"OptimumOH_Calc_{calc_id}",
|
||||||
|
"SimDuration": sim_duration_hours,
|
||||||
|
"DurationUnit": "UHour",
|
||||||
|
"OffSet": 0,
|
||||||
|
"SimSeed": 99,
|
||||||
|
"SimNumRun": 1,
|
||||||
|
"OverhaulInterval": 0,
|
||||||
|
"MaintenanceOutages": 0,
|
||||||
|
"IsDefault": False,
|
||||||
|
"OverhaulDuration": 0,
|
||||||
|
"AhmJobId": None
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{RBD_SERVICE_API}/aeros/simulation/run",
|
||||||
|
json=sim_input,
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
sim_id = resp.json()["data"]
|
||||||
|
|
||||||
|
calc_data.rbd_simulation_id = sim_id
|
||||||
|
return str(sim_id)
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def wait_for_rbd_simulation(sim_id: str) -> str:
|
||||||
|
await wait_for_workflow(sim_id)
|
||||||
|
return sim_id
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def run_optimum_oh_calculation(args: dict) -> dict:
|
||||||
|
calc_id = args["calc_id"]
|
||||||
|
token = args["token"]
|
||||||
|
sim_id = args["sim_id"]
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
async with get_collector_session() as collector_db:
|
||||||
|
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||||
|
results = await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calc_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db,
|
||||||
|
simulation_id=sim_id
|
||||||
|
)
|
||||||
|
return {"id": str(results["id"]), "optimum": results["optimum"]}
|
||||||
|
|
||||||
|
@workflow.defn
|
||||||
|
class OptimumOHCalculationWorkflow:
|
||||||
|
def __init__(self):
|
||||||
|
self.done = False
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
@workflow.signal
|
||||||
|
def notify_done(self, result: dict):
|
||||||
|
self.done = True
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
@workflow.run
|
||||||
|
async def run(self, args: dict) -> dict:
|
||||||
|
calc_id = await workflow.execute_activity(
|
||||||
|
create_optimum_oh_calculation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
args["calc_id"] = calc_id
|
||||||
|
|
||||||
|
sim_id = await workflow.execute_activity(
|
||||||
|
request_rbd_simulation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=2)
|
||||||
|
)
|
||||||
|
|
||||||
|
args["sim_id"] = sim_id
|
||||||
|
|
||||||
|
# await workflow.execute_activity(
|
||||||
|
# wait_for_rbd_simulation,
|
||||||
|
# sim_id,
|
||||||
|
# start_to_close_timeout=timedelta(hours=2)
|
||||||
|
# )
|
||||||
|
await workflow.wait_condition(lambda: self.done)
|
||||||
|
|
||||||
|
result = await workflow.execute_activity(
|
||||||
|
run_optimum_oh_calculation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,35 +1,68 @@
|
|||||||
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, get_overhaul_overview, get_overhaul_schedules, get_overhaul_system_components
|
from src.overhaul.service import (get_overhaul_critical_parts,
|
||||||
|
get_overhaul_overview,
|
||||||
|
get_overhaul_schedules,
|
||||||
|
get_overhaul_system_components)
|
||||||
from src.overhaul_scope.schema import ScopeRead
|
from src.overhaul_scope.schema import ScopeRead
|
||||||
from .schema import OverhaulCriticalParts, OverhaulRead, OverhaulSystemComponents
|
|
||||||
from src.auth.access_control import required_permission, OHPermission
|
from .schema import (OverhaulCriticalParts, OverhaulRead,
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
OverhaulSystemComponents)
|
||||||
from fastapi import Depends
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@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):
|
@router.get("", response_model=StandardResponse[OverhaulRead])
|
||||||
|
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')
|
|
||||||
|
|
||||||
@router.get('/schedules', response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
return StandardResponse(
|
||||||
|
data=OverhaulRead(
|
||||||
|
overview=overview,
|
||||||
|
schedules=schedules,
|
||||||
|
criticalParts=criticalParts,
|
||||||
|
systemComponents=systemComponents,
|
||||||
|
),
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schedules", response_model=StandardResponse[List[ScopeRead]])
|
||||||
async def get_schedules():
|
async def get_schedules():
|
||||||
|
"""Get all overhaul schedules."""
|
||||||
schedules = get_overhaul_schedules()
|
schedules = get_overhaul_schedules()
|
||||||
return StandardResponse(data=schedules, message='Data retrieved successfully')
|
return StandardResponse(
|
||||||
|
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])
|
||||||
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(data=OverhaulCriticalParts(criticalParts=criticalParts), message='Data retrieved successfully')
|
return StandardResponse(
|
||||||
|
data=OverhaulCriticalParts(criticalParts=criticalParts),
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/system-components', response_model=StandardResponse[OverhaulSystemComponents], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
@router.get(
|
||||||
|
"/system-components", response_model=StandardResponse[OverhaulSystemComponents]
|
||||||
|
)
|
||||||
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(data=OverhaulSystemComponents(systemComponents=systemComponents), message='Data retrieved successfully')
|
return StandardResponse(
|
||||||
|
data=OverhaulSystemComponents(systemComponents=systemComponents),
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|||||||
@ -1,21 +1,72 @@
|
|||||||
from typing import Any, Dict, List
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from src.models import DefultBase, Pagination
|
||||||
from src.overhaul_scope.schema import ScopeRead
|
from src.overhaul_scope.schema import ScopeRead
|
||||||
|
|
||||||
|
|
||||||
class OverhaulBase(BaseModel):
|
class OverhaulBase(BaseModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OverhaulCriticalParts(OverhaulBase):
|
class OverhaulCriticalParts(OverhaulBase):
|
||||||
criticalParts: List[str] = Field(..., description='List of critical parts')
|
criticalParts: List[str] = Field(..., description="List of critical parts")
|
||||||
|
|
||||||
|
|
||||||
class OverhaulSchedules(OverhaulBase):
|
class OverhaulSchedules(OverhaulBase):
|
||||||
schedules: List[Dict[str, Any]] = Field(..., description='List of schedules')
|
schedules: List[Dict[str, Any]] = Field(..., description="List of schedules")
|
||||||
|
|
||||||
|
|
||||||
class OverhaulSystemComponents(OverhaulBase):
|
class OverhaulSystemComponents(OverhaulBase):
|
||||||
systemComponents: Dict[str, Any] = Field(..., description='List of system components')
|
systemComponents: Dict[str, Any] = Field(
|
||||||
|
..., description="List of system components"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OverhaulRead(OverhaulBase):
|
class OverhaulRead(OverhaulBase):
|
||||||
overview: Dict[str, Any]
|
overview: Dict[str, Any]
|
||||||
criticalParts: dict
|
criticalParts: dict
|
||||||
schedules: List[ScopeRead]
|
schedules: List[ScopeRead]
|
||||||
systemComponents: Dict[str, Any]
|
systemComponents: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
# {
|
||||||
|
# "overview": {
|
||||||
|
# "totalEquipment": 30,
|
||||||
|
# "nextSchedule": {
|
||||||
|
# "date": "2025-01-12",
|
||||||
|
# "Overhaul": "B",
|
||||||
|
# "equipmentCount": 30
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "criticalParts": [
|
||||||
|
# "Boiler feed pump",
|
||||||
|
# "Boiler reheater system",
|
||||||
|
# "Drum Level (Right) Root Valve A",
|
||||||
|
# "BCP A Discharge Valve",
|
||||||
|
# "BFPT A EXH Press HI Root VLV"
|
||||||
|
# ],
|
||||||
|
# "schedules": [
|
||||||
|
# {
|
||||||
|
# "date": "2025-01-12",
|
||||||
|
# "Overhaul": "B",
|
||||||
|
# "status": "upcoming"
|
||||||
|
# }
|
||||||
|
# // ... other scheduled overhauls
|
||||||
|
# ],
|
||||||
|
# "systemComponents": {
|
||||||
|
# "boiler": {
|
||||||
|
# "status": "operational",
|
||||||
|
# "lastOverhaul": "2024-06-15"
|
||||||
|
# },
|
||||||
|
# "turbine": {
|
||||||
|
# "hpt": { "status": "operational" },
|
||||||
|
# "ipt": { "status": "operational" },
|
||||||
|
# "lpt": { "status": "operational" }
|
||||||
|
# }
|
||||||
|
# // ... other major components
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|||||||
@ -1,51 +1,355 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Delete, 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):
|
||||||
return await get_overview_overhaul(db_session=db_session)
|
"""Get all overhaul overview."""
|
||||||
|
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 = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
|
headers = {
|
||||||
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode'
|
"Authorization": f"Bearer {token}",
|
||||||
calc_plant_result = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/plant'
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
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']
|
|
||||||
plant_data = plant_response.json()['data']
|
calc_data = calc_response.json()["data"]
|
||||||
return {'calc_result': calc_data, 'plant_result': plant_data}
|
# plot_data = plot_response.json()["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):
|
||||||
equipments = await get_standard_scope_by_session_id(db_session=db_session, overhaul_session_id=session_id, collector_db=collector_db_session)
|
"""Get all overhaul critical parts."""
|
||||||
criticality_simulation = await get_simulation_results(simulation_id=TC_RBD_ID, token=token)
|
equipments = await get_standard_scope_by_session_id(
|
||||||
rbd_simulation = {asset['aeros_node']['node_name']: {'availability': asset['availability'], 'criticality': asset['criticality']} for asset in criticality_simulation['calc_result']}
|
db_session=db_session,
|
||||||
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]
|
overhaul_session_id=session_id,
|
||||||
filtered_result = [item for item in base_result if item['matrix'] is not None]
|
collector_db=collector_db_session
|
||||||
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():
|
||||||
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}}
|
"""Get all overhaul system components with dummy data."""
|
||||||
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))
|
|
||||||
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%'}}
|
# Sort the powerplant_reliability dictionary by critical_contribution in descending order
|
||||||
|
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()
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,11 +1,43 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, 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
|
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||||
|
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(UUID(as_uuid=True), ForeignKey('oh_ms_overhaul_scope.id'), nullable=False)
|
overhaul_scope_id = Column(
|
||||||
|
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,33 +1,116 @@
|
|||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
||||||
from src.csrf_protect import csrf_protect
|
from fastapi import APIRouter, HTTPException, Query, status
|
||||||
|
|
||||||
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 .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)
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session, update
|
||||||
|
|
||||||
@router.get('/{overhaul_session}', response_model=StandardResponse[dict])
|
router = APIRouter()
|
||||||
async def get_scope_equipments(common: CommonParameters, overhaul_session: str, collector_db: CollectorDbSession, location_tag: Optional[str]=Query(None), scope_name: Optional[str]=Query(None)):
|
|
||||||
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(
|
||||||
|
"/{overhaul_session}", response_model=StandardResponse[dict]
|
||||||
@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):
|
async def get_scope_equipments(
|
||||||
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)
|
common: CommonParameters,
|
||||||
return StandardResponse(data=None, message='Data created successfully')
|
overhaul_session: str,
|
||||||
|
collector_db: CollectorDbSession,
|
||||||
@router.get('/{overhaul_session}/{assetnum}', response_model=StandardResponse[OverhaulActivityRead])
|
location_tag: Optional[str] = Query(None),
|
||||||
async def get_overhaul_equipment(db_session: DbSession, assetnum: str, overhaul_session):
|
scope_name: Optional[str] = Query(None),
|
||||||
equipment = await get(db_session=db_session, assetnum=assetnum, overhaul_session_id=overhaul_session)
|
):
|
||||||
|
"""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
|
||||||
|
)
|
||||||
if not equipment:
|
if not equipment:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
raise HTTPException(
|
||||||
return StandardResponse(data=equipment, message='Data retrieved successfully')
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=equipment, message="Data retrieved successfully")
|
||||||
|
|
||||||
|
|
||||||
@router.post('/delete/{overhaul_session}/{location_tag}', response_model=StandardResponse[None], dependencies=[Depends(csrf_protect)])
|
# @router.put(
|
||||||
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session: UUID):
|
# "/{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:
|
||||||
|
# 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,
|
||||||
|
# scope_equipment_activity_in=scope_equipment_activity_in,
|
||||||
|
# ),
|
||||||
|
# message="Data updated successfully",
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/delete/{overhaul_session}/{location_tag}",
|
||||||
|
response_model=StandardResponse[None],
|
||||||
|
)
|
||||||
|
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,24 +1,37 @@
|
|||||||
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':
|
|
||||||
result = Decimal(f'{cost}') / Decimal(str(total_equipment))
|
if scope == "B":
|
||||||
|
result = Decimal(f"{cost}") / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
result = Decimal('8565468127') / Decimal(str(total_equipment))
|
else:
|
||||||
|
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':
|
|
||||||
result = Decimal('36405830225') / Decimal(str(total_equipment))
|
if scope == "B":
|
||||||
|
result = Decimal("36405830225") / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
result = Decimal('36000000000') / Decimal(str(total_equipment))
|
else:
|
||||||
|
result = Decimal("36000000000") / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,8 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,46 +1,142 @@
|
|||||||
import re
|
import re
|
||||||
from fastapi import APIRouter, Depends
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from src.auth.service import Admin
|
|
||||||
from src.csrf_protect import csrf_protect
|
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.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')
|
|
||||||
|
|
||||||
@router.get('/spreadsheet', response_model=StandardResponse[dict])
|
return StandardResponse(
|
||||||
|
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 = {'spreadsheet_id': None, 'spreadsheet_link': None}
|
result = {
|
||||||
|
"spreadsheet_id": None,
|
||||||
|
"spreadsheet_link": None
|
||||||
|
}
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
result = {'spreadsheet_id': data.spreadsheet_id, 'spreadsheet_link': data.spreadsheet_link}
|
result = {
|
||||||
return StandardResponse(data=result, message='Data retrieved successfully')
|
"spreadsheet_id": data.spreadsheet_id,
|
||||||
|
"spreadsheet_link": data.spreadsheet_link
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=result,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
@router.post('/spreadsheet', response_model=StandardResponse[dict], dependencies=[Depends(csrf_protect)])
|
@router.post(
|
||||||
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn, admin: Admin):
|
"/spreadsheet", response_model=StandardResponse[dict]
|
||||||
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_id=spreadsheet_id, spreadsheet_link=spreadsheet_in.spreadsheet_link)
|
spreadsheet = OverhaulGantt(
|
||||||
|
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 = {'spreadsheet_id': spreadsheet_id}
|
result = {
|
||||||
return StandardResponse(data=result, message='Data retrieved successfully')
|
"spreadsheet_id": spreadsheet_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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,5 +1,48 @@
|
|||||||
|
# from datetime import datetime
|
||||||
|
# from typing import List, Optional
|
||||||
|
# from uuid import UUID
|
||||||
|
|
||||||
|
# from pydantic import Field
|
||||||
|
|
||||||
|
# from src.models import DefultBase, Pagination
|
||||||
|
# from src.overhaul_scope.schema import ScopeRead
|
||||||
|
# from src.scope_equipment_job.schema import ScopeEquipmentJobRead
|
||||||
|
# from src.job.schema import ActivityMasterRead
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from src.models import DefultBase
|
from src.models import DefultBase
|
||||||
|
|
||||||
|
|
||||||
class OverhaulGanttIn(DefultBase):
|
class OverhaulGanttIn(DefultBase):
|
||||||
spreadsheet_link: str = Field(...)
|
spreadsheet_link: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
# class OverhaulScheduleCreate(OverhaulScheduleBase):
|
||||||
|
# year: int
|
||||||
|
# plan_duration: Optional[int] = Field(None)
|
||||||
|
# planned_outage: Optional[int] = Field(None)
|
||||||
|
# actual_shutdown: Optional[int] = Field(None)
|
||||||
|
# start: datetime
|
||||||
|
# finish: datetime
|
||||||
|
# remark: Optional[str] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
# class OverhaulScheduleUpdate(OverhaulScheduleBase):
|
||||||
|
# start: datetime
|
||||||
|
# finish: datetime
|
||||||
|
|
||||||
|
|
||||||
|
# class OverhaulScheduleRead(OverhaulScheduleBase):
|
||||||
|
# id: UUID
|
||||||
|
# year: int
|
||||||
|
# plan_duration: Optional[int]
|
||||||
|
# planned_outage: Optional[int]
|
||||||
|
# actual_shutdown: Optional[int]
|
||||||
|
# start: datetime
|
||||||
|
# finish: datetime
|
||||||
|
# remark: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# class OverhaulSchedulePagination(Pagination):
|
||||||
|
# items: List[OverhaulScheduleRead] = []
|
||||||
|
|||||||
@ -1,34 +1,112 @@
|
|||||||
|
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_gantt_performance_chart(*, spreadsheet_id='1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk'):
|
# async def get_all(*, common):
|
||||||
|
# """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"
|
RANGE_NAME = "'SUMMARY'!K34:AZ38" # Or just "2024 schedule"
|
||||||
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()
|
|
||||||
values = response.get('values', [])
|
response = sheet.values().get(
|
||||||
|
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).")
|
||||||
|
|
||||||
|
# Extract rows
|
||||||
day_row = values[0][1:]
|
day_row = values[0][1:]
|
||||||
date_row = values[1][1:]
|
date_row = values[1][1:]
|
||||||
plan_row = values[3][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))
|
|
||||||
plan_row += [''] * (total_days - len(plan_row))
|
# PAD rows so lengths match day count
|
||||||
actual_row += [''] * (total_days - len(actual_row))
|
date_row += [""] * (total_days - len(date_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%'
|
actual = actual_row[i] if actual_row[i] else "0%" # <-- FIX HERE
|
||||||
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
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1 +1,43 @@
|
|||||||
|
from sqlalchemy import (UUID, Column, DateTime, Float, ForeignKey, Integer,
|
||||||
|
String)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from src.database.core import Base
|
||||||
|
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||||
|
|
||||||
|
|
||||||
|
# class EquipmentWorkscopeGroup(Base, DefaultMixin):
|
||||||
|
# __tablename__ = "oh_tr_equipment_workscope_group"
|
||||||
|
|
||||||
|
# # overhaul_activity_id = Column(
|
||||||
|
# # UUID(as_uuid=True), ForeignKey("oh_tr_overhaul_activity.id"), nullable=False
|
||||||
|
# # )
|
||||||
|
|
||||||
|
# # scope_equipment_job_id = Column(
|
||||||
|
# # UUID(as_uuid=True),
|
||||||
|
# # ForeignKey("oh_ms_scope_equipment_job.id", ondelete="cascade"),
|
||||||
|
# # nullable=False,
|
||||||
|
# # )
|
||||||
|
|
||||||
|
# # notes = Column(String, nullable=True)
|
||||||
|
# # status = Column(String, nullable=True, default="pending")
|
||||||
|
# workscope_group_id = Column(
|
||||||
|
# UUID(as_uuid=True), ForeignKey("oh_ms_workscope_group.id"), nullable=False
|
||||||
|
# )
|
||||||
|
|
||||||
|
# location_tag = Column(String, nullable=False)
|
||||||
|
|
||||||
|
# equipment = relationship(
|
||||||
|
# "StandardScope",
|
||||||
|
# lazy="selectin",
|
||||||
|
# primaryjoin="and_(OverhaulJob.location_tag == foreign(StandardScope.location_tag))",
|
||||||
|
# uselist=False, # Add this if it's a one-to-one relationship
|
||||||
|
# )
|
||||||
|
|
||||||
|
# workscope_group = relationship("MasterActivity", lazy="selectin", back_populates="equipment_workscope_groups")
|
||||||
|
|
||||||
|
# # scope_equipment_job = relationship(
|
||||||
|
# # "ScopeEquipmentJob", lazy="raise", back_populates="overhaul_jobs"
|
||||||
|
# # )
|
||||||
|
|
||||||
|
# # overhaul_activity = relationship("OverhaulActivity", lazy="raise", back_populates="overhaul_jobs")
|
||||||
|
|||||||
@ -1,23 +1,91 @@
|
|||||||
from typing import Optional
|
from typing import List, 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])
|
|
||||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
@router.get(
|
||||||
|
"/{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')
|
|
||||||
|
|
||||||
@router.post('/{overhaul_equipment_id}', response_model=StandardResponse[None])
|
return StandardResponse(
|
||||||
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate):
|
data=results,
|
||||||
await create(db_session=db_session, overhaul_equipment_id=overhaul_equipment_id, overhaul_job_in=overhaul_job_in)
|
message="Data retrieved successfully",
|
||||||
return StandardResponse(data=None, message='Data created successfully')
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/delete/{overhaul_job_id}', response_model=StandardResponse[None])
|
@router.post("/{overhaul_equipment_id}", response_model=StandardResponse[None])
|
||||||
|
async def create_overhaul_equipment_jobs(
|
||||||
|
db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
|
||||||
|
):
|
||||||
|
"""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)
|
||||||
|
|||||||
@ -1,41 +1,125 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import Select
|
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.scope_equipment_job.model import ScopeEquipmentJob
|
||||||
|
# from src.overhaul_activity.model import OverhaulActivity
|
||||||
|
|
||||||
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 src.equipment_workscope_group.model import EquipmentWorkscopeGroup
|
from .model import EquipmentWorkscopeGroup
|
||||||
from .schema import OverhaulJobCreate
|
from .schema import OverhaulJobCreate
|
||||||
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
|
|
||||||
|
|
||||||
async def get_all(*, common, location_tag: str, scope: Optional[str]=None):
|
|
||||||
query = Select(EquipmentWorkscopeGroup).where(EquipmentWorkscopeGroup.location_tag == location_tag)
|
async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
|
||||||
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
"""Returns all documents."""
|
||||||
|
query = (
|
||||||
|
Select(EquipmentWorkscopeGroup)
|
||||||
|
.where(EquipmentWorkscopeGroup.location_tag == location_tag)
|
||||||
|
)
|
||||||
|
|
||||||
if scope:
|
if scope:
|
||||||
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)
|
query
|
||||||
|
.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)
|
||||||
|
return results
|
||||||
|
|
||||||
async def create(*, db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate):
|
|
||||||
|
async def create(
|
||||||
|
*, db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
|
||||||
|
):
|
||||||
overhaul_jobs = []
|
overhaul_jobs = []
|
||||||
|
|
||||||
if not overhaul_equipment_id:
|
if not overhaul_equipment_id:
|
||||||
raise ValueError('assetnum parameter is required')
|
raise ValueError("assetnum parameter is required")
|
||||||
equipment_stmt = Select(OverhaulJob).where(OverhaulJob.overhaul_activity_id == overhaul_equipment_id)
|
|
||||||
await db_session.scalar(equipment_stmt)
|
equipment_stmt = Select(OverhaulJob).where(
|
||||||
|
OverhaulJob.overhaul_activity_id == overhaul_equipment_id
|
||||||
|
)
|
||||||
|
|
||||||
|
equipment = await db_session.scalar(equipment_stmt)
|
||||||
|
|
||||||
for job_id in overhaul_job_in.job_ids:
|
for job_id in overhaul_job_in.job_ids:
|
||||||
overhaul_equipment_job = OverhaulJob(overhaul_activity_id=overhaul_equipment_id, scope_equipment_job_id=job_id)
|
overhaul_equipment_job = OverhaulJob(
|
||||||
|
overhaul_activity_id=overhaul_equipment_id, scope_equipment_job_id=job_id
|
||||||
|
)
|
||||||
|
|
||||||
overhaul_jobs.append(overhaul_equipment_job)
|
overhaul_jobs.append(overhaul_equipment_job)
|
||||||
|
|
||||||
db_session.add_all(overhaul_jobs)
|
db_session.add_all(overhaul_jobs)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
return overhaul_job_in.job_ids
|
return overhaul_job_in.job_ids
|
||||||
|
|
||||||
async def delete(*, db_session: DbSession, overhaul_job_id: str) -> bool:
|
|
||||||
|
async def delete(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
overhaul_job_id: str,
|
||||||
|
) -> 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:
|
||||||
deleted = await soft_delete_record(db_session, EquipmentWorkscopeGroup, overhaul_job_id)
|
# Check if job exists
|
||||||
if not deleted:
|
scope_job = await db_session.get(OverhaulJob, overhaul_job_id)
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
if not scope_job:
|
||||||
|
raise HTTPException(
|
||||||
|
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.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception:
|
|
||||||
|
except Exception as e:
|
||||||
await db_session.rollback()
|
await db_session.rollback()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# async def update(*, db_session: DbSession, scope: OverhaulScope, scope_in: ScopeUpdate):
|
||||||
|
# """Updates a document."""
|
||||||
|
# data = scope_in.model_dump()
|
||||||
|
|
||||||
|
# update_data = scope_in.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(OverhaulScope).where(OverhaulScope.id == scope_id)
|
||||||
|
# await db_session.execute(query)
|
||||||
|
# await db_session.commit()
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,13 +1,18 @@
|
|||||||
from sqlalchemy import Column, DateTime, Integer, String
|
from sqlalchemy import (UUID, Column, DateTime, Float, ForeignKey, Integer,
|
||||||
|
String)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||||
|
|
||||||
|
|
||||||
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))
|
start = Column(DateTime(timezone=True)) # This will be TIMESTAMP WITH TIME ZONE
|
||||||
finish = Column(DateTime(timezone=True))
|
finish = Column(DateTime(timezone=True))
|
||||||
remark = Column(String, nullable=True)
|
remark = Column(String, nullable=True)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue