|
|
|
|
@ -9,147 +9,402 @@ import numpy as np
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from enum import Enum
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import Delete, Select, text
|
|
|
|
|
from sqlalchemy import Delete, Select, select, text
|
|
|
|
|
from sqlalchemy.orm import joinedload, selectinload
|
|
|
|
|
|
|
|
|
|
from src.auth.service import CurrentUser
|
|
|
|
|
from src.database.core import DbSession
|
|
|
|
|
from src.database.service import CommonParameters, search_filter_sort_paginate
|
|
|
|
|
from src.logging import setup_logging
|
|
|
|
|
from src.overhaul_scope.service import get as get_scope
|
|
|
|
|
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.model import SparepartRemark
|
|
|
|
|
from src.sparepart.schema import ProcurementRecord, ProcurementStatus, SparepartRequirement, SparepartStock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
setup_logging(logger=log)
|
|
|
|
|
|
|
|
|
|
async def get_all(db_session: DbSession):
|
|
|
|
|
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 all spare parts with their latest PR and PO information.
|
|
|
|
|
Get paginated spare parts with usage, inventory, and PR/PO information.
|
|
|
|
|
Uses two queries: one for data, one for total count.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db_session: SQLAlchemy database session
|
|
|
|
|
assetnum: Optional asset number filter (not used in this query but kept for compatibility)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of dictionaries containing spare part information
|
|
|
|
|
page (int): Page number (1-based)
|
|
|
|
|
items_per_page (int): Number of items per page
|
|
|
|
|
"""
|
|
|
|
|
# Define the SQL query
|
|
|
|
|
query = text("""
|
|
|
|
|
WITH latest_prs AS (
|
|
|
|
|
SELECT DISTINCT ON (pl.item_num)
|
|
|
|
|
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.description,
|
|
|
|
|
pl.unit_cost,
|
|
|
|
|
pl.line_cost
|
|
|
|
|
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 h.issue_date IS NOT NULL
|
|
|
|
|
AND h.num LIKE 'K%'
|
|
|
|
|
ORDER BY pl.item_num, h.issue_date DESC
|
|
|
|
|
)
|
|
|
|
|
SELECT DISTINCT ON (pr.item_num)
|
|
|
|
|
pr.item_num,
|
|
|
|
|
pr.line_cost,
|
|
|
|
|
pr.unit_cost,
|
|
|
|
|
pr.description,
|
|
|
|
|
COALESCE(i.curbaltotal, 0) as current_balance_total,
|
|
|
|
|
pr.pr_number,
|
|
|
|
|
pr.pr_issue_date,
|
|
|
|
|
pr.pr_qty_ordered,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN po.po_number IS NOT NULL THEN 'YES'
|
|
|
|
|
ELSE 'NO'
|
|
|
|
|
END as po_exists,
|
|
|
|
|
COALESCE(po.qty_received, 0) as po_qty_received,
|
|
|
|
|
COALESCE(po.qty_ordered, 0) as po_qty_ordered,
|
|
|
|
|
po.estimated_arrival_date as po_estimated_arrival_date
|
|
|
|
|
FROM latest_prs pr
|
|
|
|
|
LEFT JOIN public.maximo_inventory i ON pr.item_num = i.itemnum
|
|
|
|
|
LEFT JOIN LATERAL (
|
|
|
|
|
# calculate limit/offset
|
|
|
|
|
# limit = items_per_page
|
|
|
|
|
# offset = (page - 1) * items_per_page
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
# ),
|
|
|
|
|
|
|
|
|
|
# -----------------------------
|
|
|
|
|
# Query #1: Fetch paginated rows
|
|
|
|
|
# -----------------------------
|
|
|
|
|
data_query = text("""
|
|
|
|
|
WITH oh_workorders AS (
|
|
|
|
|
SELECT DISTINCT wonum, asset_location, asset_unit
|
|
|
|
|
FROM public.wo_maximo ma
|
|
|
|
|
WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')
|
|
|
|
|
),
|
|
|
|
|
wo_materials AS (
|
|
|
|
|
SELECT
|
|
|
|
|
wm.wonum,
|
|
|
|
|
wm.itemnum,
|
|
|
|
|
wm.itemqty,
|
|
|
|
|
wm.inv_location AS inv_location,
|
|
|
|
|
wm.inv_curbaltotal AS inv_curbaltotal,
|
|
|
|
|
wm.inv_avgcost AS inv_avgcost,
|
|
|
|
|
sw.asset_location as location_tag
|
|
|
|
|
FROM public.wo_maximo_material wm
|
|
|
|
|
JOIN oh_workorders sw ON wm.wonum = sw.wonum
|
|
|
|
|
),
|
|
|
|
|
location_sparepart_stats AS (
|
|
|
|
|
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 wo_materials
|
|
|
|
|
GROUP BY location_tag, itemnum
|
|
|
|
|
HAVING SUM(itemqty) > 0
|
|
|
|
|
),
|
|
|
|
|
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) >= 2023
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
po_lines AS (
|
|
|
|
|
SELECT
|
|
|
|
|
pl.item_num,
|
|
|
|
|
h.num as po_number,
|
|
|
|
|
pl.qty_received,
|
|
|
|
|
pl.qty_ordered,
|
|
|
|
|
h.estimated_arrival_date
|
|
|
|
|
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.num = pr.pr_number
|
|
|
|
|
AND pl.item_num = pr.item_num
|
|
|
|
|
LIMIT 1
|
|
|
|
|
) po ON true
|
|
|
|
|
ORDER BY pr.item_num;
|
|
|
|
|
AND (h.receipts = 'NONE')
|
|
|
|
|
AND (h.status IS NOT NULL)
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
inv_summary AS (
|
|
|
|
|
SELECT
|
|
|
|
|
itemnum,
|
|
|
|
|
MAX(inv_curbaltotal) AS total_curbaltotal,
|
|
|
|
|
AVG(inv_avgcost) AS avg_cost
|
|
|
|
|
FROM wo_materials
|
|
|
|
|
GROUP BY itemnum
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
lss.itemnum,
|
|
|
|
|
COALESCE(id.description, 'No description available') as item_description,
|
|
|
|
|
lss.total_wo_count,
|
|
|
|
|
lss.total_qty_used,
|
|
|
|
|
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.total_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 inv_summary i ON lss.itemnum = i.itemnum
|
|
|
|
|
LEFT JOIN pr_po_agg ap ON lss.itemnum = ap.item_num
|
|
|
|
|
ORDER BY lss.location_tag, lss.itemnum;
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Execute the query
|
|
|
|
|
result = await db_session.execute(query)
|
|
|
|
|
overhaul = await get_overview_overhaul(db_session=db_session)
|
|
|
|
|
|
|
|
|
|
# Fetch all results and convert to list of dictionaries
|
|
|
|
|
standard_overhaul = await get_standard_scope_by_session_id(db_session=db_session, collector_db=collector_db_session, overhaul_session_id=overhaul['overhaul']['id'])
|
|
|
|
|
|
|
|
|
|
asset_locations = [eq.location_tag for eq in standard_overhaul]
|
|
|
|
|
|
|
|
|
|
rows = await collector_db_session.execute(
|
|
|
|
|
data_query,
|
|
|
|
|
{"asset_locations": asset_locations}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
sparepart_remark = (await db_session.execute(
|
|
|
|
|
select(SparepartRemark)
|
|
|
|
|
)).scalars().all()
|
|
|
|
|
|
|
|
|
|
sparepart_remark_dict = {item.itemnum: item.remark for item in sparepart_remark}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
spare_parts = []
|
|
|
|
|
for row in result:
|
|
|
|
|
for row in rows:
|
|
|
|
|
spare_parts.append({
|
|
|
|
|
"item_num": row.item_num,
|
|
|
|
|
"description": row.description,
|
|
|
|
|
"line_cost": row.line_cost,
|
|
|
|
|
"unit_cost": row.unit_cost,
|
|
|
|
|
"current_balance_total": float(row.current_balance_total) if row.current_balance_total is not None else 0.0,
|
|
|
|
|
"pr_number": row.pr_number,
|
|
|
|
|
"pr_issue_date": row.pr_issue_date,
|
|
|
|
|
"pr_qty_ordered": float(row.pr_qty_ordered) if row.pr_qty_ordered is not None else 0.0,
|
|
|
|
|
"po_exists": row.po_exists,
|
|
|
|
|
"po_qty_received": float(row.po_qty_received) if row.po_qty_received is not None else 0.0,
|
|
|
|
|
"po_qty_ordered": float(row.po_qty_ordered) if row.po_qty_ordered is not None else 0.0,
|
|
|
|
|
"po_estimated_arrival_date": row.po_estimated_arrival_date
|
|
|
|
|
"item_num": row.itemnum,
|
|
|
|
|
"description": row.item_description,
|
|
|
|
|
"remark": sparepart_remark_dict.get(row.itemnum, ""),
|
|
|
|
|
"current_balance_total": float(row.current_balance_total) if row.current_balance_total else 0.0,
|
|
|
|
|
"total_required_for_oh": float(row.avg_qty_per_wo),
|
|
|
|
|
"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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
@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"""
|
|
|
|
|
|
|
|
|
|
@ -279,7 +534,8 @@ class SparepartManager:
|
|
|
|
|
sparepart_id = requirement.sparepart_id
|
|
|
|
|
needed_quantity = requirement.quantity_required
|
|
|
|
|
sparepart_name = requirement.sparepart_name
|
|
|
|
|
unit_cost = requirement.unit_cost
|
|
|
|
|
sparepart_remark= requirement.remark
|
|
|
|
|
unit_cost = requirement.avg_cost if requirement.avg_cost > 0 else requirement.unit_cost
|
|
|
|
|
|
|
|
|
|
current_stock = adjusted_stocks.get(sparepart_id, 0)
|
|
|
|
|
|
|
|
|
|
@ -307,7 +563,8 @@ class SparepartManager:
|
|
|
|
|
'status': order.status.value,
|
|
|
|
|
'months_until_delivery': self._calculate_months_until_delivery(order.expected_delivery_date, target_month),
|
|
|
|
|
'is_on_time': self._is_delivery_on_time(order.expected_delivery_date, target_month),
|
|
|
|
|
'usage': 'covers_requirement'
|
|
|
|
|
'usage': 'covers_requirement',
|
|
|
|
|
'remark': sparepart_remark
|
|
|
|
|
}
|
|
|
|
|
pr_po_summary['existing_orders'].append(order_info)
|
|
|
|
|
pr_po_summary['total_existing_value'] += order.total_cost
|
|
|
|
|
@ -319,6 +576,7 @@ class SparepartManager:
|
|
|
|
|
missing_parts.append({
|
|
|
|
|
'sparepart_id': sparepart_id,
|
|
|
|
|
'sparepart_name': sparepart_name,
|
|
|
|
|
'remark': sparepart_remark,
|
|
|
|
|
'required': needed_quantity,
|
|
|
|
|
'current_stock': current_stock,
|
|
|
|
|
'ordered_quantity': total_ordered_quantity,
|
|
|
|
|
@ -350,6 +608,7 @@ class SparepartManager:
|
|
|
|
|
new_order = {
|
|
|
|
|
'sparepart_id': sparepart_id,
|
|
|
|
|
'sparepart_name': sparepart_name,
|
|
|
|
|
'remark': sparepart_remark,
|
|
|
|
|
'quantity_needed': shortage,
|
|
|
|
|
'unit_cost': unit_cost,
|
|
|
|
|
'total_cost': procurement_cost,
|
|
|
|
|
@ -656,7 +915,7 @@ class SparepartManager:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Integration functions for database operations
|
|
|
|
|
async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session) -> SparepartManager:
|
|
|
|
|
async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, app_db_session, analysis_window_months = None) -> SparepartManager:
|
|
|
|
|
"""Load sparepart data from database"""
|
|
|
|
|
# You'll need to implement these queries based on your database schema
|
|
|
|
|
# Get scope dates for analysis window
|
|
|
|
|
@ -664,31 +923,40 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session) -> Spare
|
|
|
|
|
# prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
|
|
|
|
|
|
|
|
|
analysis_start_date = prev_oh_scope.end_date
|
|
|
|
|
analysis_window_months = int(((scope.start_date - prev_oh_scope.end_date).days / 30) * 1.5)
|
|
|
|
|
analysis_window_months = int(((scope.start_date - prev_oh_scope.end_date).days / 30) * 1.2) if not analysis_window_months else analysis_window_months
|
|
|
|
|
|
|
|
|
|
sparepart_manager = SparepartManager(analysis_start_date, analysis_window_months)
|
|
|
|
|
start_date = prev_oh_scope.end_date
|
|
|
|
|
end_date = scope.start_date
|
|
|
|
|
|
|
|
|
|
# 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_maximo_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)
|
|
|
|
|
|
|
|
|
|
sparepart_remark = (await app_db_session.execute(
|
|
|
|
|
select(SparepartRemark)
|
|
|
|
|
)).scalars().all()
|
|
|
|
|
|
|
|
|
|
sparepart_remark_dict = {item.itemnum: item.remark for item in sparepart_remark}
|
|
|
|
|
|
|
|
|
|
for stock_record in sparepart_stocks_query:
|
|
|
|
|
stock = SparepartStock(
|
|
|
|
|
sparepart_id=stock_record.itemnum,
|
|
|
|
|
remark=sparepart_remark_dict.get(stock_record.itemnum),
|
|
|
|
|
sparepart_name=stock_record.description,
|
|
|
|
|
current_stock=stock_record.curbaltotal,
|
|
|
|
|
unit_cost=stock_record.avgcost,
|
|
|
|
|
@ -696,27 +964,186 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session) -> Spare
|
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
SELECT DISTINCT
|
|
|
|
|
wonum,
|
|
|
|
|
asset_location
|
|
|
|
|
FROM public.wo_staging_maximo_2
|
|
|
|
|
WHERE worktype = 'OH' AND asset_location IS NOT NULL
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
-- First, get all OH work orders
|
|
|
|
|
SELECT DISTINCT
|
|
|
|
|
wonum,
|
|
|
|
|
asset_location
|
|
|
|
|
FROM public.wo_maximo ma
|
|
|
|
|
WHERE worktype = 'OH' AND asset_location IS NOT NULL and asset_unit IN ('3', '00') AND EXTRACT(YEAR FROM reportdate) >= 2019
|
|
|
|
|
),
|
|
|
|
|
current_oh as (
|
|
|
|
|
SELECT DISTINCT wonum, asset_location, asset_unit
|
|
|
|
|
FROM public.wo_maximo ma
|
|
|
|
|
WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')
|
|
|
|
|
),
|
|
|
|
|
sparepart_usage AS (
|
|
|
|
|
SELECT
|
|
|
|
|
oh.asset_location,
|
|
|
|
|
mwm.itemnum,
|
|
|
|
|
mwm.itemqty,
|
|
|
|
|
mwm.wonum,
|
|
|
|
|
mwm.inv_avgcost
|
|
|
|
|
FROM current_oh oh
|
|
|
|
|
INNER JOIN public.wo_maximo_material mwm
|
|
|
|
|
ON oh.wonum = mwm.wonum
|
|
|
|
|
),
|
|
|
|
|
location_sparepart_stats AS (
|
|
|
|
|
-- Calculate average usage per sparepart per location
|
|
|
|
|
SELECT
|
|
|
|
|
@ -801,6 +1228,12 @@ item_descriptions AS (
|
|
|
|
|
) as description
|
|
|
|
|
FROM public.maximo_sparepart_pr_po_line
|
|
|
|
|
WHERE description IS NOT NULL
|
|
|
|
|
),
|
|
|
|
|
item_inventory as (
|
|
|
|
|
SELECT
|
|
|
|
|
itemnum,
|
|
|
|
|
avgcost
|
|
|
|
|
FROM public.maximo_inventory
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
lss.asset_location,
|
|
|
|
|
@ -811,6 +1244,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.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,
|
|
|
|
|
@ -831,6 +1265,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 sparepart_usage iin ON lss.itemnum = iin.itemnum
|
|
|
|
|
ORDER BY lss.asset_location, lss.itemnum;""")
|
|
|
|
|
|
|
|
|
|
equipment_requirements_query = await db_session.execute(query)
|
|
|
|
|
@ -842,7 +1277,9 @@ ORDER BY lss.asset_location, lss.itemnum;""")
|
|
|
|
|
quantity_required=float(req_record.avg_qty_per_wo),
|
|
|
|
|
lead_time=float(req_record.avg_leadtime_months),
|
|
|
|
|
sparepart_name=req_record.item_description,
|
|
|
|
|
unit_cost=float(req_record.avg_unit_cost)
|
|
|
|
|
unit_cost=float(req_record.avg_unit_cost),
|
|
|
|
|
avg_cost=float(req_record.inv_avgcost or 0),
|
|
|
|
|
remark=sparepart_remark_dict.get(req_record.itemnum, "")
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
equipment_requirements[req_record.asset_location].append(requirement)
|
|
|
|
|
@ -854,54 +1291,65 @@ ORDER BY lss.asset_location, lss.itemnum;""")
|
|
|
|
|
# Load procurement records (PO/PR)
|
|
|
|
|
query = text("""
|
|
|
|
|
WITH active_pos AS (
|
|
|
|
|
-- Get all POs that are NOT complete (not in inventory yet) and NOT closed
|
|
|
|
|
SELECT
|
|
|
|
|
pl.item_num,
|
|
|
|
|
h.num as po_number,
|
|
|
|
|
pl.qty_received,
|
|
|
|
|
pl.qty_ordered,
|
|
|
|
|
h.estimated_arrival_date,
|
|
|
|
|
h.vendeliverydate,
|
|
|
|
|
h.receipts as po_receipts,
|
|
|
|
|
h.status as po_status,
|
|
|
|
|
pl.description,
|
|
|
|
|
pl.unit_cost,
|
|
|
|
|
pl.line_cost
|
|
|
|
|
FROM public.maximo_sparepart_pr_po h
|
|
|
|
|
JOIN public.maximo_sparepart_pr_po_line pl ON h.num = pl.num
|
|
|
|
|
WHERE h.type = 'PO'
|
|
|
|
|
-- Exclude POs where receipts = 'COMPLETE'
|
|
|
|
|
AND (h.receipts IS NULL OR h.receipts != 'COMPLETE')
|
|
|
|
|
-- Exclude closed POs
|
|
|
|
|
AND (h.status IS NULL OR h.status != 'CLOSE')
|
|
|
|
|
),
|
|
|
|
|
po_with_pr_date AS (
|
|
|
|
|
-- Join with PR to get the issue_date
|
|
|
|
|
SELECT
|
|
|
|
|
po.*,
|
|
|
|
|
pr.issue_date as pr_issue_date
|
|
|
|
|
FROM active_pos po
|
|
|
|
|
LEFT JOIN public.maximo_sparepart_pr_po pr
|
|
|
|
|
ON pr.num = po.po_number
|
|
|
|
|
AND pr.type = 'PR'
|
|
|
|
|
)
|
|
|
|
|
-- Get all POs that are NOT complete (not in inventory yet) and NOT closed
|
|
|
|
|
SELECT
|
|
|
|
|
pl.item_num,
|
|
|
|
|
h.num as po_number,
|
|
|
|
|
pl.qty_received,
|
|
|
|
|
pl.qty_ordered,
|
|
|
|
|
h.estimated_arrival_date,
|
|
|
|
|
h.vendeliverydate,
|
|
|
|
|
h.receipts as po_receipts,
|
|
|
|
|
h.status as po_status,
|
|
|
|
|
pl.description,
|
|
|
|
|
pl.unit_cost,
|
|
|
|
|
pl.line_cost
|
|
|
|
|
FROM public.maximo_sparepart_pr_po h
|
|
|
|
|
JOIN public.maximo_sparepart_pr_po_line pl
|
|
|
|
|
ON h.num = pl.num
|
|
|
|
|
WHERE h.type = 'PO'
|
|
|
|
|
-- Exclude POs where receipts = 'COMPLETE'
|
|
|
|
|
AND (h.receipts IS NULL OR h.receipts != 'COMPLETE')
|
|
|
|
|
-- Exclude closed POs
|
|
|
|
|
AND (h.status IS NULL OR h.status = 'APPR')
|
|
|
|
|
),
|
|
|
|
|
po_with_pr_date AS (
|
|
|
|
|
-- Force join with PR to ensure every PO has a PR
|
|
|
|
|
SELECT
|
|
|
|
|
po.*,
|
|
|
|
|
pr.issue_date as pr_issue_date
|
|
|
|
|
FROM active_pos po
|
|
|
|
|
INNER JOIN public.maximo_sparepart_pr_po pr
|
|
|
|
|
ON pr.num = po.po_number
|
|
|
|
|
AND pr.type = 'PR'
|
|
|
|
|
),
|
|
|
|
|
item_inventory AS (
|
|
|
|
|
SELECT
|
|
|
|
|
po.item_num,
|
|
|
|
|
po.description,
|
|
|
|
|
po.line_cost,
|
|
|
|
|
po.unit_cost,
|
|
|
|
|
COALESCE(i.curbaltotal, 0) as current_balance_total,
|
|
|
|
|
po.po_number,
|
|
|
|
|
po.pr_issue_date,
|
|
|
|
|
po.po_status,
|
|
|
|
|
po.po_receipts,
|
|
|
|
|
COALESCE(po.qty_received, 0) as po_qty_received,
|
|
|
|
|
COALESCE(po.qty_ordered, 0) as po_qty_ordered,
|
|
|
|
|
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 ON po.item_num = i.itemnum
|
|
|
|
|
ORDER BY po.item_num, po.pr_issue_date DESC;
|
|
|
|
|
itemnum,
|
|
|
|
|
MAX(inv_curbaltotal) AS current_balance_total,
|
|
|
|
|
AVG(inv_avgcost) AS avg_cost
|
|
|
|
|
FROM public.wo_maximo_material
|
|
|
|
|
WHERE inv_itemnum IS NOT NULL
|
|
|
|
|
GROUP BY itemnum
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
po.item_num,
|
|
|
|
|
po.description,
|
|
|
|
|
po.line_cost,
|
|
|
|
|
po.unit_cost,
|
|
|
|
|
COALESCE(i.current_balance_total, 0) as current_balance_total,
|
|
|
|
|
po.po_number,
|
|
|
|
|
po.pr_issue_date,
|
|
|
|
|
po.po_status,
|
|
|
|
|
po.po_receipts,
|
|
|
|
|
COALESCE(po.qty_received, 0) as po_qty_received,
|
|
|
|
|
COALESCE(po.qty_ordered, 0) as po_qty_ordered,
|
|
|
|
|
po.estimated_arrival_date as po_estimated_arrival_date,
|
|
|
|
|
po.vendeliverydate as po_vendor_delivery_date
|
|
|
|
|
FROM po_with_pr_date po
|
|
|
|
|
LEFT JOIN item_inventory i
|
|
|
|
|
ON po.item_num = i.itemnum
|
|
|
|
|
ORDER BY po.item_num, po.pr_issue_date DESC;
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Execute the query
|
|
|
|
|
@ -949,4 +1397,29 @@ ORDER BY lss.asset_location, lss.itemnum;""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def create_remark(*, db_session, collector_db_session, remark_in):
|
|
|
|
|
# Step 1: Check if remark already exists for this itemnum
|
|
|
|
|
result = await db_session.execute(
|
|
|
|
|
select(SparepartRemark).where(SparepartRemark.itemnum == remark_in.itemnum)
|
|
|
|
|
)
|
|
|
|
|
existing_remark = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
# Step 2: If it already exists, you can decide what to do
|
|
|
|
|
if existing_remark:
|
|
|
|
|
# Option B: Update existing remark (if needed)
|
|
|
|
|
existing_remark.remark = remark_in.remark
|
|
|
|
|
await db_session.commit()
|
|
|
|
|
await db_session.refresh(existing_remark)
|
|
|
|
|
return existing_remark
|
|
|
|
|
|
|
|
|
|
# Step 3: If it doesn’t exist, create new one
|
|
|
|
|
new_remark = SparepartRemark(
|
|
|
|
|
itemnum=remark_in.itemnum,
|
|
|
|
|
remark=remark_in.remark,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
db_session.add(new_remark)
|
|
|
|
|
await db_session.commit()
|
|
|
|
|
await db_session.refresh(new_remark)
|
|
|
|
|
|
|
|
|
|
return new_remark
|
|
|
|
|
|