From 7a9d5aacabdf95e377178dcdb7808e099c5fb1d3 Mon Sep 17 00:00:00 2001 From: Cizz22 Date: Fri, 17 Oct 2025 08:04:21 +0700 Subject: [PATCH] fix query --- src/equipment_sparepart/service.py | 186 +++++++---- src/maximo/service.py | 162 ++++++--- src/sparepart/schema.py | 80 ++--- src/sparepart/service.py | 505 ++++++++++++++++++++++------- 4 files changed, 660 insertions(+), 273 deletions(-) diff --git a/src/equipment_sparepart/service.py b/src/equipment_sparepart/service.py index 19a7c7d..9cec348 100644 --- a/src/equipment_sparepart/service.py +++ b/src/equipment_sparepart/service.py @@ -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 \ No newline at end of file diff --git a/src/maximo/service.py b/src/maximo/service.py index 595329a..b046686 100644 --- a/src/maximo/service.py +++ b/src/maximo/service.py @@ -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 = [] diff --git a/src/sparepart/schema.py b/src/sparepart/schema.py index f9bc756..efa700a 100644 --- a/src/sparepart/schema.py +++ b/src/sparepart/schema.py @@ -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 \ No newline at end of file diff --git a/src/sparepart/service.py b/src/sparepart/service.py index da448a2..fec50af 100644 --- a/src/sparepart/service.py +++ b/src/sparepart/service.py @@ -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; """)