Push the rest
This commit is contained in:
0
backend/app/domain/wwi/__init__.py
Normal file
0
backend/app/domain/wwi/__init__.py
Normal file
297
backend/app/domain/wwi/analytics.py
Normal file
297
backend/app/domain/wwi/analytics.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from opentelemetry import metrics, trace
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
|
||||
from app.core.audit import append_audit
|
||||
from app.domain.wwi.models import (
|
||||
WWIReorderRecommendation,
|
||||
WWISupplierScore,
|
||||
WWIWhatIfScenario,
|
||||
WWIBusinessEvent,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
tracer = trace.get_tracer("otel-bi.domain.wwi")
|
||||
meter = metrics.get_meter("otel-bi.domain.wwi")
|
||||
|
||||
_persist_counter = meter.create_counter(
|
||||
"wwi_persist_writes_total",
|
||||
description="Number of WWI PostgreSQL write operations",
|
||||
)
|
||||
_event_counter = meter.create_counter(
|
||||
"wwi_business_events_generated_total",
|
||||
description="Business events automatically generated",
|
||||
)
|
||||
|
||||
|
||||
def _current_span_context() -> tuple[str | None, str | None]:
|
||||
ctx = trace.get_current_span().get_span_context()
|
||||
if not ctx.is_valid:
|
||||
return None, None
|
||||
return f"{ctx.trace_id:032x}", f"{ctx.span_id:016x}"
|
||||
|
||||
|
||||
def _actor_type(trigger_source: str) -> str:
|
||||
return "scheduler" if trigger_source.startswith("scheduler") else "api"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Persist functions — called after Go service returns data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def persist_reorder_recommendations(
|
||||
factory: sessionmaker[Session],
|
||||
data: list[dict],
|
||||
trigger_source: str,
|
||||
) -> None:
|
||||
trace_id, span_id = _current_span_context()
|
||||
try:
|
||||
with factory() as session:
|
||||
session.add(WWIReorderRecommendation(
|
||||
item_count=len(data),
|
||||
trigger_source=trigger_source,
|
||||
trace_id=trace_id,
|
||||
span_id=span_id,
|
||||
payload=data,
|
||||
))
|
||||
session.commit()
|
||||
_persist_counter.add(1, {"entity": "reorder_recommendations"})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOGGER.warning("Failed to persist WWI reorder recommendations: %s", exc)
|
||||
append_audit(
|
||||
factory,
|
||||
action="recommendations.generated",
|
||||
actor_type=_actor_type(trigger_source),
|
||||
actor_id=trigger_source,
|
||||
domain="wwi",
|
||||
service="otel-bi-backend",
|
||||
entity_type="reorder_recommendations",
|
||||
payload={"item_count": len(data)},
|
||||
)
|
||||
|
||||
|
||||
def persist_supplier_scores(
|
||||
factory: sessionmaker[Session],
|
||||
data: list[dict],
|
||||
top_n: int,
|
||||
trigger_source: str,
|
||||
) -> None:
|
||||
trace_id, span_id = _current_span_context()
|
||||
try:
|
||||
with factory() as session:
|
||||
session.add(WWISupplierScore(
|
||||
supplier_count=len(data),
|
||||
top_n=top_n,
|
||||
trigger_source=trigger_source,
|
||||
trace_id=trace_id,
|
||||
span_id=span_id,
|
||||
payload=data,
|
||||
))
|
||||
session.commit()
|
||||
_persist_counter.add(1, {"entity": "supplier_scores"})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOGGER.warning("Failed to persist WWI supplier scores: %s", exc)
|
||||
append_audit(
|
||||
factory,
|
||||
action="scores.generated",
|
||||
actor_type=_actor_type(trigger_source),
|
||||
actor_id=trigger_source,
|
||||
domain="wwi",
|
||||
service="otel-bi-backend",
|
||||
entity_type="supplier_scores",
|
||||
payload={"supplier_count": len(data), "top_n": top_n},
|
||||
)
|
||||
|
||||
|
||||
def persist_whatif_scenario(
|
||||
factory: sessionmaker[Session],
|
||||
result: dict,
|
||||
) -> None:
|
||||
trace_id, span_id = _current_span_context()
|
||||
try:
|
||||
with factory() as session:
|
||||
session.add(WWIWhatIfScenario(
|
||||
stock_item_key=result["stock_item_key"],
|
||||
stock_item_name=result["stock_item_name"],
|
||||
demand_multiplier=result["demand_multiplier"],
|
||||
current_stock=result["current_stock"],
|
||||
avg_daily_demand=result["adjusted_daily_demand"],
|
||||
projected_days_until_stockout=result.get("projected_days_until_stockout"),
|
||||
recommended_order_qty=float(result["recommended_order_qty"]),
|
||||
trace_id=trace_id,
|
||||
span_id=span_id,
|
||||
result=result,
|
||||
))
|
||||
session.commit()
|
||||
_persist_counter.add(1, {"entity": "whatif_scenario"})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOGGER.warning("Failed to persist WWI what-if scenario: %s", exc)
|
||||
append_audit(
|
||||
factory,
|
||||
action="scenario.submitted",
|
||||
actor_type="user",
|
||||
domain="wwi",
|
||||
service="otel-bi-backend",
|
||||
entity_type="whatif_scenario",
|
||||
payload={
|
||||
"stock_item_key": result["stock_item_key"],
|
||||
"demand_multiplier": result["demand_multiplier"],
|
||||
"projected_days_until_stockout": result.get("projected_days_until_stockout"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Business events — generated from reorder data in Python (PostgreSQL writes)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate_stock_events(
|
||||
factory: sessionmaker[Session],
|
||||
recommendations: list[dict],
|
||||
) -> None:
|
||||
"""Write LOW_STOCK events for HIGH-urgency items, deduplicating within 24h."""
|
||||
trace_id, span_id = _current_span_context()
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
try:
|
||||
with factory() as session:
|
||||
for item in recommendations:
|
||||
if item.get("urgency") != "HIGH":
|
||||
continue
|
||||
entity_key = str(item["stock_item_key"])
|
||||
existing = (
|
||||
session.query(WWIBusinessEvent)
|
||||
.filter(
|
||||
WWIBusinessEvent.event_type == "LOW_STOCK",
|
||||
WWIBusinessEvent.entity_key == entity_key,
|
||||
WWIBusinessEvent.occurred_at >= cutoff,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
continue
|
||||
days_str = (
|
||||
f"{item['days_until_stockout']:.1f} days"
|
||||
if item.get("days_until_stockout") is not None
|
||||
else "immediately"
|
||||
)
|
||||
session.add(WWIBusinessEvent(
|
||||
event_type="LOW_STOCK",
|
||||
severity="HIGH",
|
||||
entity_key=entity_key,
|
||||
entity_name=item["stock_item_name"],
|
||||
message=(
|
||||
f"Stock for '{item['stock_item_name']}' will be exhausted in {days_str}. "
|
||||
f"Current stock: {item['current_stock']:.0f} units, "
|
||||
f"daily demand: {item['avg_daily_demand']:.1f} units."
|
||||
),
|
||||
trace_id=trace_id,
|
||||
span_id=span_id,
|
||||
details={
|
||||
"current_stock": item["current_stock"],
|
||||
"avg_daily_demand": item["avg_daily_demand"],
|
||||
"recommended_reorder_qty": item["recommended_reorder_qty"],
|
||||
},
|
||||
))
|
||||
_event_counter.add(1, {"event_type": "LOW_STOCK"})
|
||||
session.commit()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOGGER.warning("Failed to persist WWI business events: %s", exc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read functions — query PostgreSQL for stored results
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_business_events(factory: sessionmaker[Session], limit: int = 100) -> list[dict]:
|
||||
with tracer.start_as_current_span("wwi.analytics.business_events"):
|
||||
with factory() as session:
|
||||
rows = (
|
||||
session.query(WWIBusinessEvent)
|
||||
.order_by(WWIBusinessEvent.occurred_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"occurred_at": r.occurred_at.isoformat(),
|
||||
"event_type": r.event_type,
|
||||
"severity": r.severity,
|
||||
"entity_key": r.entity_key,
|
||||
"entity_name": r.entity_name,
|
||||
"message": r.message,
|
||||
"trace_id": r.trace_id,
|
||||
"details": r.details,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def list_reorder_recommendations(factory: sessionmaker[Session], limit: int = 50) -> list[dict]:
|
||||
with factory() as session:
|
||||
rows = (
|
||||
session.query(WWIReorderRecommendation)
|
||||
.order_by(WWIReorderRecommendation.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"created_at": r.created_at.isoformat(),
|
||||
"item_count": r.item_count,
|
||||
"trigger_source": r.trigger_source,
|
||||
"trace_id": r.trace_id,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def list_supplier_scores(factory: sessionmaker[Session], limit: int = 50) -> list[dict]:
|
||||
with factory() as session:
|
||||
rows = (
|
||||
session.query(WWISupplierScore)
|
||||
.order_by(WWISupplierScore.computed_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"computed_at": r.computed_at.isoformat(),
|
||||
"supplier_count": r.supplier_count,
|
||||
"top_n": r.top_n,
|
||||
"trigger_source": r.trigger_source,
|
||||
"trace_id": r.trace_id,
|
||||
"payload": r.payload,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def list_whatif_scenarios(factory: sessionmaker[Session], limit: int = 50) -> list[dict]:
|
||||
with factory() as session:
|
||||
rows = (
|
||||
session.query(WWIWhatIfScenario)
|
||||
.order_by(WWIWhatIfScenario.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"created_at": r.created_at.isoformat(),
|
||||
"stock_item_key": r.stock_item_key,
|
||||
"stock_item_name": r.stock_item_name,
|
||||
"demand_multiplier": r.demand_multiplier,
|
||||
"projected_days_until_stockout": r.projected_days_until_stockout,
|
||||
"recommended_order_qty": r.recommended_order_qty,
|
||||
"result": r.result,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
80
backend/app/domain/wwi/models.py
Normal file
80
backend/app/domain/wwi/models.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import JSON, DateTime, Float, Integer, String, Text
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class WWIBase(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class WWIReorderRecommendation(WWIBase):
|
||||
"""Persisted WWI stock reorder recommendation runs."""
|
||||
|
||||
__tablename__ = "wwi_reorder_recommendations"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, index=True)
|
||||
item_count: Mapped[int] = mapped_column(Integer)
|
||||
trigger_source: Mapped[str] = mapped_column(String(64), index=True)
|
||||
trace_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
span_id: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
payload: Mapped[list[dict]] = mapped_column(JSON, default=list)
|
||||
|
||||
|
||||
class WWISupplierScore(WWIBase):
|
||||
"""Persisted WWI supplier reliability scoring runs."""
|
||||
|
||||
__tablename__ = "wwi_supplier_scores"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
computed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, index=True)
|
||||
supplier_count: Mapped[int] = mapped_column(Integer)
|
||||
top_n: Mapped[int] = mapped_column(Integer)
|
||||
trigger_source: Mapped[str] = mapped_column(String(64), index=True)
|
||||
trace_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
span_id: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
payload: Mapped[list[dict]] = mapped_column(JSON, default=list)
|
||||
|
||||
|
||||
class WWIWhatIfScenario(WWIBase):
|
||||
"""User-submitted what-if simulation results."""
|
||||
|
||||
__tablename__ = "wwi_whatif_scenarios"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, index=True)
|
||||
stock_item_key: Mapped[int] = mapped_column(Integer, index=True)
|
||||
stock_item_name: Mapped[str] = mapped_column(String(200))
|
||||
demand_multiplier: Mapped[float] = mapped_column(Float)
|
||||
current_stock: Mapped[float] = mapped_column(Float)
|
||||
avg_daily_demand: Mapped[float] = mapped_column(Float)
|
||||
projected_days_until_stockout: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
recommended_order_qty: Mapped[float] = mapped_column(Float)
|
||||
trace_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
span_id: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
result: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
|
||||
|
||||
class WWIBusinessEvent(WWIBase):
|
||||
"""Automatically generated business alert events."""
|
||||
|
||||
__tablename__ = "wwi_business_events"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, index=True)
|
||||
event_type: Mapped[str] = mapped_column(String(50), index=True) # LOW_STOCK, ORDER_DROP, SUPPLIER_RISK
|
||||
severity: Mapped[str] = mapped_column(String(20), index=True) # HIGH, MEDIUM, LOW
|
||||
entity_key: Mapped[str] = mapped_column(String(100), index=True)
|
||||
entity_name: Mapped[str] = mapped_column(String(200))
|
||||
message: Mapped[str] = mapped_column(Text)
|
||||
trace_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
span_id: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
details: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
171
backend/app/domain/wwi/queries.py
Normal file
171
backend/app/domain/wwi/queries.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WideWorldImportersDW — read-only MSSQL queries
|
||||
#
|
||||
# Column names in this DW use spaces and require bracket notation.
|
||||
# Each list contains fallback variants tried in order.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Daily sales from Fact.Sale joined to Dimension.Date
|
||||
WWI_DAILY_SALES: list[str] = [
|
||||
"""
|
||||
SELECT
|
||||
d.[Date] AS sale_date,
|
||||
SUM(s.[Total Excluding Tax]) AS revenue,
|
||||
SUM(s.[Total Excluding Tax] - s.[Profit]) AS cost,
|
||||
SUM(CAST(s.[Quantity] AS FLOAT)) AS quantity,
|
||||
COUNT_BIG(*) AS orders
|
||||
FROM [Fact].[Sale] AS s
|
||||
INNER JOIN [Dimension].[Date] AS d
|
||||
ON d.[Date Key] = s.[Delivery Date Key]
|
||||
GROUP BY d.[Date]
|
||||
ORDER BY d.[Date];
|
||||
""",
|
||||
# Fallback: use Invoice Date Key if Delivery Date Key is missing
|
||||
"""
|
||||
SELECT
|
||||
d.[Date] AS sale_date,
|
||||
SUM(s.[Total Excluding Tax]) AS revenue,
|
||||
SUM(s.[Total Excluding Tax] - s.[Profit]) AS cost,
|
||||
SUM(CAST(s.[Quantity] AS FLOAT)) AS quantity,
|
||||
COUNT_BIG(*) AS orders
|
||||
FROM [Fact].[Sale] AS s
|
||||
INNER JOIN [Dimension].[Date] AS d
|
||||
ON d.[Date Key] = s.[Invoice Date Key]
|
||||
GROUP BY d.[Date]
|
||||
ORDER BY d.[Date];
|
||||
""",
|
||||
]
|
||||
|
||||
# Current stock levels per stock item (net movement quantity)
|
||||
WWI_STOCK_LEVELS: list[str] = [
|
||||
"""
|
||||
SELECT
|
||||
si.[Stock Item Key] AS stock_item_key,
|
||||
si.[Stock Item] AS stock_item_name,
|
||||
si.[Unit Price] AS unit_price,
|
||||
si.[Lead Time Days] AS lead_time_days,
|
||||
SUM(CAST(m.[Quantity] AS FLOAT)) AS current_stock
|
||||
FROM [Dimension].[Stock Item] AS si
|
||||
LEFT JOIN [Fact].[Movement] AS m
|
||||
ON m.[Stock Item Key] = si.[Stock Item Key]
|
||||
WHERE si.[Stock Item Key] <> 0
|
||||
GROUP BY
|
||||
si.[Stock Item Key],
|
||||
si.[Stock Item],
|
||||
si.[Unit Price],
|
||||
si.[Lead Time Days];
|
||||
""",
|
||||
# Fallback: without movement (returns 0 stock)
|
||||
"""
|
||||
SELECT
|
||||
si.[Stock Item Key] AS stock_item_key,
|
||||
si.[Stock Item] AS stock_item_name,
|
||||
si.[Unit Price] AS unit_price,
|
||||
si.[Lead Time Days] AS lead_time_days,
|
||||
CAST(0 AS FLOAT) AS current_stock
|
||||
FROM [Dimension].[Stock Item] AS si
|
||||
WHERE si.[Stock Item Key] <> 0;
|
||||
""",
|
||||
]
|
||||
|
||||
# 90-day demand velocity per stock item from Fact.Sale
|
||||
WWI_DEMAND_VELOCITY: list[str] = [
|
||||
"""
|
||||
SELECT
|
||||
s.[Stock Item Key] AS stock_item_key,
|
||||
SUM(CAST(s.[Quantity] AS FLOAT)) AS qty_sold_90d,
|
||||
COUNT_BIG(DISTINCT s.[WWI Invoice ID]) AS invoice_count_90d
|
||||
FROM [Fact].[Sale] AS s
|
||||
INNER JOIN [Dimension].[Date] AS d
|
||||
ON d.[Date Key] = s.[Delivery Date Key]
|
||||
WHERE d.[Date] >= DATEADD(day, -90, GETDATE())
|
||||
AND s.[Stock Item Key] <> 0
|
||||
GROUP BY s.[Stock Item Key];
|
||||
""",
|
||||
"""
|
||||
SELECT
|
||||
s.[Stock Item Key] AS stock_item_key,
|
||||
SUM(CAST(s.[Quantity] AS FLOAT)) AS qty_sold_90d,
|
||||
COUNT_BIG(DISTINCT s.[WWI Invoice ID]) AS invoice_count_90d
|
||||
FROM [Fact].[Sale] AS s
|
||||
INNER JOIN [Dimension].[Date] AS d
|
||||
ON d.[Date Key] = s.[Invoice Date Key]
|
||||
WHERE d.[Date] >= DATEADD(day, -90, GETDATE())
|
||||
AND s.[Stock Item Key] <> 0
|
||||
GROUP BY s.[Stock Item Key];
|
||||
""",
|
||||
]
|
||||
|
||||
# Supplier reliability data from Fact.Purchase
|
||||
WWI_SUPPLIER_PERFORMANCE: list[str] = [
|
||||
"""
|
||||
SELECT
|
||||
sup.[Supplier Key] AS supplier_key,
|
||||
sup.[Supplier] AS supplier_name,
|
||||
sup.[Category] AS category,
|
||||
COUNT_BIG(*) AS total_orders,
|
||||
SUM(CAST(p.[Ordered Outers] AS FLOAT)) AS total_ordered_outers,
|
||||
SUM(CAST(p.[Received Outers] AS FLOAT)) AS total_received_outers,
|
||||
SUM(CASE WHEN p.[Is Order Finalized] = 1 THEN 1 ELSE 0 END) AS finalized_orders
|
||||
FROM [Dimension].[Supplier] AS sup
|
||||
INNER JOIN [Fact].[Purchase] AS p
|
||||
ON p.[Supplier Key] = sup.[Supplier Key]
|
||||
WHERE sup.[Supplier Key] <> 0
|
||||
GROUP BY
|
||||
sup.[Supplier Key],
|
||||
sup.[Supplier],
|
||||
sup.[Category]
|
||||
ORDER BY total_orders DESC;
|
||||
""",
|
||||
# Fallback: without Is Order Finalized
|
||||
"""
|
||||
SELECT
|
||||
sup.[Supplier Key] AS supplier_key,
|
||||
sup.[Supplier] AS supplier_name,
|
||||
sup.[Category] AS category,
|
||||
COUNT_BIG(*) AS total_orders,
|
||||
SUM(CAST(p.[Ordered Outers] AS FLOAT)) AS total_ordered_outers,
|
||||
SUM(CAST(p.[Received Outers] AS FLOAT)) AS total_received_outers,
|
||||
COUNT_BIG(*) AS finalized_orders
|
||||
FROM [Dimension].[Supplier] AS sup
|
||||
INNER JOIN [Fact].[Purchase] AS p
|
||||
ON p.[Supplier Key] = sup.[Supplier Key]
|
||||
WHERE sup.[Supplier Key] <> 0
|
||||
GROUP BY
|
||||
sup.[Supplier Key],
|
||||
sup.[Supplier],
|
||||
sup.[Category]
|
||||
ORDER BY total_orders DESC;
|
||||
""",
|
||||
]
|
||||
|
||||
# Single stock item detail for what-if scenario computation
|
||||
WWI_STOCK_ITEM_DETAIL = """
|
||||
SELECT
|
||||
si.[Stock Item Key] AS stock_item_key,
|
||||
si.[Stock Item] AS stock_item_name,
|
||||
si.[Unit Price] AS unit_price,
|
||||
si.[Lead Time Days] AS lead_time_days,
|
||||
COALESCE(SUM(CAST(m.[Quantity] AS FLOAT)), 0) AS current_stock
|
||||
FROM [Dimension].[Stock Item] AS si
|
||||
LEFT JOIN [Fact].[Movement] AS m
|
||||
ON m.[Stock Item Key] = si.[Stock Item Key]
|
||||
WHERE si.[Stock Item Key] = :stock_item_key
|
||||
GROUP BY
|
||||
si.[Stock Item Key],
|
||||
si.[Stock Item],
|
||||
si.[Unit Price],
|
||||
si.[Lead Time Days];
|
||||
"""
|
||||
|
||||
WWI_STOCK_ITEM_DEMAND = """
|
||||
SELECT
|
||||
SUM(CAST(s.[Quantity] AS FLOAT)) / NULLIF(90.0, 0) AS avg_daily_demand
|
||||
FROM [Fact].[Sale] AS s
|
||||
INNER JOIN [Dimension].[Date] AS d
|
||||
ON d.[Date Key] = s.[Delivery Date Key]
|
||||
WHERE s.[Stock Item Key] = :stock_item_key
|
||||
AND d.[Date] >= DATEADD(day, -90, GETDATE());
|
||||
"""
|
||||
Reference in New Issue
Block a user