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 sqlalchemy.orm import selectinload
from src.auth.service import CurrentUser 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 src.database.service import CommonParameters, search_filter_sort_paginate
from .model import ScopeEquipmentPart from .model import ScopeEquipmentPart
@ -16,33 +16,119 @@ from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id) # result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
# return result # return result
def create_dummy_parts(assetnum: str, count: int = 5):
"""
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 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( async def get_all(
db_session: DbSession, db_session,
location_tag: Optional[str] = None, location_tag: Optional[str] = None,
start_year: int = 2023, start_year: int = 2023,
end_year: Optional[int] = None end_year: Optional[int] = None
@ -81,22 +167,20 @@ async def get_all(
base_query += """ base_query += """
), ),
filtered_transactions AS ( filtered_materials AS (
SELECT wonum, itemnum, curbal SELECT wonum, itemnum, itemqty, inv_curbaltotal, inv_avgcost
FROM public.maximo_material_use_transactions FROM public.wo_maxim_material
WHERE issuetype = 'ISSUE' WHERE wonum IN (SELECT wonum FROM filtered_wo)
AND wonum IN (SELECT wonum FROM filtered_wo) )
)
SELECT SELECT
fwo.asset_location AS location_tag, fwo.asset_location AS location_tag,
ft.itemnum, ft.itemnum,
spl.description AS sparepart_name, spl.description AS sparepart_name,
COUNT(*) AS parts_consumed_in_oh, COALESCE(SUM(ft.itemqty), 0) AS parts_consumed_in_oh,
MIN(ft.curbal) AS min_remaining_balance, COALESCE(AVG(ft.inv_avgcost), 0) AS avgcost,
MAX(mi.curbaltotal) AS inv_curbaltotal COALESCE(AVG(ft.inv_curbaltotal), 0) AS inv_curbaltotal
FROM filtered_wo fwo FROM filtered_wo fwo
INNER JOIN filtered_transactions ft ON fwo.wonum = ft.wonum INNER JOIN filtered_materials ft ON fwo.wonum = ft.wonum
INNER JOIN public.maximo_inventory mi ON ft.itemnum = mi.itemnum
LEFT JOIN public.maximo_sparepart_pr_po_line spl ON ft.itemnum = spl.item_num LEFT JOIN public.maximo_sparepart_pr_po_line spl ON ft.itemnum = spl.item_num
GROUP BY fwo.asset_location, ft.itemnum, spl.description GROUP BY fwo.asset_location, ft.itemnum, spl.description
ORDER BY fwo.asset_location, ft.itemnum ORDER BY fwo.asset_location, ft.itemnum
@ -123,32 +207,4 @@ async def get_all(
except Exception as e: except Exception as e:
# Log the error appropriately in your application # Log the error appropriately in your application
print(f"Database query error: {e}") print(f"Database query error: {e}")
raise 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()

@ -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): async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
query = text(""" query = text("""
WITH part_costs AS ( part_costs AS (
SELECT SELECT
mu.wonum, wm.wonum,
SUM(mu.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost SUM(wm.itemqty * COALESCE(wm.inv_avgcost, po.unit_cost, 0)) AS parts_total_cost
FROM maximo_workorder_materials mu FROM public.wo_maxim_material wm
LEFT JOIN maximo_inventory inv LEFT JOIN (
ON mu.itemnum = inv.itemnum SELECT item_num, AVG(unit_cost) AS unit_cost
LEFT JOIN ( FROM public.maximo_sparepart_pr_po_line
SELECT item_num, AVG(unit_cost) AS unit_cost GROUP BY item_num
FROM maximo_sparepart_pr_po_line ) po ON wm.itemnum = po.item_num
GROUP BY item_num WHERE wm.itemnum IS NOT NULL
) po GROUP BY wm.wonum
ON mu.itemnum = po.item_num ),
GROUP BY mu.wonum wo_costs AS (
), SELECT
wo_costs AS ( w.wonum,
SELECT w.asset_location,
w.wonum, (COALESCE(pc.parts_total_cost, 0)) AS total_wo_cost
w.asset_location, FROM wo_staging_maximo_2 w
(COALESCE(w.mat_cost_max, 0) + COALESCE(pc.parts_total_cost, 0)) AS total_wo_cost LEFT JOIN part_costs pc
FROM wo_staging_maximo_2 w ON w.wonum = pc.wonum
LEFT JOIN part_costs pc WHERE
ON w.wonum = pc.wonum w.worktype = 'OH'
WHERE AND w.asset_system IN (
w.worktype = 'OH' 'HPB', 'AH', 'APC', 'SCR', 'CL', 'DM', 'CRH', 'ASH', 'BAD', 'DS', 'WTP',
AND w.asset_system IN ( 'MT', 'SUP', 'DCS', 'FF', 'EG', 'AI', 'SPS', 'EVM', 'SCW', 'KLH', 'CH',
'HPB', 'AH', 'APC', 'SCR', 'CL', 'DM', 'CRH', 'ASH', 'BAD', 'DS', 'WTP', 'TUR', 'LOT', 'HRH', 'ESP', 'CAE', 'GMC', 'BFT', 'LSH', 'CHB', 'BSS',
'MT', 'SUP', 'DCS', 'FF', 'EG', 'AI', 'SPS', 'EVM', 'SCW', 'KLH', 'CH', 'LOS', 'LPB', 'SAC', 'CP', 'EHS', 'RO', 'GG', 'MS', 'CW', 'SO', 'ATT',
'TUR', 'LOT', 'HRH', 'ESP', 'CAE', 'GMC', 'BFT', 'LSH', 'CHB', 'BSS', 'AFG', 'EHB', 'RP', 'FO', 'PC', 'APE', 'AF', 'DMW', 'BRS', 'GEN', 'ABS',
'LOS', 'LPB', 'SAC', 'CP', 'EHS', 'RO', 'GG', 'MS', 'CW', 'SO', 'ATT', 'CHA', 'TR', 'H2', 'BDW', 'LOM', 'ACR', 'AL', 'FW', 'COND', 'CCCW', 'IA',
'AFG', 'EHB', 'RP', 'FO', 'PC', 'APE', 'AF', 'DMW', 'BRS', 'GEN', 'ABS', 'GSS', 'BOL', 'SSB', 'CO', 'OA', 'CTH-UPD', 'AS', 'DP'
'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.reportdate IS NOT NULL AND w.actfinish IS NOT NULL
AND w.actstart IS NOT NULL AND w.asset_unit IN ('3', '00')
AND w.actfinish IS NOT NULL AND w.reportdate >= '2019-01-01'
AND w.asset_unit IN ('3', '00') AND w.wonum NOT LIKE 'T%'
AND w.reportdate >= '2015-01-01' )
AND w.wonum NOT LIKE 'T%' SELECT
) asset_location,
SELECT AVG(total_wo_cost) as avg_cost
asset_location, FROM wo_costs
SUM(total_wo_cost)::numeric / COUNT(wonum) AS avg_cost GROUP BY asset_location
FROM wo_costs ORDER BY COUNT(wonum) DESC;
GROUP BY asset_location """)
ORDER BY COUNT(wonum) DESC;
""")
result = await collector_db.execute(query) result = await collector_db.execute(query)
data = [] 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 typing import Any, Dict, List, Optional
from uuid import UUID from uuid import UUID
@ -35,41 +37,41 @@ class ActivityMasterRead(ActivityMaster):
class ActivityMasterPagination(Pagination): class ActivityMasterPagination(Pagination):
items: List[ActivityMasterRead] = [] items: List[ActivityMasterRead] = []
class ProcurementStatus(Enum):
# { PLANNED = "planned"
# "overview": { ORDERED = "ordered"
# "totalEquipment": 30, RECEIVED = "received"
# "nextSchedule": { CANCELLED = "cancelled"
# "date": "2025-01-12",
# "Overhaul": "B", @dataclass
# "equipmentCount": 30 class SparepartRequirement:
# } """Sparepart requirement for equipment overhaul"""
# }, sparepart_id: str
# "criticalParts": [ quantity_required: int
# "Boiler feed pump", lead_time: int
# "Boiler reheater system", sparepart_name: str
# "Drum Level (Right) Root Valve A", unit_cost: float
# "BCP A Discharge Valve", avg_cost: float
# "BFPT A EXH Press HI Root VLV"
# ], @dataclass
# "schedules": [ class SparepartStock:
# { """Current sparepart stock information"""
# "date": "2025-01-12", sparepart_id: str
# "Overhaul": "B", sparepart_name: str
# "status": "upcoming" current_stock: int
# } unit_cost: float
# // ... other scheduled overhauls location: str
# ],
# "systemComponents": { @dataclass
# "boiler": { class ProcurementRecord:
# "status": "operational", """Purchase Order/Purchase Request record"""
# "lastOverhaul": "2024-06-15" po_pr_id: str
# }, sparepart_id: str
# "turbine": { sparepart_name: str
# "hpt": { "status": "operational" }, quantity: int
# "ipt": { "status": "operational" }, unit_cost: float
# "lpt": { "status": "operational" } total_cost: float
# } order_date: date
# // ... other major components 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_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 as get_scope, get_overview_overhaul
from src.overhaul_scope.service import get_prev_oh from src.overhaul_scope.service import get_prev_oh
from src.sparepart.schema import ProcurementRecord, ProcurementStatus, SparepartRequirement, SparepartStock
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -27,6 +28,177 @@ setup_logging(logger=log)
from sqlalchemy import text from sqlalchemy import text
import math 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): async def get_spareparts_paginated(*, db_session, collector_db_session):
""" """
Get paginated spare parts with usage, inventory, and PR/PO information. 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 asset_location IS NOT NULL
AND EXTRACT(YEAR FROM reportdate) >= 2019 AND EXTRACT(YEAR FROM reportdate) >= 2019
AND asset_unit IN ('3', '00') 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 ( location_sparepart_stats AS (
SELECT asset_location, itemnum, SELECT location_tag, itemnum,
COUNT(DISTINCT wonum) as total_wo_count, COUNT(DISTINCT wonum) as total_wo_count,
SUM(itemqty) as total_qty_used, SUM(itemqty) as total_qty_used,
AVG(itemqty) as avg_qty_per_wo, AVG(itemqty) as avg_qty_per_wo,
MIN(itemqty) as min_qty_used, MIN(itemqty) as min_qty_used,
MAX(itemqty) as max_qty_used MAX(itemqty) as max_qty_used
FROM sparepart_usage FROM wo_materials
GROUP BY asset_location, itemnum GROUP BY location_tag, itemnum
HAVING SUM(itemqty) > 0 HAVING SUM(itemqty) > 0
), ),
pr_lines AS ( 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, ROUND(CAST(lss.avg_qty_per_wo AS NUMERIC), 2) as avg_qty_per_wo,
lss.min_qty_used, lss.min_qty_used,
lss.max_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_pr_qty,0) as total_pr_qty,
COALESCE(ap.total_po_qty,0) as total_po_qty, COALESCE(ap.total_po_qty,0) as total_po_qty,
COALESCE(ap.total_po_received,0) as total_po_received, COALESCE(ap.total_po_received,0) as total_po_received,
ap.pr_po_details ap.pr_po_details
FROM location_sparepart_stats lss FROM location_sparepart_stats lss
LEFT JOIN item_descriptions id ON lss.itemnum = id.item_num 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 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) 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 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: class SparepartManager:
"""Manages sparepart availability and procurement for overhaul optimization""" """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 # Load sparepart stocks
# Example query - adjust based on your schema # Example query - adjust based on your schema
query = text(""" query = text("""SELECT
SELECT wm.inv_itemnum AS itemnum,
mi.id, wm.inv_itemsetid AS itemsetid,
mi.itemnum, wm.inv_location AS location,
mi.itemsetid, MAX(wm.inv_curbaltotal) AS curbaltotal,
mi."location", AVG(wm.inv_avgcost) AS avgcost,
mi.curbaltotal, COALESCE(mspl.description, 'No description available') AS description
mi.avgcost, FROM public.wo_maxim_material wm
mspl.description LEFT JOIN public.maximo_sparepart_pr_po_line mspl
FROM public.maximo_inventory mi ON wm.inv_itemnum = mspl.item_num
LEFT JOIN public.maximo_sparepart_pr_po_line mspl WHERE wm.inv_itemnum IS NOT NULL
ON mi.itemnum = mspl.item_num GROUP BY wm.inv_itemnum, wm.inv_itemsetid, wm.inv_location, mspl.description
""") """)
log.info("Fetch sparepart") log.info("Fetch sparepart")
sparepart_stocks_query = await db_session.execute(query) 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) 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 # You'll need to create this table/relationship
query = text("""WITH oh_workorders AS ( query = text("""WITH oh_workorders AS (
-- First, get all OH work orders -- 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, wonum,
asset_location asset_location
FROM public.wo_staging_maximo_2 FROM public.wo_staging_maximo_2
WHERE worktype = 'OH' AND asset_location IS NOT NULL and asset_unit IN ('3', '00') WHERE worktype = 'OH' AND asset_location IS NOT NULL and asset_unit IN ('3', '00') AND EXTRACT(YEAR FROM reportdate) >= 2019
),
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
), ),
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 ( location_sparepart_stats AS (
-- Calculate average usage per sparepart per location -- Calculate average usage per sparepart per location
SELECT SELECT
@ -947,7 +1201,7 @@ SELECT
ROUND(CAST(lss.avg_qty_per_wo AS NUMERIC), 2) as avg_qty_per_wo, ROUND(CAST(lss.avg_qty_per_wo AS NUMERIC), 2) as avg_qty_per_wo,
lss.min_qty_used, lss.min_qty_used,
lss.max_qty_used, lss.max_qty_used,
iin.avgcost, iin.inv_avgcost,
-- Lead time metrics -- Lead time metrics
COALESCE(lt.avg_leadtime_months, 0) as avg_leadtime_months, COALESCE(lt.avg_leadtime_months, 0) as avg_leadtime_months,
COALESCE(lt.min_leadtime_months, 0) as min_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 item_descriptions id ON lss.itemnum = id.item_num
LEFT JOIN leadtime_stats lt ON lss.itemnum = lt.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 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;""") ORDER BY lss.asset_location, lss.itemnum;""")
equipment_requirements_query = await db_session.execute(query) 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 INNER JOIN public.maximo_sparepart_pr_po pr
ON pr.num = po.po_number ON pr.num = po.po_number
AND pr.type = 'PR' 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 SELECT
po.item_num, po.item_num,
po.description, po.description,
po.line_cost, po.line_cost,
po.unit_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.po_number,
po.pr_issue_date, po.pr_issue_date,
po.po_status, po.po_status,
@ -1040,7 +1303,7 @@ SELECT
po.estimated_arrival_date as po_estimated_arrival_date, po.estimated_arrival_date as po_estimated_arrival_date,
po.vendeliverydate as po_vendor_delivery_date po.vendeliverydate as po_vendor_delivery_date
FROM po_with_pr_date po FROM po_with_pr_date po
LEFT JOIN public.maximo_inventory i LEFT JOIN item_inventory i
ON po.item_num = i.itemnum ON po.item_num = i.itemnum
ORDER BY po.item_num, po.pr_issue_date DESC; ORDER BY po.item_num, po.pr_issue_date DESC;
""") """)

Loading…
Cancel
Save