fix query

main
Cizz22 3 months ago
parent 110d7cc7ee
commit 7a9d5aacab

@ -5,7 +5,7 @@ from sqlalchemy import Delete, Select, and_, text
from sqlalchemy.orm import selectinload
from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.core import CollectorDbSession, DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from .model import ScopeEquipmentPart
@ -16,33 +16,119 @@ from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
# return result
def create_dummy_parts(assetnum: str, count: int = 5):
"""
Create a list of dummy ScopeEquipmentPart objects with random stock values.
Args:
assetnum (str): The base asset number to generate dummy parts for.
count (int): The number of parts to create. Default is 5.
Returns:
List[ScopeEquipmentPart]: A list of dummy ScopeEquipmentPart objects.
"""
parts = []
for i in range(1, count + 1):
# Generate a unique part asset number
part_assetnum = f"{assetnum}_PART_{i}"
stock = random.randint(1, 100) # Random stock value between 1 and 100
parts.append({"assetnum": part_assetnum, "stock": stock})
return parts
from sqlalchemy import text
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession as DbSession
from sqlalchemy.sql import text
import logging
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
async def get_all(
db_session: DbSession,
db_session,
location_tag: Optional[str] = None,
start_year: int = 2023,
end_year: Optional[int] = None
@ -81,22 +167,20 @@ async def get_all(
base_query += """
),
filtered_transactions AS (
SELECT wonum, itemnum, curbal
FROM public.maximo_material_use_transactions
WHERE issuetype = 'ISSUE'
AND wonum IN (SELECT wonum FROM filtered_wo)
)
filtered_materials AS (
SELECT wonum, itemnum, itemqty, inv_curbaltotal, inv_avgcost
FROM public.wo_maxim_material
WHERE wonum IN (SELECT wonum FROM filtered_wo)
)
SELECT
fwo.asset_location AS location_tag,
ft.itemnum,
spl.description AS sparepart_name,
COUNT(*) AS parts_consumed_in_oh,
MIN(ft.curbal) AS min_remaining_balance,
MAX(mi.curbaltotal) AS inv_curbaltotal
COALESCE(SUM(ft.itemqty), 0) AS parts_consumed_in_oh,
COALESCE(AVG(ft.inv_avgcost), 0) AS avgcost,
COALESCE(AVG(ft.inv_curbaltotal), 0) AS inv_curbaltotal
FROM filtered_wo fwo
INNER JOIN filtered_transactions ft ON fwo.wonum = ft.wonum
INNER JOIN public.maximo_inventory mi ON ft.itemnum = mi.itemnum
INNER JOIN filtered_materials ft ON fwo.wonum = ft.wonum
LEFT JOIN public.maximo_sparepart_pr_po_line spl ON ft.itemnum = spl.item_num
GROUP BY fwo.asset_location, ft.itemnum, spl.description
ORDER BY fwo.asset_location, ft.itemnum
@ -123,32 +207,4 @@ async def get_all(
except Exception as e:
# Log the error appropriately in your application
print(f"Database query error: {e}")
raise
# async def create(*, db_session: DbSession, scope_equipment_activty_in: ScopeEquipmentActivityCreate):
# activity = ScopeEquipmentActivity(
# **scope_equipment_activty_in.model_dump())
# db_session.add(activity)
# await db_session.commit()
# return activity
# async def update(*, db_session: DbSession, activity: ScopeEquipmentActivity, scope_equipment_activty_in: ScopeEquipmentActivityUpdate):
# """Updates a document."""
# data = scope_equipment_activty_in.model_dump()
# update_data = scope_equipment_activty_in.model_dump(exclude_defaults=True)
# for field in data:
# if field in update_data:
# setattr(activity, field, update_data[field])
# await db_session.commit()
# return activity
# async def delete(*, db_session: DbSession, scope_equipment_activity_id: str):
# """Deletes a document."""
# activity = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
# await db_session.delete(activity)
# await db_session.commit()
raise

@ -85,56 +85,122 @@ ORDER BY avg_cost DESC;
}
# 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
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 = 'OH'
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%'
)
SELECT
asset_location,
SUM(total_wo_cost)::numeric / COUNT(wonum) AS avg_cost
FROM wo_costs
GROUP BY asset_location
ORDER BY COUNT(wonum) DESC;
""")
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,
(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 = 'OH'
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 >= '2019-01-01'
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;
""")
result = await collector_db.execute(query)
data = []

@ -1,4 +1,6 @@
from datetime import datetime
from dataclasses import dataclass
from datetime import date, datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID
@ -35,41 +37,41 @@ class ActivityMasterRead(ActivityMaster):
class ActivityMasterPagination(Pagination):
items: List[ActivityMasterRead] = []
# {
# "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
# }
# }
class ProcurementStatus(Enum):
PLANNED = "planned"
ORDERED = "ordered"
RECEIVED = "received"
CANCELLED = "cancelled"
@dataclass
class SparepartRequirement:
"""Sparepart requirement for equipment overhaul"""
sparepart_id: str
quantity_required: int
lead_time: int
sparepart_name: str
unit_cost: float
avg_cost: float
@dataclass
class SparepartStock:
"""Current sparepart stock information"""
sparepart_id: str
sparepart_name: str
current_stock: int
unit_cost: float
location: str
@dataclass
class ProcurementRecord:
"""Purchase Order/Purchase Request record"""
po_pr_id: str
sparepart_id: str
sparepart_name: str
quantity: int
unit_cost: float
total_cost: float
order_date: date
expected_delivery_date: date
status: ProcurementStatus
po_vendor_delivery_date: date

@ -19,6 +19,7 @@ from src.logging import setup_logging
from src.overhaul_activity.service import get_standard_scope_by_session_id
from src.overhaul_scope.service import get as get_scope, get_overview_overhaul
from src.overhaul_scope.service import get_prev_oh
from src.sparepart.schema import ProcurementRecord, ProcurementStatus, SparepartRequirement, SparepartStock
log = logging.getLogger(__name__)
@ -27,6 +28,177 @@ setup_logging(logger=log)
from sqlalchemy import text
import math
from sqlalchemy import text
# async def get_spareparts_paginated(
# *,
# db_session,
# collector_db_session,
# ):
# """
# Get spare parts for work orders under specific parent WO(s),
# including inventory and PR/PO data.
# """
# # Normalize parent_num to array for SQL ANY()
# # parent_nums = parent_num if isinstance(parent_num, (list, tuple)) else [parent_num]
# parent_nums = []
# data_query = text("""
# WITH selected_wo AS (
# SELECT
# wonum,
# xx_parent,
# location_tag,
# assetnum,
# siteid,
# reportdate
# FROM public.wo_maxim
# WHERE xx_parent = ANY(:parent_nums)
# ),
# wo_materials AS (
# SELECT
# wm.wonum,
# wm.itemnum,
# wm.itemqty,
# wm.inv_itemnum,
# wm.inv_location,
# wm.inv_curbaltotal,
# wm.inv_avgcost,
# sw.location_tag
# FROM public.wo_maxim_material wm
# JOIN selected_wo sw ON wm.wonum = sw.wonum
# ),
# -- PR Lines
# pr_lines AS (
# SELECT
# pl.item_num,
# h.num AS pr_number,
# h.issue_date AS pr_issue_date,
# h.status AS pr_status,
# pl.qty_ordered AS pr_qty_ordered,
# pl.qty_requested AS pr_qty_requested
# FROM public.maximo_sparepart_pr_po h
# JOIN public.maximo_sparepart_pr_po_line pl
# ON h.num = pl.num
# WHERE h.type = 'PR'
# AND EXTRACT(YEAR FROM h.issue_date) >= 2019
# ),
# -- PO Lines
# po_lines AS (
# SELECT
# pl.item_num,
# h.num AS po_number,
# h.estimated_arrival_date AS po_estimated_arrival_date,
# h.vendeliverydate AS po_vendeliverydate,
# h.receipts AS po_receipt,
# h.status AS po_status,
# pl.qty_ordered AS po_qty_ordered,
# pl.qty_received AS po_qty_received
# FROM public.maximo_sparepart_pr_po h
# JOIN public.maximo_sparepart_pr_po_line pl
# ON h.num = pl.num
# WHERE h.type = 'PO'
# AND (h.receipts = 'NONE')
# AND (h.status IS NOT NULL)
# ),
# -- Item Descriptions
# item_descriptions AS (
# SELECT DISTINCT
# item_num,
# FIRST_VALUE(description) OVER (
# PARTITION BY item_num
# ORDER BY created_at DESC NULLS LAST
# ) AS description
# FROM public.maximo_sparepart_pr_po_line
# WHERE description IS NOT NULL
# ),
# -- Unified PR/PO data
# pr_po_unified AS (
# SELECT
# pr.item_num,
# pr.pr_number,
# pr.pr_issue_date,
# pr.pr_qty_ordered,
# pr.pr_status,
# po.po_number,
# COALESCE(po.po_qty_ordered, 0) AS po_qty_ordered,
# COALESCE(po.po_qty_received, 0) AS po_qty_received,
# po.po_estimated_arrival_date,
# po.po_vendeliverydate,
# po.po_receipt,
# po.po_status,
# CASE WHEN po.po_number IS NOT NULL THEN 'YES' ELSE 'NO' END AS po_exists
# FROM pr_lines pr
# LEFT JOIN po_lines po
# ON pr.item_num = po.item_num
# AND pr.pr_number = po.po_number
# ),
# -- Aggregate PR/PO info
# pr_po_agg AS (
# SELECT
# item_num,
# SUM(COALESCE(pr_qty_ordered, 0)) AS total_pr_qty,
# SUM(COALESCE(po_qty_ordered, 0)) AS total_po_qty,
# SUM(COALESCE(po_qty_received, 0)) AS total_po_received,
# JSON_AGG(
# JSON_BUILD_OBJECT(
# 'pr_number', pr_number,
# 'pr_issue_date', pr_issue_date,
# 'pr_qty_requested', pr_qty_ordered,
# 'pr_status', pr_status,
# 'po_exists', po_exists,
# 'po_qty_ordered', po_qty_ordered,
# 'po_qty_received', po_qty_received,
# 'po_estimated_arrival_date', po_estimated_arrival_date,
# 'po_vendeliverydate', po_vendeliverydate,
# 'po_receipt', po_receipt,
# 'po_status', po_status
# )
# ORDER BY pr_issue_date DESC
# ) AS pr_po_details
# FROM pr_po_unified
# GROUP BY item_num
# )
# SELECT
# wm.itemnum,
# COALESCE(id.description, 'No description available') AS item_description,
# SUM(wm.itemqty) AS total_required_for_oh,
# COALESCE(MAX(wm.inv_curbaltotal), 0) AS current_balance_total,
# COALESCE(ap.total_pr_qty, 0) AS total_pr_qty,
# COALESCE(ap.total_po_qty, 0) AS total_po_qty,
# COALESCE(ap.total_po_received, 0) AS total_po_received,
# ap.pr_po_details
# FROM wo_materials wm
# LEFT JOIN item_descriptions id
# ON wm.itemnum = id.item_num
# LEFT JOIN pr_po_agg ap
# ON wm.itemnum = ap.item_num
# GROUP BY
# wm.itemnum, id.description,
# ap.total_pr_qty, ap.total_po_qty, ap.total_po_received, ap.pr_po_details
# ORDER BY wm.itemnum;
# """)
# rows = await collector_db_session.execute(data_query, {"parent_nums": parent_nums})
# spare_parts = []
# for row in rows:
# spare_parts.append({
# "item_num": row.itemnum,
# "description": row.item_description,
# "current_balance_total": float(row.current_balance_total or 0.0),
# "total_required_for_oh": float(row.total_required_for_oh or 0.0),
# "total_pr_qty": row.total_pr_qty,
# "total_po_qty": row.total_po_qty,
# "total_po_received": row.total_po_received,
# "pr_po_details": row.pr_po_details,
# })
# return spare_parts
async def get_spareparts_paginated(*, db_session, collector_db_session):
"""
Get paginated spare parts with usage, inventory, and PR/PO information.
@ -52,22 +224,29 @@ async def get_spareparts_paginated(*, db_session, collector_db_session):
AND asset_location IS NOT NULL
AND EXTRACT(YEAR FROM reportdate) >= 2019
AND asset_unit IN ('3', '00')
AND asset_location = ANY(:asset_locations)
),
sparepart_usage AS (
SELECT oh.asset_location, mwm.itemnum, mwm.itemqty, mwm.wonum
FROM oh_workorders oh
INNER JOIN public.maximo_workorder_materials mwm ON oh.wonum = mwm.wonum
),
wo_materials AS (
SELECT
wm.wonum,
wm.itemnum,
wm.itemqty,
wm.inv_itemnum,
wm.inv_location,
wm.inv_curbaltotal,
wm.inv_avgcost,
sw.asset_location as location_tag
FROM public.wo_maxim_material wm
JOIN oh_workorders sw ON wm.wonum = sw.wonum
),
location_sparepart_stats AS (
SELECT asset_location, itemnum,
SELECT location_tag, itemnum,
COUNT(DISTINCT wonum) as total_wo_count,
SUM(itemqty) as total_qty_used,
AVG(itemqty) as avg_qty_per_wo,
MIN(itemqty) as min_qty_used,
MAX(itemqty) as max_qty_used
FROM sparepart_usage
GROUP BY asset_location, itemnum
FROM wo_materials
GROUP BY location_tag, itemnum
HAVING SUM(itemqty) > 0
),
pr_lines AS (
@ -160,16 +339,16 @@ async def get_spareparts_paginated(*, db_session, collector_db_session):
ROUND(CAST(lss.avg_qty_per_wo AS NUMERIC), 2) as avg_qty_per_wo,
lss.min_qty_used,
lss.max_qty_used,
COALESCE(i.curbaltotal,0) as current_balance_total,
COALESCE(i.inv_curbaltotal,0) as current_balance_total,
COALESCE(ap.total_pr_qty,0) as total_pr_qty,
COALESCE(ap.total_po_qty,0) as total_po_qty,
COALESCE(ap.total_po_received,0) as total_po_received,
ap.pr_po_details
FROM location_sparepart_stats lss
LEFT JOIN item_descriptions id ON lss.itemnum = id.item_num
LEFT JOIN public.maximo_inventory i ON lss.itemnum = i.itemnum
LEFT JOIN wo_materials i ON lss.itemnum = i.itemnum
LEFT JOIN pr_po_agg ap ON lss.itemnum = ap.item_num
ORDER BY lss.asset_location, lss.itemnum
ORDER BY lss.location_tag, lss.itemnum
""")
overhaul = await get_overview_overhaul(db_session=db_session)
@ -198,85 +377,6 @@ async def get_spareparts_paginated(*, db_session, collector_db_session):
return spare_parts
# # -----------------------------
# # Query #2: Count total rows
# # -----------------------------
# count_query = text("""
# WITH oh_workorders AS (
# SELECT DISTINCT wonum, asset_location, asset_unit
# FROM public.wo_staging_maximo_2
# WHERE worktype = 'OH'
# AND asset_location IS NOT NULL
# AND EXTRACT(YEAR FROM reportdate) >= 2019
# AND asset_unit IN ('3', '00')
# ),
# sparepart_usage AS (
# SELECT oh.asset_location, mwm.itemnum, mwm.itemqty, mwm.wonum
# FROM oh_workorders oh
# INNER JOIN public.maximo_workorder_materials mwm ON oh.wonum = mwm.wonum
# ),
# location_sparepart_stats AS (
# SELECT asset_location, itemnum
# FROM sparepart_usage
# GROUP BY asset_location, itemnum
# )
# SELECT COUNT(*) as total_count
# FROM location_sparepart_stats;
# """)
# total_count_result = await db_session.execute(count_query)
# total_count = total_count_result.scalar() or 0
# # calculate total pages
# total_pages = math.ceil(total_count / items_per_page) if items_per_page > 0 else 1
# return {
# "total": total_count,
# "page": page,
# "items_per_page": items_per_page,
# "total_pages": total_pages,
# "items": spare_parts
# }
class ProcurementStatus(Enum):
PLANNED = "planned"
ORDERED = "ordered"
RECEIVED = "received"
CANCELLED = "cancelled"
@dataclass
class SparepartRequirement:
"""Sparepart requirement for equipment overhaul"""
sparepart_id: str
quantity_required: int
lead_time: int
sparepart_name: str
unit_cost: float
avg_cost: float
@dataclass
class SparepartStock:
"""Current sparepart stock information"""
sparepart_id: str
sparepart_name: str
current_stock: int
unit_cost: float
location: str
@dataclass
class ProcurementRecord:
"""Purchase Order/Purchase Request record"""
po_pr_id: str
sparepart_id: str
sparepart_name: str
quantity: int
unit_cost: float
total_cost: float
order_date: date
expected_delivery_date: date
status: ProcurementStatus
po_vendor_delivery_date: date
class SparepartManager:
"""Manages sparepart availability and procurement for overhaul optimization"""
@ -800,19 +900,19 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, analysis
# Load sparepart stocks
# Example query - adjust based on your schema
query = text("""
SELECT
mi.id,
mi.itemnum,
mi.itemsetid,
mi."location",
mi.curbaltotal,
mi.avgcost,
mspl.description
FROM public.maximo_inventory mi
LEFT JOIN public.maximo_sparepart_pr_po_line mspl
ON mi.itemnum = mspl.item_num
""")
query = text("""SELECT
wm.inv_itemnum AS itemnum,
wm.inv_itemsetid AS itemsetid,
wm.inv_location AS location,
MAX(wm.inv_curbaltotal) AS curbaltotal,
AVG(wm.inv_avgcost) AS avgcost,
COALESCE(mspl.description, 'No description available') AS description
FROM public.wo_maxim_material wm
LEFT JOIN public.maximo_sparepart_pr_po_line mspl
ON wm.inv_itemnum = mspl.item_num
WHERE wm.inv_itemnum IS NOT NULL
GROUP BY wm.inv_itemnum, wm.inv_itemsetid, wm.inv_location, mspl.description
""")
log.info("Fetch sparepart")
sparepart_stocks_query = await db_session.execute(query)
@ -826,7 +926,161 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, analysis
)
sparepart_manager.add_sparepart_stock(stock)
# Load equipment sparepart requirements
# parent_nums = []
# query = text("""
# WITH target_wo AS (
# -- Work orders from the given parent(s)
# SELECT
# wonum,
# xx_parent,
# location_tag AS asset_location
# FROM public.wo_maxim
# WHERE xx_parent = ANY(:parent_nums)
# ),
# target_materials AS (
# -- Materials directly linked to target WOs (new requirement data)
# SELECT
# tw.asset_location,
# wm.itemnum,
# wm.inv_avgcost
# SUM(wm.itemqty) AS total_qty_required
# FROM public.wo_maxim_material wm
# JOIN target_wo tw ON wm.wonum = tw.wonum
# WHERE wm.itemnum IS NOT NULL
# GROUP BY tw.asset_location, wm.itemnum
# ),
# -- Historical OH work orders (for lead time reference)
# oh_workorders AS (
# SELECT DISTINCT
# wonum,
# asset_location
# FROM public.wo_staging_maximo_2
# WHERE worktype = 'OH'
# AND asset_location IS NOT NULL
# AND asset_unit IN ('3', '00')
# ),
# sparepart_usage AS (
# SELECT
# oh.asset_location,
# mwm.itemnum,
# mwm.itemqty,
# mwm.wonum
# FROM oh_workorders oh
# INNER JOIN public.wo_maxim_material mwm
# ON oh.wonum = mwm.wonum
# ),
# location_sparepart_stats AS (
# SELECT
# asset_location,
# itemnum,
# COUNT(DISTINCT wonum) as total_wo_count,
# SUM(itemqty) as total_qty_used,
# AVG(itemqty) as avg_qty_per_wo
# FROM sparepart_usage
# GROUP BY asset_location, itemnum
# ),
# pr_po_combined AS (
# SELECT
# mspl.item_num,
# mspl.num,
# mspl.unit_cost,
# mspl.qty_ordered,
# MAX(CASE WHEN mspo.type = 'PR' THEN mspo.issue_date END) as issue_date,
# MAX(CASE WHEN mspo.type = 'PO' THEN mspo.vendeliverydate END) as vendeliverydate,
# MAX(CASE WHEN mspo.type = 'PO' THEN mspo.estimated_arrival_date END) as estimated_arrival_date
# FROM public.maximo_sparepart_pr_po_line mspl
# INNER JOIN public.maximo_sparepart_pr_po mspo
# ON mspl.num = mspo.num
# WHERE mspo.type IN ('PR', 'PO')
# GROUP BY mspl.item_num, mspl.num, mspl.unit_cost, mspl.qty_ordered
# ),
# leadtime_stats AS (
# SELECT
# item_num,
# ROUND(CAST(AVG(
# EXTRACT(EPOCH FROM (
# COALESCE(vendeliverydate, estimated_arrival_date) - issue_date
# )) / 86400 / 30.44
# ) AS NUMERIC), 1) as avg_leadtime_months,
# ROUND(CAST(MIN(
# EXTRACT(EPOCH FROM (
# COALESCE(vendeliverydate, estimated_arrival_date) - issue_date
# )) / 86400 / 30.44
# ) AS NUMERIC), 1) as min_leadtime_months,
# ROUND(CAST(MAX(
# EXTRACT(EPOCH FROM (
# COALESCE(vendeliverydate, estimated_arrival_date) - issue_date
# )) / 86400 / 30.44
# ) AS NUMERIC), 1) as max_leadtime_months,
# COUNT(*) as leadtime_sample_size,
# COUNT(CASE WHEN vendeliverydate IS NOT NULL THEN 1 END) as vendelivery_count,
# COUNT(CASE WHEN vendeliverydate IS NULL AND estimated_arrival_date IS NOT NULL THEN 1 END) as estimated_only_count
# FROM pr_po_combined
# WHERE issue_date IS NOT NULL
# AND COALESCE(vendeliverydate, estimated_arrival_date) IS NOT NULL
# AND COALESCE(vendeliverydate, estimated_arrival_date) > issue_date
# GROUP BY item_num
# ),
# cost_stats AS (
# SELECT
# item_num,
# ROUND(CAST(AVG(unit_cost) AS NUMERIC), 2) as avg_unit_cost,
# ROUND(CAST(MIN(unit_cost) AS NUMERIC), 2) as min_unit_cost,
# ROUND(CAST(MAX(unit_cost) AS NUMERIC), 2) as max_unit_cost,
# COUNT(*) as cost_sample_size,
# ROUND(CAST(AVG(unit_cost * qty_ordered) AS NUMERIC), 2) as avg_order_value,
# ROUND(CAST(SUM(unit_cost * qty_ordered) AS NUMERIC), 2) as total_value_ordered
# FROM pr_po_combined
# WHERE unit_cost IS NOT NULL AND unit_cost > 0
# GROUP BY item_num
# ),
# item_descriptions AS (
# SELECT DISTINCT
# item_num,
# FIRST_VALUE(description) OVER (
# PARTITION BY item_num
# ORDER BY created_at DESC NULLS LAST
# ) as description
# FROM public.maximo_sparepart_pr_po_line
# WHERE description IS NOT NULL
# )
# SELECT
# tr.asset_location,
# tr.itemnum,
# COALESCE(id.description, 'No description available') as item_description,
# tr.total_qty_required AS total_required_for_oh,
# tr.inv_avgcost,
# COALESCE(lt.avg_leadtime_months, 0) as avg_leadtime_months,
# COALESCE(cs.avg_unit_cost, 0) as avg_unit_cost,
# ROUND(CAST(COALESCE(tr.total_qty_required * cs.avg_unit_cost, 0) AS NUMERIC), 2) as estimated_cost_for_oh
# FROM target_materials tr
# LEFT JOIN item_descriptions id ON tr.itemnum = id.item_num
# LEFT JOIN leadtime_stats lt ON tr.itemnum = lt.item_num
# LEFT JOIN cost_stats cs ON tr.itemnum = cs.item_num
# ORDER BY tr.asset_location, tr.itemnum;
# """)
# equipment_requirements_query = await db_session.execute(query, {"parent_nums": parent_nums})
# equipment_requirements = defaultdict(list)
# for req_record in equipment_requirements_query:
# requirement = SparepartRequirement(
# sparepart_id=req_record.itemnum,
# quantity_required=float(req_record.total_required_for_oh or 0.0),
# lead_time=float(req_record.avg_leadtime_months or 0.0),
# sparepart_name=req_record.item_description,
# unit_cost=float(req_record.avg_unit_cost or 0.0),
# avg_cost=float(req_record.inv_avgcost or 0.0),
# )
# equipment_requirements[req_record.asset_location].append(requirement)
# for equipment_tag, requirements in equipment_requirements.items():
# sparepart_manager.add_equipment_requirements(equipment_tag, requirements)
# Load equipment sparepart requirements
# You'll need to create this table/relationship
query = text("""WITH oh_workorders AS (
-- First, get all OH work orders
@ -834,19 +1088,19 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, analysis
wonum,
asset_location
FROM public.wo_staging_maximo_2
WHERE worktype = 'OH' AND asset_location IS NOT NULL and asset_unit IN ('3', '00')
),
sparepart_usage AS (
-- Get sparepart usage for OH work orders
SELECT
oh.asset_location,
mwm.itemnum,
mwm.itemqty,
mwm.wonum
FROM oh_workorders oh
INNER JOIN public.maximo_workorder_materials mwm
ON oh.wonum = mwm.wonum
WHERE worktype = 'OH' AND asset_location IS NOT NULL and asset_unit IN ('3', '00') AND EXTRACT(YEAR FROM reportdate) >= 2019
),
sparepart_usage AS (
SELECT
oh.asset_location,
mwm.itemnum,
mwm.itemqty,
mwm.wonum,
mwm.inv_avgcost
FROM oh_workorders oh
INNER JOIN public.wo_maxim_material mwm
ON oh.wonum = mwm.wonum
),
location_sparepart_stats AS (
-- Calculate average usage per sparepart per location
SELECT
@ -947,7 +1201,7 @@ SELECT
ROUND(CAST(lss.avg_qty_per_wo AS NUMERIC), 2) as avg_qty_per_wo,
lss.min_qty_used,
lss.max_qty_used,
iin.avgcost,
iin.inv_avgcost,
-- Lead time metrics
COALESCE(lt.avg_leadtime_months, 0) as avg_leadtime_months,
COALESCE(lt.min_leadtime_months, 0) as min_leadtime_months,
@ -968,7 +1222,7 @@ FROM location_sparepart_stats lss
LEFT JOIN item_descriptions id ON lss.itemnum = id.item_num
LEFT JOIN leadtime_stats lt ON lss.itemnum = lt.item_num
LEFT JOIN cost_stats cs ON lss.itemnum = cs.item_num
LEFT JOIN item_inventory iin ON lss.itemnum = iin.itemnum
LEFT JOIN sparepart_usage iin ON lss.itemnum = iin.itemnum
ORDER BY lss.asset_location, lss.itemnum;""")
equipment_requirements_query = await db_session.execute(query)
@ -1024,13 +1278,22 @@ po_with_pr_date AS (
INNER JOIN public.maximo_sparepart_pr_po pr
ON pr.num = po.po_number
AND pr.type = 'PR'
),
WITH item_inventory AS (
SELECT
itemnum,
MAX(inv_curbaltotal) AS current_balance_total,
AVG(inv_avgcost) AS avg_cost
FROM public.wo_maxim_material
WHERE inv_itemnum IS NOT NULL
GROUP BY itemnum
)
SELECT
po.item_num,
po.description,
po.line_cost,
po.unit_cost,
COALESCE(i.curbaltotal, 0) as current_balance_total,
COALESCE(i.current_balance_total, 0) as current_balance_total,
po.po_number,
po.pr_issue_date,
po.po_status,
@ -1040,7 +1303,7 @@ SELECT
po.estimated_arrival_date as po_estimated_arrival_date,
po.vendeliverydate as po_vendor_delivery_date
FROM po_with_pr_date po
LEFT JOIN public.maximo_inventory i
LEFT JOIN item_inventory i
ON po.item_num = i.itemnum
ORDER BY po.item_num, po.pr_issue_date DESC;
""")

Loading…
Cancel
Save