- Tablas rate_sheets/lanes/breaks/charges (esquema crm) + migración con down(). - Catálogos nuevos: modo_tarifario, unidad_tarifa, concepto_cargo. - CRUD de tarifarios y rutas; descarga de plantilla Excel por modo; import con vista previa y validación; alta directa desde Excel. - Motor de costeo /rate-quote: aéreo (peso facturable + quiebres + optimización), marítimo FCL (por contenedor), LCL (W/M) y terrestre; suma cargos adicionales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
205 lines
6.6 KiB
Python
205 lines
6.6 KiB
Python
"""Endpoints del módulo Tarifario."""
|
|
|
|
from datetime import date
|
|
|
|
from fastapi import APIRouter, Depends, File, Form, Query, Response, UploadFile, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from core.database import get_core_db
|
|
from core.security import get_current_user
|
|
|
|
from . import service
|
|
from .dto import (
|
|
CostRequest,
|
|
CostResult,
|
|
ImportPreview,
|
|
RateBreakDTO,
|
|
RateLaneCreate,
|
|
RateLaneResponse,
|
|
RateSheetCreate,
|
|
RateSheetResponse,
|
|
RateSheetUpdate,
|
|
)
|
|
|
|
router = APIRouter(prefix="/rate-sheets", tags=["Tarifario"])
|
|
|
|
|
|
def _ctx(current_user: dict):
|
|
return current_user["tenant_id"], current_user.get("sub") or current_user.get("id")
|
|
|
|
|
|
def _sheet_out(db: Session, tenant_id: int, sheet) -> RateSheetResponse:
|
|
out = RateSheetResponse.model_validate(sheet)
|
|
out.lane_count = service.lane_count(db, tenant_id, sheet.id)
|
|
return out
|
|
|
|
|
|
def _lane_out(db: Session, lane) -> RateLaneResponse:
|
|
out = RateLaneResponse.model_validate(lane)
|
|
out.breaks = [RateBreakDTO.model_validate(b) for b in service.breaks_of(db, lane.id)]
|
|
return out
|
|
|
|
|
|
# ---------------- Tarifarios ----------------
|
|
@router.get("", response_model=list[RateSheetResponse])
|
|
def list_sheets(
|
|
company_id: int = Query(...),
|
|
mode: str | None = Query(None),
|
|
supplier_id: int | None = Query(None),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, _ = _ctx(current_user)
|
|
sheets = service.list_sheets(db, tenant_id, company_id, mode=mode, supplier_id=supplier_id)
|
|
return [_sheet_out(db, tenant_id, s) for s in sheets]
|
|
|
|
|
|
@router.post("", response_model=RateSheetResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_sheet(
|
|
data: RateSheetCreate,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, user_id = _ctx(current_user)
|
|
sheet = service.create_sheet(db, tenant_id, company_id, data, user_id)
|
|
return _sheet_out(db, tenant_id, sheet)
|
|
|
|
|
|
@router.get("/template")
|
|
def download_template(
|
|
mode: str = Query(..., description="aereo | maritimo_fcl | maritimo_lcl | terrestre"),
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
content = service.build_template(mode)
|
|
return Response(
|
|
content=content,
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
headers={"Content-Disposition": f'attachment; filename="plantilla_tarifario_{mode}.xlsx"'},
|
|
)
|
|
|
|
|
|
@router.post("/import/preview", response_model=ImportPreview)
|
|
async def import_preview(
|
|
company_id: int = Query(...),
|
|
mode: str = Form(...),
|
|
file: UploadFile = File(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
content = await file.read()
|
|
return service.parse_excel(mode, content)
|
|
|
|
|
|
@router.post("/import", response_model=RateSheetResponse, status_code=status.HTTP_201_CREATED)
|
|
async def import_sheet(
|
|
company_id: int = Query(...),
|
|
mode: str = Form(...),
|
|
name: str = Form(...),
|
|
supplier_id: int | None = Form(None),
|
|
currency: str = Form("USD"),
|
|
valid_from: date | None = Form(None),
|
|
valid_to: date | None = Form(None),
|
|
default_origin: str | None = Form(None),
|
|
file: UploadFile = File(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, user_id = _ctx(current_user)
|
|
content = await file.read()
|
|
header = RateSheetCreate(
|
|
mode=mode, name=name, supplier_id=supplier_id, currency=currency,
|
|
valid_from=valid_from, valid_to=valid_to, default_origin=default_origin,
|
|
)
|
|
sheet = service.import_from_excel(db, tenant_id, company_id, mode, content, header, user_id)
|
|
return _sheet_out(db, tenant_id, sheet)
|
|
|
|
|
|
@router.get("/{sheet_id}", response_model=RateSheetResponse)
|
|
def get_sheet(
|
|
sheet_id: int,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, _ = _ctx(current_user)
|
|
return _sheet_out(db, tenant_id, service.get_sheet(db, tenant_id, company_id, sheet_id))
|
|
|
|
|
|
@router.patch("/{sheet_id}", response_model=RateSheetResponse)
|
|
def update_sheet(
|
|
sheet_id: int,
|
|
data: RateSheetUpdate,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, user_id = _ctx(current_user)
|
|
return _sheet_out(db, tenant_id, service.update_sheet(db, tenant_id, company_id, sheet_id, data, user_id))
|
|
|
|
|
|
@router.delete("/{sheet_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_sheet(
|
|
sheet_id: int,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, _ = _ctx(current_user)
|
|
service.delete_sheet(db, tenant_id, company_id, sheet_id)
|
|
|
|
|
|
# ---------------- Rutas (lanes) ----------------
|
|
@router.get("/{sheet_id}/lanes", response_model=list[RateLaneResponse])
|
|
def list_lanes(
|
|
sheet_id: int,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, _ = _ctx(current_user)
|
|
service.get_sheet(db, tenant_id, company_id, sheet_id)
|
|
return [_lane_out(db, lane) for lane in service.list_lanes(db, tenant_id, sheet_id)]
|
|
|
|
|
|
@router.post("/{sheet_id}/lanes", response_model=RateLaneResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_lane(
|
|
sheet_id: int,
|
|
data: RateLaneCreate,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, _ = _ctx(current_user)
|
|
lane = service.create_lane(db, tenant_id, company_id, sheet_id, data)
|
|
return _lane_out(db, lane)
|
|
|
|
|
|
@router.delete("/{sheet_id}/lanes/{lane_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_lane(
|
|
sheet_id: int,
|
|
lane_id: int,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
tenant_id, _ = _ctx(current_user)
|
|
service.delete_lane(db, tenant_id, sheet_id, lane_id)
|
|
|
|
|
|
# ---------------- Motor de costeo ----------------
|
|
cost_router = APIRouter(tags=["Tarifario"])
|
|
|
|
|
|
@cost_router.post("/rate-quote", response_model=CostResult)
|
|
def rate_quote(
|
|
req: CostRequest,
|
|
company_id: int = Query(...),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_core_db),
|
|
):
|
|
"""Calcula opciones de costo (por proveedor) para una ruta/carga."""
|
|
tenant_id, _ = _ctx(current_user)
|
|
options = service.quote_cost(db, tenant_id, company_id, req)
|
|
return CostResult(request=req, options=options)
|