Files
CRM_AGENTES_CARGA/backend/api/v1/modules/crm/rates/routes.py
Ernesto Herrera ef7e69ed57 feat(crm): tarifario — editor de cargos adicionales + alta manual de rutas
- Backend: CRUD de cargos (rate_charges) por tarifario.
- Frontend: detalle del tarifario con alta manual de rutas (con editor de quiebres
  para aéreo/LCL) y sección de cargos adicionales (agregar/eliminar).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:46:25 -06:00

258 lines
8.3 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,
RateChargeCreate,
RateChargeResponse,
RateChargeUpdate,
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)
# ---------------- Cargos adicionales ----------------
@router.get("/{sheet_id}/charges", response_model=list[RateChargeResponse])
def list_charges(
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 service.list_charges(db, tenant_id, sheet_id)
@router.post("/{sheet_id}/charges", response_model=RateChargeResponse, status_code=status.HTTP_201_CREATED)
def create_charge(
sheet_id: int,
data: RateChargeCreate,
company_id: int = Query(...),
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_core_db),
):
tenant_id, _ = _ctx(current_user)
return service.create_charge(db, tenant_id, company_id, sheet_id, data)
@router.patch("/{sheet_id}/charges/{charge_id}", response_model=RateChargeResponse)
def update_charge(
sheet_id: int,
charge_id: int,
data: RateChargeUpdate,
company_id: int = Query(...),
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_core_db),
):
tenant_id, _ = _ctx(current_user)
return service.update_charge(db, tenant_id, sheet_id, charge_id, data)
@router.delete("/{sheet_id}/charges/{charge_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_charge(
sheet_id: int,
charge_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_charge(db, tenant_id, sheet_id, charge_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)