300 lines
10 KiB
Python
300 lines
10 KiB
Python
"""
|
|
API Endpoints for Items management
|
|
Handles CRUD operations for Item with one-to-many relationships to LineItems
|
|
"""
|
|
|
|
import datetime
|
|
from typing import Dict, Any, List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from core.database import get_core_db
|
|
from core.security import get_current_user, validate_access_to_resource
|
|
|
|
from .schemas import (
|
|
LineItemCreate,
|
|
LineItemUpdate,
|
|
LineItemResponse,
|
|
LineItemListResponse,
|
|
)
|
|
from .service import ItemService
|
|
|
|
router = APIRouter(prefix="/items", tags=["Items"])
|
|
|
|
|
|
# ITEM CRUD ENDPOINTS
|
|
|
|
@router.post("/", response_model=LineItemResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_item(
|
|
item_data: LineItemCreate,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Create a new item with multiple line items and their nested data
|
|
|
|
The item follows a one-to-many relationship structure:
|
|
- Each LineItem has one LineFinancial
|
|
- Each LineItem has one LineQuantity
|
|
- Each LineItem has one LineCustoms
|
|
- Each LineItem has one LineDescription
|
|
- Each LineItem has one LineReference
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
service = ItemService()
|
|
return service.create(db, item_data, tenant_id, company_id)
|
|
|
|
|
|
@router.get("/{item_id}", response_model=LineItemResponse)
|
|
async def get_item(
|
|
item_id: int = Path(..., description="Item ID"),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Get a specific item by ID with all nested data
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
service = ItemService()
|
|
item = service.get_by_id(db, item_id, tenant_id, company_id)
|
|
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
return item
|
|
|
|
|
|
@router.get("/", response_model=LineItemListResponse)
|
|
async def list_items(
|
|
company_id: int = Query(..., description="Company ID"),
|
|
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
|
limit: int = Query(100, ge=1, le=1000,
|
|
description="Maximum records to return"),
|
|
invoice_id: Optional[int] = Query(
|
|
None, description="Filter by invoice ID"),
|
|
item_type: Optional[str] = Query(None, description="Filter by item type"),
|
|
system_origin: Optional[str] = Query(
|
|
None, description="Filter by system origin (SCAF/SCAII)"),
|
|
search: Optional[str] = Query(
|
|
None, description="Search term for invoice number, reference, order, or guide"),
|
|
sort_by: Optional[str] = Query(None, description="Column to sort by"),
|
|
sort_order: Optional[str] = Query("asc", pattern="^(asc|desc)$", description="Sort order (asc or desc)"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
List items with optional filtering and pagination
|
|
|
|
Filters:
|
|
- invoice_id: Filter by specific invoice
|
|
- item_type: Filter by item type (IMPORT_TEMP, IMPORT_DEF, EXPORT, REPAIR)
|
|
- system_origin: Filter by system (SCAF, SCAII)
|
|
- search: Search across multiple fields
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
service = ItemService()
|
|
|
|
filters = {
|
|
"invoice_id": invoice_id,
|
|
"item_type": item_type,
|
|
"system_origin": system_origin,
|
|
"search": search,
|
|
}
|
|
|
|
items, total = service.get_all(
|
|
db, tenant_id, company_id, skip, limit, filters, sort_by=sort_by, sort_order=sort_order
|
|
)
|
|
|
|
return LineItemListResponse(
|
|
total=total,
|
|
items=items,
|
|
skip=skip,
|
|
limit=limit
|
|
)
|
|
|
|
|
|
@router.put("/{item_id}", response_model=LineItemResponse)
|
|
async def update_item(
|
|
item_id: int = Path(..., description="Item ID"),
|
|
item_data: LineItemUpdate = ...,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Update an item and optionally its nested line data
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
# Verify the item exists and belongs to the tenant/company
|
|
service = ItemService()
|
|
existing_item = service.get_by_id(db, item_id, tenant_id, company_id)
|
|
if not existing_item:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
return service.update(db, item_id, item_data, tenant_id, company_id)
|
|
|
|
|
|
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_item(
|
|
item_id: int = Path(..., description="Item ID"),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Delete an item and all its related data (cascade delete)
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
service = ItemService()
|
|
# Verify the item exists and belongs to the tenant/company
|
|
existing_item = service.get_by_id(db, item_id, tenant_id, company_id)
|
|
if not existing_item:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
success = service.delete(db, item_id, tenant_id, company_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
return None
|
|
|
|
|
|
@router.delete("/{item_id}/series", status_code=status.HTTP_200_OK)
|
|
async def delete_item_series(
|
|
item_id: int = Path(..., description="Item ID"),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Delete all serial numbers for a specific item.
|
|
Equivalent to Clarion BORRAR_SERIES_EXPO.
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
service = ItemService()
|
|
count = service.delete_item_series(db, item_id, tenant_id, company_id)
|
|
|
|
return {"message": f"Successfully deleted {count} series", "count": count}
|
|
|
|
|
|
# ADDITIONAL ENDPOINTS FOR INVOICE
|
|
|
|
@router.get("/invoice/{invoice_id}/items/", response_model=LineItemListResponse)
|
|
async def list_items_by_invoice(
|
|
invoice_id: int = Path(..., description="Invoice ID"),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
|
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
|
|
sort_by: Optional[str] = Query(None, description="Column to sort by"),
|
|
sort_order: Optional[str] = Query("asc", pattern="^(asc|desc)$", description="Sort order (asc or desc)"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
List all items for a specific invoice
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
service = ItemService()
|
|
items, total = service.get_by_invoice(
|
|
db, invoice_id, tenant_id, company_id, skip, limit, sort_by=sort_by, sort_order=sort_order
|
|
)
|
|
|
|
return LineItemListResponse(
|
|
total=total,
|
|
items=items,
|
|
skip=skip,
|
|
limit=limit
|
|
)
|
|
@router.get("/invoice/{invoice_id}/items-with-balance", response_model=List[dict])
|
|
async def get_items_with_balance(
|
|
invoice_id: int = Path(..., description="Import Invoice ID"),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
as_of_date: Optional[datetime.date] = Query(
|
|
None,
|
|
description=(
|
|
"Cut-off date for balance calculation. Only consumptions on or "
|
|
"before this date are subtracted (CALCULA_SALDO_FECHA_EXPO logic)."
|
|
),
|
|
),
|
|
current_export_invoice_id: Optional[int] = Query(
|
|
None,
|
|
description="ID of the current export invoice being edited to subtract its pending quantities from balance."
|
|
),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Returns every line of the given import invoice with its available balance
|
|
from the a24.balance_movement ledger.
|
|
|
|
Each item in the response includes:
|
|
- id, line_number, part_number, class_code, unit_of_measure_code
|
|
- quantity : original imported quantity
|
|
- available_balance : net balance still available for export discharge
|
|
- has_balance : true when available_balance > 0
|
|
|
|
If current_export_invoice_id is provided, quantities already assigned to
|
|
this specified invoice will be subtracted from available_balance.
|
|
|
|
Use ``as_of_date`` to restrict consumption movements to a specific date
|
|
(pass the export invoice date so that future discharges are not counted).
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
service = ItemService()
|
|
return service.get_lines_with_balance(db, invoice_id, tenant_id, company_id, as_of_date, current_export_invoice_id)
|
|
|
|
|
|
# STATISTICS & UTILITIES
|
|
|
|
|
|
@router.get("/stats/summary")
|
|
async def get_items_summary(
|
|
company_id: int = Query(..., description="Company ID"),
|
|
invoice_id: Optional[int] = Query(
|
|
None, description="Filter by invoice ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Get summary statistics for items
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
filters = {"invoice_id": invoice_id} if invoice_id else None
|
|
items, total = ItemService.get_all(
|
|
db=db,
|
|
tenant_id=tenant_id,
|
|
company_id=company_id,
|
|
skip=0,
|
|
limit=10000, # Get all for stats
|
|
filters=filters
|
|
)
|
|
|
|
# Calculate stats
|
|
stats = {
|
|
"total_items": total,
|
|
"by_type": {},
|
|
"by_system": {},
|
|
}
|
|
|
|
for item in items:
|
|
# Count by type
|
|
if item.item_type:
|
|
stats["by_type"][item.item_type] = stats["by_type"].get(
|
|
item.item_type, 0) + 1
|
|
|
|
# Count by system
|
|
if item.system_origin:
|
|
stats["by_system"][item.system_origin] = stats["by_system"].get(
|
|
item.system_origin, 0) + 1
|
|
|
|
return stats
|