feat: Enhance Pedimentos creation and validation logic in frontend and backend
This commit is contained in:
@@ -2,7 +2,7 @@ from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -113,7 +113,12 @@ class TenantCRUDRoutes(
|
||||
if self.enable_list:
|
||||
if self.enable_filters:
|
||||
|
||||
@self.router.get("/", response_model=Dict[str, Any])
|
||||
@self.router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary=f"List {self.resource_name}s",
|
||||
description=f"Get paginated list of {self.resource_name}s with optional filters",
|
||||
)
|
||||
async def list_resources(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
@@ -127,7 +132,6 @@ class TenantCRUDRoutes(
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
f"""List all {self.resource_name}s with pagination"""
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user
|
||||
)
|
||||
@@ -152,7 +156,12 @@ class TenantCRUDRoutes(
|
||||
|
||||
else:
|
||||
|
||||
@self.router.get("/", response_model=Dict[str, Any])
|
||||
@self.router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary=f"List {self.resource_name}s",
|
||||
description=f"Get paginated list of {self.resource_name}s",
|
||||
)
|
||||
async def list_resources(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
@@ -165,7 +174,6 @@ class TenantCRUDRoutes(
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
f"""List all {self.resource_name}s with pagination"""
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user
|
||||
)
|
||||
@@ -190,14 +198,19 @@ class TenantCRUDRoutes(
|
||||
# For child resources: GET / (parent_id comes from path)
|
||||
if self.parent_id_name:
|
||||
# Child resource - single GET without ID in path
|
||||
@self.router.get("/", response_model=self.response_schema)
|
||||
@self.router.get(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
summary=f"Get {self.resource_name}",
|
||||
description=f"Get {self.resource_name} by {self.parent_id_name}",
|
||||
)
|
||||
async def get_resource(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
**path_params,
|
||||
):
|
||||
f"""Get {self.resource_name} by {self.parent_id_name}"""
|
||||
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
@@ -223,15 +236,19 @@ class TenantCRUDRoutes(
|
||||
else:
|
||||
# Parent resource - GET by ID in path
|
||||
@self.router.get(
|
||||
f"/{{{self.id_name}}}", response_model=self.response_schema
|
||||
f"/{{{self.id_name}}}",
|
||||
response_model=self.response_schema,
|
||||
summary=f"Get {self.resource_name} by ID",
|
||||
description=f"Get a specific {self.resource_name} by {self.id_name}",
|
||||
)
|
||||
async def get_resource_by_id(
|
||||
resource_id: Union[int, str] = Path(..., alias=self.id_name),
|
||||
resource_id: Union[int, str] = Path(
|
||||
..., alias=self.id_name, description=f"{self.resource_name} ID"
|
||||
),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
f"""Get {self.resource_name} by ID"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
resource = self.service.get_by_id(
|
||||
@@ -245,49 +262,80 @@ class TenantCRUDRoutes(
|
||||
return resource
|
||||
|
||||
# POST route
|
||||
@self.router.post("/", response_model=self.response_schema, status_code=201)
|
||||
async def create_resource(
|
||||
data: CreateSchemaType,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
**path_params,
|
||||
):
|
||||
f"""Create {self.resource_name}"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Validate parent ID match if enabled and parent_id_name exists
|
||||
if self.validate_parent_match and self.parent_id_name:
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
data_parent_id = getattr(data, self.parent_id_name, None)
|
||||
if data_parent_id is not None and data_parent_id != parent_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"{self.parent_id_name.replace('_', ' ').title()} mismatch",
|
||||
)
|
||||
|
||||
resource = self.service.create(db, data, tenant_id, company_id)
|
||||
return resource
|
||||
if self.parent_id_name:
|
||||
# Child resource - needs parent_id from path
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
create_schema = self.create_schema
|
||||
|
||||
@self.router.post(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
status_code=201,
|
||||
summary=f"Create {self.resource_name}",
|
||||
description=f"Create a new {self.resource_name}",
|
||||
)
|
||||
async def create_child_resource(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
data: create_schema = Body(...), # type: ignore
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# For child resources, parent_id validation would go here
|
||||
resource = self.service.create(db, data, tenant_id, company_id)
|
||||
return resource
|
||||
else:
|
||||
# Parent resource - no parent_id needed
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
create_schema = self.create_schema
|
||||
|
||||
@self.router.post(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
status_code=201,
|
||||
summary=f"Create {self.resource_name}",
|
||||
description=f"Create a new {self.resource_name}",
|
||||
)
|
||||
async def create_parent_resource(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
data: create_schema = Body(...), # type: ignore
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
resource = self.service.create(db, data, tenant_id, company_id)
|
||||
return resource
|
||||
|
||||
# PUT route
|
||||
# For parent resources: PUT /{id}
|
||||
# For child resources: PUT / (parent_id comes from path)
|
||||
if self.parent_id_name:
|
||||
# Child resource
|
||||
@self.router.put("/", response_model=self.response_schema)
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
update_schema = self.update_schema
|
||||
|
||||
@self.router.put(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
summary=f"Update {self.resource_name}",
|
||||
description=f"Update an existing {self.resource_name}",
|
||||
)
|
||||
async def update_resource(
|
||||
data: UpdateSchemaType,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
data: update_schema = Body(...), # type: ignore
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
**path_params,
|
||||
):
|
||||
f"""Update {self.resource_name}"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
resource = self.service.update(
|
||||
db, parent_id, tenant_id, company_id, data
|
||||
db, parent_id, tenant_id, data, company_id
|
||||
)
|
||||
|
||||
if not resource:
|
||||
@@ -298,13 +346,22 @@ class TenantCRUDRoutes(
|
||||
|
||||
else:
|
||||
# Parent resource
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
update_schema = self.update_schema
|
||||
|
||||
@self.router.put(
|
||||
f"/{{{self.id_name}}}", response_model=self.response_schema
|
||||
f"/{{{self.id_name}}}",
|
||||
response_model=self.response_schema,
|
||||
summary=f"Update {self.resource_name}",
|
||||
description=f"Update an existing {self.resource_name} by {self.id_name}",
|
||||
)
|
||||
async def update_resource_by_id(
|
||||
data: UpdateSchemaType,
|
||||
resource_id: Union[int, str] = Path(..., alias=self.id_name),
|
||||
resource_id: Union[int, str] = Path(
|
||||
..., alias=self.id_name, description=f"{self.resource_name} ID"
|
||||
),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
data: update_schema = Body(...), # type: ignore
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
@@ -312,7 +369,7 @@ class TenantCRUDRoutes(
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
resource = self.service.update(
|
||||
db, resource_id, tenant_id, company_id, data
|
||||
db, resource_id, tenant_id, data, company_id
|
||||
)
|
||||
|
||||
if not resource:
|
||||
@@ -326,14 +383,18 @@ class TenantCRUDRoutes(
|
||||
# For child resources: DELETE / (parent_id comes from path)
|
||||
if self.parent_id_name:
|
||||
# Child resource
|
||||
@self.router.delete("/", status_code=204)
|
||||
@self.router.delete(
|
||||
"/",
|
||||
status_code=204,
|
||||
summary=f"Delete {self.resource_name}",
|
||||
description=f"Delete an existing {self.resource_name}",
|
||||
)
|
||||
async def delete_resource(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
**path_params,
|
||||
):
|
||||
f"""Delete {self.resource_name}"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
@@ -347,14 +408,20 @@ class TenantCRUDRoutes(
|
||||
|
||||
else:
|
||||
# Parent resource
|
||||
@self.router.delete(f"/{{{self.id_name}}}", status_code=204)
|
||||
@self.router.delete(
|
||||
f"/{{{self.id_name}}}",
|
||||
status_code=204,
|
||||
summary=f"Delete {self.resource_name}",
|
||||
description=f"Delete an existing {self.resource_name} by {self.id_name}",
|
||||
)
|
||||
async def delete_resource_by_id(
|
||||
resource_id: Union[int, str] = Path(..., alias=self.id_name),
|
||||
resource_id: Union[int, str] = Path(
|
||||
..., alias=self.id_name, description=f"{self.resource_name} ID"
|
||||
),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
f"""Delete {self.resource_name}"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
success = self.service.delete(db, resource_id, tenant_id, company_id)
|
||||
|
||||
@@ -39,7 +39,16 @@ class PedimentosBase(BaseModel):
|
||||
class PedimentosCreate(PedimentosBase):
|
||||
"""Schema for creating a new Pedimento"""
|
||||
|
||||
pass
|
||||
# Override to make required fields non-optional
|
||||
year: str = Field(..., max_length=2, description="Year")
|
||||
customs_office: str = Field(..., max_length=2, description="Customs office")
|
||||
license: str = Field(..., max_length=4, description="License")
|
||||
pedimento_number: str = Field(..., max_length=7, description="Pedimento number")
|
||||
client_id: int = Field(..., description="Client ID")
|
||||
operation_type: int = Field(..., description="Operation type")
|
||||
pedimento_type: int = Field(..., description="Pedimento type")
|
||||
regime: str = Field(..., max_length=3, description="Regime")
|
||||
status: str = Field(..., max_length=30, description="Status")
|
||||
|
||||
|
||||
class PedimentosUpdate(BaseModel):
|
||||
|
||||
@@ -14,7 +14,7 @@ router = TenantCRUDRoutes(
|
||||
update_schema=PedimentosUpdate,
|
||||
response_schema=PedimentosResponse,
|
||||
prefix="", # No prefix here, will be added in main router
|
||||
tags=[],
|
||||
tags=["a76 / pedimentos"], # Tag for Swagger documentation
|
||||
resource_name="Pedimento",
|
||||
id_name="pedimento_id",
|
||||
enable_list=True, # Enable GET / with pagination
|
||||
|
||||
@@ -55,7 +55,7 @@ class PedimentosService:
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, pedimento_id: int, tenant_id: int
|
||||
db: Session, pedimento_id: int, tenant_id: int, company_id: int = None
|
||||
) -> Optional[Pedimentos]:
|
||||
"""
|
||||
Get a pedimento by ID
|
||||
@@ -64,19 +64,23 @@ class PedimentosService:
|
||||
db: Database session
|
||||
pedimento_id: Pedimento ID
|
||||
tenant_id: Tenant ID
|
||||
company_id: Company ID (optional for backwards compatibility)
|
||||
|
||||
Returns:
|
||||
Pedimento or None if not found
|
||||
"""
|
||||
return (
|
||||
db.query(Pedimentos)
|
||||
.filter(Pedimentos.id == pedimento_id, Pedimentos.tenant_id == tenant_id)
|
||||
.first()
|
||||
query = db.query(Pedimentos).filter(
|
||||
Pedimentos.id == pedimento_id, Pedimentos.tenant_id == tenant_id
|
||||
)
|
||||
|
||||
if company_id is not None:
|
||||
query = query.filter(Pedimentos.company_id == company_id)
|
||||
|
||||
return query.first()
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session, pedimento_data: PedimentosCreate, tenant_id: int
|
||||
db: Session, pedimento_data: PedimentosCreate, tenant_id: int, company_id: int
|
||||
) -> Pedimentos:
|
||||
"""
|
||||
Create a new pedimento
|
||||
@@ -84,12 +88,15 @@ class PedimentosService:
|
||||
Args:
|
||||
db: Database session
|
||||
pedimento_data: Pedimento creation data
|
||||
tenant_id: Tenant ID
|
||||
company_id: Company ID
|
||||
|
||||
Returns:
|
||||
Created pedimento
|
||||
"""
|
||||
pedimento = Pedimentos(**pedimento_data.model_dump())
|
||||
pedimento.tenant_id = 1
|
||||
pedimento.tenant_id = tenant_id
|
||||
pedimento.company_id = company_id
|
||||
|
||||
db.add(pedimento)
|
||||
db.commit()
|
||||
@@ -98,7 +105,7 @@ class PedimentosService:
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session, pedimento_id: int, tenant_id: int, pedimento_data: PedimentosUpdate
|
||||
db: Session, pedimento_id: int, tenant_id: int, pedimento_data: PedimentosUpdate, company_id: int = None
|
||||
) -> Optional[Pedimentos]:
|
||||
"""
|
||||
Update a pedimento
|
||||
@@ -108,11 +115,12 @@ class PedimentosService:
|
||||
pedimento_id: Pedimento ID
|
||||
tenant_id: Tenant ID
|
||||
pedimento_data: Updated data
|
||||
company_id: Company ID (optional for backwards compatibility)
|
||||
|
||||
Returns:
|
||||
Updated pedimento or None if not found
|
||||
"""
|
||||
pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id)
|
||||
pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id, company_id)
|
||||
if not pedimento:
|
||||
return None
|
||||
|
||||
@@ -125,7 +133,7 @@ class PedimentosService:
|
||||
return pedimento
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool:
|
||||
def delete(db: Session, pedimento_id: int, tenant_id: int, company_id: int = None) -> bool:
|
||||
"""
|
||||
Delete a pedimento
|
||||
|
||||
@@ -133,14 +141,17 @@ class PedimentosService:
|
||||
db: Database session
|
||||
pedimento_id: Pedimento ID
|
||||
tenant_id: Tenant ID
|
||||
company_id: Company ID (optional for backwards compatibility)
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id)
|
||||
pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id, company_id)
|
||||
if not pedimento:
|
||||
return False
|
||||
|
||||
db.delete(pedimento)
|
||||
db.commit()
|
||||
return True
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@@ -197,6 +197,29 @@ async function fetchApi<T = any>(
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
// Manejo especial para errores 422 (validation error)
|
||||
if (response.status === 422 && data.detail) {
|
||||
let errorMessage = 'Error de validación: ';
|
||||
|
||||
// FastAPI devuelve errores de validación en data.detail como array
|
||||
if (Array.isArray(data.detail)) {
|
||||
const errors = data.detail.map((err: any) => {
|
||||
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
|
||||
return `${field}: ${err.msg}`;
|
||||
}).join(', ');
|
||||
errorMessage += errors;
|
||||
} else if (typeof data.detail === 'string') {
|
||||
errorMessage = data.detail;
|
||||
} else {
|
||||
errorMessage += JSON.stringify(data.detail);
|
||||
}
|
||||
|
||||
return {
|
||||
error: errorMessage,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: data.detail || 'Error en la petición',
|
||||
status: response.status
|
||||
|
||||
@@ -74,27 +74,65 @@
|
||||
|
||||
// 1. Crear o actualizar datos generales
|
||||
if (generalFormData) {
|
||||
const payload = {
|
||||
year: generalFormData.year || null,
|
||||
customs_office: generalFormData.customs_office || null,
|
||||
license: generalFormData.license || null,
|
||||
pedimento_number: generalFormData.pedimento_number || null,
|
||||
client_id: generalFormData.client_id,
|
||||
operation_type: generalFormData.operation_type,
|
||||
pedimento_type: generalFormData.pedimento_type,
|
||||
pedimento_code: generalFormData.pedimento_code || null,
|
||||
regime: generalFormData.regime || null,
|
||||
status: generalFormData.status || null,
|
||||
usd_value: generalFormData.usd_value,
|
||||
paid_price: generalFormData.paid_price,
|
||||
gross_weight: generalFormData.gross_weight,
|
||||
exchange_rate: generalFormData.exchange_rate
|
||||
// Validar campos requeridos para creación
|
||||
if (data.isCreate) {
|
||||
const requiredFields = {
|
||||
year: 'Año',
|
||||
customs_office: 'Aduana',
|
||||
license: 'Patente',
|
||||
pedimento_number: 'Número de Pedimento',
|
||||
client_id: 'ID del Cliente',
|
||||
operation_type: 'Tipo de Operación',
|
||||
pedimento_type: 'Tipo de Pedimento',
|
||||
regime: 'Régimen',
|
||||
status: 'Estado'
|
||||
};
|
||||
|
||||
const missingFields: string[] = [];
|
||||
for (const [field, label] of Object.entries(requiredFields)) {
|
||||
const value = (generalFormData as any)[field];
|
||||
if (value === null || value === undefined || value === '') {
|
||||
missingFields.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
year: generalFormData.year || undefined,
|
||||
customs_office: generalFormData.customs_office || undefined,
|
||||
license: generalFormData.license || undefined,
|
||||
pedimento_number: generalFormData.pedimento_number || undefined,
|
||||
client_id: generalFormData.client_id || undefined,
|
||||
operation_type: generalFormData.operation_type || undefined,
|
||||
pedimento_type: generalFormData.pedimento_type || undefined,
|
||||
pedimento_code: generalFormData.pedimento_code || undefined,
|
||||
regime: generalFormData.regime || undefined,
|
||||
status: generalFormData.status || undefined,
|
||||
usd_value: generalFormData.usd_value || undefined,
|
||||
paid_price: generalFormData.paid_price || undefined,
|
||||
gross_weight: generalFormData.gross_weight || undefined,
|
||||
exchange_rate: generalFormData.exchange_rate || undefined
|
||||
};
|
||||
|
||||
// Eliminar campos undefined para no enviarlos
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (payload[key] === undefined) {
|
||||
delete payload[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (data.isCreate) {
|
||||
// Crear nuevo pedimento
|
||||
const response = await pedimentosApi.create(payload);
|
||||
if (response.error) throw new Error(response.error);
|
||||
if (response.error) {
|
||||
// Intentar extraer mensaje de error más específico
|
||||
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear el pedimento';
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
if (!response.data?.id) throw new Error('No se recibió el ID del pedimento creado');
|
||||
pedimentoId = response.data.id;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user