Files
plantillas-proyectos/backend/api/v1/modules/a76/items/routes.py

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