|
|
|
@ -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 (
|
|
|
|
wo_materials AS (
|
|
|
|
SELECT oh.asset_location, mwm.itemnum, mwm.itemqty, mwm.wonum
|
|
|
|
SELECT
|
|
|
|
FROM oh_workorders oh
|
|
|
|
wm.wonum,
|
|
|
|
INNER JOIN public.maximo_workorder_materials mwm ON oh.wonum = mwm.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,18 +900,18 @@ 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
|
|
|
|
|
|
|
|
FROM public.maximo_inventory mi
|
|
|
|
|
|
|
|
LEFT JOIN public.maximo_sparepart_pr_po_line mspl
|
|
|
|
LEFT JOIN public.maximo_sparepart_pr_po_line mspl
|
|
|
|
ON mi.itemnum = mspl.item_num
|
|
|
|
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")
|
|
|
|
log.info("Fetch sparepart")
|
|
|
|
sparepart_stocks_query = await db_session.execute(query)
|
|
|
|
sparepart_stocks_query = await db_session.execute(query)
|
|
|
|
@ -826,6 +926,160 @@ 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
# 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 (
|
|
|
|
@ -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 (
|
|
|
|
sparepart_usage AS (
|
|
|
|
-- Get sparepart usage for OH work orders
|
|
|
|
|
|
|
|
SELECT
|
|
|
|
SELECT
|
|
|
|
oh.asset_location,
|
|
|
|
oh.asset_location,
|
|
|
|
mwm.itemnum,
|
|
|
|
mwm.itemnum,
|
|
|
|
mwm.itemqty,
|
|
|
|
mwm.itemqty,
|
|
|
|
mwm.wonum
|
|
|
|
mwm.wonum,
|
|
|
|
|
|
|
|
mwm.inv_avgcost
|
|
|
|
FROM oh_workorders oh
|
|
|
|
FROM oh_workorders oh
|
|
|
|
INNER JOIN public.maximo_workorder_materials mwm
|
|
|
|
INNER JOIN public.wo_maxim_material mwm
|
|
|
|
ON oh.wonum = mwm.wonum
|
|
|
|
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;
|
|
|
|
""")
|
|
|
|
""")
|
|
|
|
|