Some checks failed
Clean up 28 backward compatibility instances identified in the codebase. The app is not live, so all shims are replaced with the target architecture: - Remove legacy Inventory.location column (use bin_location exclusively) - Remove dashboard _extract_metric_value helper (use flat metrics dict) - Remove legacy stat field duplicates (total_stores, total_imports, etc.) - Remove 13 re-export shims and class aliases across modules - Remove module-enabling JSON fallback (use PlatformModule junction table) - Remove menu_to_legacy_format() conversion (return dataclasses directly) - Remove title/description from MarketplaceProductBase schema - Clean billing convenience method docstrings - Clean test fixtures and backward-compat comments - Add PlatformModule seeding to init_production.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
318 lines
10 KiB
Python
318 lines
10 KiB
Python
# app/modules/inventory/services/inventory_metrics.py
|
|
"""
|
|
Metrics provider for the inventory module.
|
|
|
|
Provides metrics for:
|
|
- Inventory quantities
|
|
- Stock levels
|
|
- Low stock alerts
|
|
"""
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.modules.contracts.metrics import (
|
|
MetricsContext,
|
|
MetricValue,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class InventoryMetricsProvider:
|
|
"""
|
|
Metrics provider for inventory module.
|
|
|
|
Provides stock and inventory metrics for store and platform dashboards.
|
|
"""
|
|
|
|
@property
|
|
def metrics_category(self) -> str:
|
|
return "inventory"
|
|
|
|
def get_store_metrics(
|
|
self,
|
|
db: Session,
|
|
store_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get inventory metrics for a specific store.
|
|
|
|
Provides:
|
|
- Total inventory quantity
|
|
- Reserved quantity
|
|
- Available quantity
|
|
- Inventory locations
|
|
- Low stock items
|
|
"""
|
|
from app.modules.inventory.models import Inventory
|
|
|
|
try:
|
|
# Total inventory
|
|
total_quantity = (
|
|
db.query(func.sum(Inventory.quantity))
|
|
.filter(Inventory.store_id == store_id)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Reserved inventory
|
|
reserved_quantity = (
|
|
db.query(func.sum(Inventory.reserved_quantity))
|
|
.filter(Inventory.store_id == store_id)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Available inventory
|
|
available_quantity = int(total_quantity) - int(reserved_quantity)
|
|
|
|
# Inventory entries (SKU/location combinations)
|
|
inventory_entries = (
|
|
db.query(Inventory).filter(Inventory.store_id == store_id).count()
|
|
)
|
|
|
|
# Unique locations
|
|
unique_locations = (
|
|
db.query(func.count(func.distinct(Inventory.bin_location)))
|
|
.filter(Inventory.store_id == store_id)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Low stock items (quantity < 10 and > 0)
|
|
low_stock_items = (
|
|
db.query(Inventory)
|
|
.filter(
|
|
Inventory.store_id == store_id,
|
|
Inventory.quantity > 0,
|
|
Inventory.quantity < 10,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
# Out of stock items (quantity = 0)
|
|
out_of_stock_items = (
|
|
db.query(Inventory)
|
|
.filter(Inventory.store_id == store_id, Inventory.quantity == 0)
|
|
.count()
|
|
)
|
|
|
|
return [
|
|
MetricValue(
|
|
key="inventory.total_quantity",
|
|
value=int(total_quantity),
|
|
label="Total Stock",
|
|
category="inventory",
|
|
icon="package",
|
|
unit="items",
|
|
description="Total inventory quantity",
|
|
),
|
|
MetricValue(
|
|
key="inventory.reserved_quantity",
|
|
value=int(reserved_quantity),
|
|
label="Reserved",
|
|
category="inventory",
|
|
icon="lock",
|
|
unit="items",
|
|
description="Inventory reserved for orders",
|
|
),
|
|
MetricValue(
|
|
key="inventory.available_quantity",
|
|
value=available_quantity,
|
|
label="Available",
|
|
category="inventory",
|
|
icon="check",
|
|
unit="items",
|
|
description="Inventory available for sale",
|
|
),
|
|
MetricValue(
|
|
key="inventory.entries",
|
|
value=inventory_entries,
|
|
label="SKU/Location Entries",
|
|
category="inventory",
|
|
icon="list",
|
|
description="Total inventory entries",
|
|
),
|
|
MetricValue(
|
|
key="inventory.locations",
|
|
value=unique_locations,
|
|
label="Locations",
|
|
category="inventory",
|
|
icon="map-pin",
|
|
description="Unique storage locations",
|
|
),
|
|
MetricValue(
|
|
key="inventory.low_stock",
|
|
value=low_stock_items,
|
|
label="Low Stock",
|
|
category="inventory",
|
|
icon="alert-triangle",
|
|
description="Items with quantity < 10",
|
|
),
|
|
MetricValue(
|
|
key="inventory.out_of_stock",
|
|
value=out_of_stock_items,
|
|
label="Out of Stock",
|
|
category="inventory",
|
|
icon="x-circle",
|
|
description="Items with zero quantity",
|
|
),
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get inventory store metrics: {e}")
|
|
return []
|
|
|
|
def get_platform_metrics(
|
|
self,
|
|
db: Session,
|
|
platform_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get inventory metrics aggregated for a platform.
|
|
|
|
Aggregates stock data across all stores.
|
|
"""
|
|
from app.modules.inventory.models import Inventory
|
|
from app.modules.tenancy.models import StorePlatform
|
|
|
|
try:
|
|
# Get all store IDs for this platform using StorePlatform junction table
|
|
store_ids = (
|
|
db.query(StorePlatform.store_id)
|
|
.filter(
|
|
StorePlatform.platform_id == platform_id,
|
|
StorePlatform.is_active == True,
|
|
)
|
|
.subquery()
|
|
)
|
|
|
|
# Total inventory
|
|
total_quantity = (
|
|
db.query(func.sum(Inventory.quantity))
|
|
.filter(Inventory.store_id.in_(store_ids))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Reserved inventory
|
|
reserved_quantity = (
|
|
db.query(func.sum(Inventory.reserved_quantity))
|
|
.filter(Inventory.store_id.in_(store_ids))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Available inventory
|
|
available_quantity = int(total_quantity) - int(reserved_quantity)
|
|
|
|
# Total inventory entries
|
|
inventory_entries = (
|
|
db.query(Inventory).filter(Inventory.store_id.in_(store_ids)).count()
|
|
)
|
|
|
|
# Stores with inventory
|
|
stores_with_inventory = (
|
|
db.query(func.count(func.distinct(Inventory.store_id)))
|
|
.filter(Inventory.store_id.in_(store_ids))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Low stock items across platform
|
|
low_stock_items = (
|
|
db.query(Inventory)
|
|
.filter(
|
|
Inventory.store_id.in_(store_ids),
|
|
Inventory.quantity > 0,
|
|
Inventory.quantity < 10,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
# Out of stock items
|
|
out_of_stock_items = (
|
|
db.query(Inventory)
|
|
.filter(Inventory.store_id.in_(store_ids), Inventory.quantity == 0)
|
|
.count()
|
|
)
|
|
|
|
return [
|
|
MetricValue(
|
|
key="inventory.total_quantity",
|
|
value=int(total_quantity),
|
|
label="Total Stock",
|
|
category="inventory",
|
|
icon="package",
|
|
unit="items",
|
|
description="Total inventory across all stores",
|
|
),
|
|
MetricValue(
|
|
key="inventory.reserved_quantity",
|
|
value=int(reserved_quantity),
|
|
label="Reserved",
|
|
category="inventory",
|
|
icon="lock",
|
|
unit="items",
|
|
description="Inventory reserved for orders",
|
|
),
|
|
MetricValue(
|
|
key="inventory.available_quantity",
|
|
value=available_quantity,
|
|
label="Available",
|
|
category="inventory",
|
|
icon="check",
|
|
unit="items",
|
|
description="Inventory available for sale",
|
|
),
|
|
MetricValue(
|
|
key="inventory.entries",
|
|
value=inventory_entries,
|
|
label="Total Entries",
|
|
category="inventory",
|
|
icon="list",
|
|
description="Total inventory entries across stores",
|
|
),
|
|
MetricValue(
|
|
key="inventory.stores_with_inventory",
|
|
value=stores_with_inventory,
|
|
label="Stores with Stock",
|
|
category="inventory",
|
|
icon="store",
|
|
description="Stores managing inventory",
|
|
),
|
|
MetricValue(
|
|
key="inventory.low_stock",
|
|
value=low_stock_items,
|
|
label="Low Stock Items",
|
|
category="inventory",
|
|
icon="alert-triangle",
|
|
description="Items with quantity < 10",
|
|
),
|
|
MetricValue(
|
|
key="inventory.out_of_stock",
|
|
value=out_of_stock_items,
|
|
label="Out of Stock",
|
|
category="inventory",
|
|
icon="x-circle",
|
|
description="Items with zero quantity",
|
|
),
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get inventory platform metrics: {e}")
|
|
return []
|
|
|
|
|
|
# Singleton instance
|
|
inventory_metrics_provider = InventoryMetricsProvider()
|
|
|
|
__all__ = ["InventoryMetricsProvider", "inventory_metrics_provider"]
|