Refactor and enhance CRUD operations for Seal, Trailer, Transporter, Vehicle, and Customs Broker modules

- Updated SealService to support tenant and company filtering with pagination and enhanced CRUD methods.
- Refactored TrailerService to include tenant and company support, added filtering capabilities, and improved CRUD methods.
- Introduced TenantCRUDRoutes for Trailer and Transporter routes to streamline API endpoint creation and management.
- Enhanced TransporterService with tenant and company filtering, pagination, and improved CRUD operations.
- Added Customs Broker module with DTOs, models, services, and routes for managing customs broker data.
- Implemented CRUD operations for Customs Broker, including personnel and VU management.
- Improved data validation and descriptions in DTOs for better API documentation.
This commit is contained in:
2025-11-11 18:20:39 -06:00
parent b68c4316ff
commit 962f43fe62
39 changed files with 1488 additions and 1366 deletions

View File

@@ -1,17 +1,17 @@
from decimal import Decimal from decimal import Decimal
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field from pydantic import Field
class CurrencyMixin(BaseModel): class CurrencyMixin:
"""Mixin for currency-related fields""" """Mixin for currency-related fields"""
currency: Optional[str] = Field(None, max_length=3, description="Currency") currency: Optional[str] = Field(None, max_length=3, description="Currency")
currency_factor: Optional[Decimal] = Field(None, description="Currency factor") currency_factor: Optional[Decimal] = Field(None, description="Currency factor")
class AffectValueMixin(BaseModel): class AffectValueMixin:
"""Mixin for value affect flags""" """Mixin for value affect flags"""
not_affect_usd_value: Optional[int] = Field( not_affect_usd_value: Optional[int] = Field(
@@ -22,7 +22,7 @@ class AffectValueMixin(BaseModel):
) )
class UpdateFlagsMixin(BaseModel): class UpdateFlagsMixin:
"""Mixin for update flags""" """Mixin for update flags"""
update_vat: Optional[int] = Field(None, description="Update VAT") update_vat: Optional[int] = Field(None, description="Update VAT")

View File

@@ -1,4 +1,4 @@
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union
from core.database import get_core_db from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource from core.security import get_current_user, validate_access_to_resource
@@ -49,6 +49,20 @@ class TenantCRUDRoutes(
parent_id_name="pedimento_id", parent_id_name="pedimento_id",
enable_list=False, enable_list=False,
).router ).router
3. Parent resource with string ID (e.g., /vehicles with vehicle_key):
router = TenantCRUDRoutes(
service=VehicleService,
create_schema=VehicleCreate,
update_schema=VehicleUpdate,
response_schema=VehicleResponse,
prefix="/vehicles",
tags=["Vehicles"],
resource_name="Vehicle",
id_name="vehicle_key",
id_type=str, # Specify string type for vehicle_key
enable_list=True,
).router
""" """
def __init__( def __init__(
@@ -61,6 +75,7 @@ class TenantCRUDRoutes(
tags: list[str], tags: list[str],
resource_name: str = "Resource", resource_name: str = "Resource",
id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id") id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id")
id_type: Type = int, # Type of the ID (int, str, etc.)
parent_id_name: Optional[ parent_id_name: Optional[
str str
] = None, # For child resources (e.g., "pedimento_id") ] = None, # For child resources (e.g., "pedimento_id")
@@ -78,6 +93,7 @@ class TenantCRUDRoutes(
self.response_schema = response_schema self.response_schema = response_schema
self.resource_name = resource_name self.resource_name = resource_name
self.id_name = id_name or parent_id_name or "id" self.id_name = id_name or parent_id_name or "id"
self.id_type = id_type
self.parent_id_name = parent_id_name self.parent_id_name = parent_id_name
self.db_dependency = db_dependency self.db_dependency = db_dependency
self.auth_dependency = auth_dependency self.auth_dependency = auth_dependency
@@ -210,7 +226,7 @@ class TenantCRUDRoutes(
f"/{{{self.id_name}}}", response_model=self.response_schema f"/{{{self.id_name}}}", response_model=self.response_schema
) )
async def get_resource_by_id( async def get_resource_by_id(
resource_id: int = Path(..., alias=self.id_name), resource_id: Union[int, str] = Path(..., alias=self.id_name),
company_id: int = Query(..., description="Company ID"), company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency), db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency), current_user: Dict[str, Any] = Depends(self.auth_dependency),
@@ -287,7 +303,7 @@ class TenantCRUDRoutes(
) )
async def update_resource_by_id( async def update_resource_by_id(
data: UpdateSchemaType, data: UpdateSchemaType,
resource_id: int = Path(..., alias=self.id_name), resource_id: Union[int, str] = Path(..., alias=self.id_name),
company_id: int = Query(..., description="Company ID"), company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency), db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency), current_user: Dict[str, Any] = Depends(self.auth_dependency),
@@ -333,7 +349,7 @@ class TenantCRUDRoutes(
# Parent resource # Parent resource
@self.router.delete(f"/{{{self.id_name}}}", status_code=204) @self.router.delete(f"/{{{self.id_name}}}", status_code=204)
async def delete_resource_by_id( async def delete_resource_by_id(
resource_id: int = Path(..., alias=self.id_name), resource_id: Union[int, str] = Path(..., alias=self.id_name),
company_id: int = Query(..., description="Company ID"), company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency), db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency), current_user: Dict[str, Any] = Depends(self.auth_dependency),

View File

@@ -19,7 +19,7 @@ from .dto import (
) )
from .service import ClassService from .service import ClassService
router = APIRouter(prefix="/classes", tags=["Classes"]) router = APIRouter(prefix="/classes")
@router.get("/", response_model=ClassListDTO) @router.get("/", response_model=ClassListDTO)

View File

@@ -93,7 +93,6 @@ class ClientProviderProgramsDTO(BaseModel):
class ClientProviderCreateDTO(BaseModel): class ClientProviderCreateDTO(BaseModel):
"""DTO para crear cliente/proveedor""" """DTO para crear cliente/proveedor"""
client_id: str = Field(..., max_length=8, description="Client ID")
type_nat_foreign: Optional[str] = Field( type_nat_foreign: Optional[str] = Field(
None, max_length=1, description="Type national/foreign" None, max_length=1, description="Type national/foreign"
) )
@@ -180,7 +179,7 @@ class ClientProviderUpdateDTO(BaseModel):
class ClientProviderResponseDTO(BaseModel): class ClientProviderResponseDTO(BaseModel):
"""DTO para respuesta de cliente/proveedor""" """DTO para respuesta de cliente/proveedor"""
client_id: str id: int
type_nat_foreign: Optional[str] = None type_nat_foreign: Optional[str] = None
name: Optional[str] = None name: Optional[str] = None
short_name: Optional[str] = None short_name: Optional[str] = None
@@ -196,6 +195,8 @@ class ClientProviderResponseDTO(BaseModel):
incoterm: Optional[str] = None incoterm: Optional[str] = None
is_national_provider: Optional[str] = None is_national_provider: Optional[str] = None
enabled_disabled: Optional[int] = None enabled_disabled: Optional[int] = None
tenant_id: int
company_id: int
# Nested DTOs # Nested DTOs
address: Optional[ClientProviderAddressDTO] = None address: Optional[ClientProviderAddressDTO] = None

View File

@@ -5,348 +5,154 @@ Endpoints API para gestión de clientes y proveedores
from typing import List, Optional from typing import List, Optional
from core.database import get_core_db from core.database import get_core_db
from core.security import get_current_user from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import ( from .dto import (
ClientProviderBasicDTO, ClientProviderBasicDTO,
ClientProviderCreateDTO, ClientProviderCreateDTO,
ClientProviderListDTO,
ClientProviderResponseDTO, ClientProviderResponseDTO,
ClientProviderUpdateDTO, ClientProviderUpdateDTO,
) )
from .service import ClientProviderService from .service import ClientProviderService
from .models import ClientProvider
# Create base CRUD router using TenantCRUDRoutes factory
base_router = TenantCRUDRoutes(
service=ClientProviderService,
create_schema=ClientProviderCreateDTO,
update_schema=ClientProviderUpdateDTO,
response_schema=ClientProviderResponseDTO,
prefix="/clients-providers",
tags=[],
resource_name="Client/Provider",
id_name="id", # Using numeric ID
enable_list=True, # Enable GET /clients-providers with pagination
enable_filters=True, # Enable filtering
default_page_size=50,
max_page_size=100,
).router
# Create main router to add custom endpoints
router = APIRouter(prefix="/clients-providers") router = APIRouter(prefix="/clients-providers")
# Include base CRUD routes
@router.post( router.include_router(base_router, prefix="")
"/", response_model=ClientProviderResponseDTO, status_code=status.HTTP_201_CREATED
)
async def create_client_provider(
client_data: ClientProviderCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new client or provider in the system
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
# Ensure the client_data is associated with the correct tenant and company
if client_data.tenant_id != tenant_id or client_data.company_id != company_id:
raise HTTPException(
status_code=400, detail="Mismatch in tenant or company association"
)
service = ClientProviderService(db)
return service.create_client_provider(client_data)
@router.get("/", response_model=ClientProviderListDTO)
async def list_clients_providers(
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(
100, ge=1, le=1000, description="Maximum number of records to return"
),
search: Optional[str] = Query(None, description="Search text for name, RFC, or ID"),
client_or_provider: Optional[str] = Query(
None, regex="^[CP]$", description="Filter by type: C=Client, P=Provider"
),
enabled_only: bool = Query(False, description="Show only enabled records"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
List clients and providers with optional filters and pagination
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
return service.list_clients_providers(
skip, limit, search, client_or_provider, enabled_only
)
# Custom endpoints
@router.get("/clients", response_model=List[ClientProviderBasicDTO]) @router.get("/clients", response_model=List[ClientProviderBasicDTO])
async def get_clients_only( async def get_clients_only(
company_id: int = Query(..., description="Company ID"),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000), limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db), db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
): ):
""" """Get only clients (client_or_provider = 'C')"""
Get only clients (client_or_provider = 'C') tenant_id = validate_access_to_resource(db, company_id, current_user)
"""
# Validate access to the tenant and company clients = (
tenant_id = current_user.get("tenant_id") db.query(ClientProvider)
company_id = current_user.get("company_id") .filter(
ClientProvider.tenant_id == tenant_id,
if not tenant_id or not company_id: ClientProvider.company_id == company_id,
raise HTTPException( ClientProvider.client_or_provider == "C"
status_code=403, detail="Access denied: Tenant or Company not found"
) )
.offset(skip)
service = ClientProviderService(db) .limit(limit)
return service.get_clients_only(skip, limit) .all()
)
return [ClientProviderBasicDTO.model_validate(c) for c in clients]
@router.get("/providers", response_model=List[ClientProviderBasicDTO]) @router.get("/providers", response_model=List[ClientProviderBasicDTO])
async def get_providers_only( async def get_providers_only(
company_id: int = Query(..., description="Company ID"),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000), limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db), db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
): ):
""" """Get only providers (client_or_provider = 'P')"""
Get only providers (client_or_provider = 'P') tenant_id = validate_access_to_resource(db, company_id, current_user)
"""
# Validate access to the tenant and company providers = (
tenant_id = current_user.get("tenant_id") db.query(ClientProvider)
company_id = current_user.get("company_id") .filter(
ClientProvider.tenant_id == tenant_id,
if not tenant_id or not company_id: ClientProvider.company_id == company_id,
raise HTTPException( ClientProvider.client_or_provider == "P"
status_code=403, detail="Access denied: Tenant or Company not found"
) )
.offset(skip)
service = ClientProviderService(db) .limit(limit)
return service.get_providers_only(skip, limit) .all()
)
return [ClientProviderBasicDTO.model_validate(p) for p in providers]
@router.get("/search/rfc/{rfc}", response_model=List[ClientProviderBasicDTO]) @router.get("/search/rfc/{rfc}", response_model=List[ClientProviderBasicDTO])
async def search_by_rfc( async def search_by_rfc(
rfc: str, rfc: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db), db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
): ):
""" """Search clients/providers by RFC"""
Search clients/providers by RFC tenant_id = validate_access_to_resource(db, company_id, current_user)
"""
# Validate access to the tenant and company clients = (
tenant_id = current_user.get("tenant_id") db.query(ClientProvider)
company_id = current_user.get("company_id") .filter(
ClientProvider.tenant_id == tenant_id,
if not tenant_id or not company_id: ClientProvider.company_id == company_id,
raise HTTPException( ClientProvider.rfc.ilike(f"%{rfc}%")
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
return service.search_by_rfc(rfc)
@router.get("/{client_id}", response_model=ClientProviderResponseDTO)
async def get_client_provider(
client_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get client/provider by ID with all related information
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:
raise HTTPException(
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
)
return client
@router.put("/{client_id}", response_model=ClientProviderResponseDTO)
async def update_client_provider(
client_id: str,
client_data: ClientProviderUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Update client/provider information
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.update_client_provider(client_id, client_data)
if not client:
raise HTTPException(
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
)
return client
@router.delete("/{client_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_client_provider(
client_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete client/provider from the system
Note: This will completely remove the client/provider and all related data.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
if not service.delete_client_provider(client_id):
raise HTTPException(
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
) )
.all()
)
return [ClientProviderBasicDTO.model_validate(c) for c in clients]
@router.patch("/{client_id}/toggle-status", response_model=ClientProviderResponseDTO) @router.patch("/{client_id}/toggle-status", response_model=ClientProviderResponseDTO)
async def toggle_client_provider_status( async def toggle_client_provider_status(
client_id: str, client_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db), db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
): ):
""" """Toggle client/provider enabled/disabled status"""
Toggle client/provider enabled/disabled status tenant_id = validate_access_to_resource(db, company_id, current_user)
"""
# Validate access to the tenant and company client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.toggle_status(client_id)
if not client: if not client:
raise HTTPException( raise HTTPException(status_code=404, detail="Client/Provider not found")
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
) # Toggle status (1 = enabled, 0 = disabled)
return client client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0
try:
# Endpoints específicos para información detallada db.commit()
@router.get("/{client_id}/address", response_model=dict) db.refresh(client)
async def get_client_provider_address( return client
client_id: str, except Exception as e:
db: Session = Depends(get_core_db), db.rollback()
current_user: dict = Depends(get_current_user), raise HTTPException(status_code=500, detail="Error updating status")
):
"""
Get only address information for a client/provider
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:
raise HTTPException(
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
)
return {"client_id": client.client_id, "address": client.address}
@router.get("/{client_id}/programs", response_model=dict)
async def get_client_provider_programs(
client_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get only programs information for a client/provider
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:
raise HTTPException(
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
)
return {"client_id": client.client_id, "programs": client.programs}
@router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO) @router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO)
async def get_client_provider_basic_info( async def get_client_provider_basic_info(
client_id: str, client_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db), db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
): ):
""" """Get basic information for a client/provider (without address and programs)"""
Get basic information for a client/provider (without address and programs) tenant_id = validate_access_to_resource(db, company_id, current_user)
"""
# Validate access to the tenant and company client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client: if not client:
raise HTTPException( raise HTTPException(status_code=404, detail="Client/Provider not found")
status_code=404, detail=f"Client/Provider with ID '{client_id}' not found"
) return ClientProviderBasicDTO.model_validate(client)
return ClientProviderBasicDTO(
client_id=client.client_id,
name=client.name,
short_name=client.short_name,
rfc=client.rfc,
client_or_provider=client.client_or_provider,
enabled_disabled=client.enabled_disabled,
)

View File

@@ -3,7 +3,7 @@ Capa de servicio para lógica de negocio de clientes y proveedores
""" """
import logging import logging
from typing import List, Optional from typing import List, Optional, Tuple, Dict, Any
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import or_ from sqlalchemy import or_
@@ -28,77 +28,237 @@ class ClientProviderService:
def __init__(self, db: Session): def __init__(self, db: Session):
self.db = db self.db = db
def create_client_provider( # Métodos para TenantCRUDRoutes
self, client_data: ClientProviderCreateDTO @staticmethod
) -> ClientProviderResponseDTO: def get_all(
""" db: Session,
Crea un nuevo cliente/proveedor en el sistema tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ClientProvider], int]:
"""Get all clients/providers for a tenant/company with pagination"""
query = db.query(ClientProvider).filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
Args: # Apply filters if provided
client_data: Datos del cliente/proveedor a crear if filters:
if filters.get("search"):
Returns: search_pattern = f"%{filters['search']}%"
ClientProviderResponseDTO con información del cliente/proveedor creado query = query.filter(
or_(
Raises: ClientProvider.name.ilike(search_pattern),
HTTPException: Si el cliente ya existe o error en la creación ClientProvider.short_name.ilike(search_pattern),
""" ClientProvider.rfc.ilike(search_pattern),
try: )
# Verificar que no exista el cliente
existing = (
self.db.query(ClientProvider)
.filter(ClientProvider.client_id == client_data.client_id)
.first()
)
if existing:
raise HTTPException(
status_code=400,
detail=f"Client with ID '{client_data.client_id}' already exists",
) )
if filters.get("client_or_provider"):
query = query.filter(
ClientProvider.client_or_provider == filters["client_or_provider"]
)
if filters.get("status"):
enabled = 1 if filters["status"] == "enabled" else 0
query = query.filter(ClientProvider.enabled_disabled == enabled)
# Crear cliente/proveedor principal total = query.count()
clients = query.options(
joinedload(ClientProvider.address),
joinedload(ClientProvider.programs)
).offset(skip).limit(limit).all()
return clients, total
@staticmethod
def get_by_id(
db: Session, client_id: int, tenant_id: int, company_id: int
) -> Optional[ClientProvider]:
"""Get client/provider by ID"""
return (
db.query(ClientProvider)
.options(
joinedload(ClientProvider.address),
joinedload(ClientProvider.programs)
)
.filter(
ClientProvider.id == client_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
client_data: ClientProviderCreateDTO,
tenant_id: int,
company_id: int,
) -> ClientProvider:
"""Create a new client/provider"""
try:
# Create main client/provider
data_dict = client_data.model_dump(exclude={"address", "programs"})
db_client = ClientProvider( db_client = ClientProvider(
client_id=client_data.client_id, **data_dict,
type_nat_foreign=client_data.type_nat_foreign, tenant_id=tenant_id,
name=client_data.name, company_id=company_id
short_name=client_data.short_name,
rfc=client_data.rfc,
curp=client_data.curp,
client_or_provider=client_data.client_or_provider,
linking=client_data.linking,
transform_subassembly=client_data.transform_subassembly,
extra_information=client_data.extra_information,
web_key=client_data.web_key,
responsible=client_data.responsible,
position=client_data.position,
incoterm=client_data.incoterm,
is_national_provider=client_data.is_national_provider,
enabled_disabled=client_data.enabled_disabled,
) )
self.db.add(db_client) db.add(db_client)
self.db.flush() # Para obtener el ID antes del commit db.flush()
# Crear dirección si se proporciona # Create address if provided
if client_data.address: if client_data.address:
db_address = ClientProviderAddress( db_address = ClientProviderAddress(
client_id=client_data.client_id, client_id=db_client.id,
tenant_id=tenant_id,
**client_data.address.model_dump(exclude_unset=True),
)
db.add(db_address)
# Create programs if provided
if client_data.programs:
db_programs = ClientProviderPrograms(
client_id=db_client.id,
tenant_id=tenant_id,
**client_data.programs.model_dump(exclude_unset=True),
)
db.add(db_programs)
db.commit()
db.refresh(db_client)
return db_client
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError creating client/provider: {str(e)}")
raise HTTPException(
status_code=400, detail="Client/Provider already exists"
)
except Exception as e:
db.rollback()
logger.error(f"Error creating client/provider: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating client/provider"
)
@staticmethod
def update(
db: Session,
client_id: int,
tenant_id: int,
company_id: int,
client_data: ClientProviderUpdateDTO,
) -> Optional[ClientProvider]:
"""Update a client/provider"""
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
if not client:
return None
try:
# Update main fields
update_data = client_data.model_dump(
exclude_unset=True, exclude={"address", "programs"}
)
for field, value in update_data.items():
setattr(client, field, value)
# Update address
if client_data.address:
if client.address:
address_data = client_data.address.model_dump(exclude_unset=True)
for field, value in address_data.items():
setattr(client.address, field, value)
else:
db_address = ClientProviderAddress(
client_id=client.id,
tenant_id=tenant_id,
**client_data.address.model_dump(exclude_unset=True),
)
db.add(db_address)
# Update programs
if client_data.programs:
if client.programs:
programs_data = client_data.programs.model_dump(exclude_unset=True)
for field, value in programs_data.items():
setattr(client.programs, field, value)
else:
db_programs = ClientProviderPrograms(
client_id=client.id,
tenant_id=tenant_id,
**client_data.programs.model_dump(exclude_unset=True),
)
db.add(db_programs)
db.commit()
db.refresh(client)
return client
except Exception as e:
db.rollback()
logger.error(f"Error updating client/provider {client_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error updating client/provider"
)
@staticmethod
def delete(
db: Session, client_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a client/provider"""
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
if not client:
return False
try:
db.delete(client)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting client/provider {client_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error deleting client/provider"
)
# Legacy methods for custom endpoints
def create_client_provider_legacy(
self, client_data: ClientProviderCreateDTO
) -> ClientProviderResponseDTO:
"""Legacy method for creating client/provider"""
try:
# Create main client/provider
data_dict = client_data.model_dump(exclude={"address", "programs"})
db_client = ClientProvider(**data_dict)
self.db.add(db_client)
self.db.flush()
# Create address if provided
if client_data.address:
db_address = ClientProviderAddress(
client_id=db_client.client_id,
**client_data.address.model_dump(exclude_unset=True), **client_data.address.model_dump(exclude_unset=True),
) )
self.db.add(db_address) self.db.add(db_address)
# Crear programas si se proporciona # Create programs if provided
if client_data.programs: if client_data.programs:
db_programs = ClientProviderPrograms( db_programs = ClientProviderPrograms(
client_id=client_data.client_id, client_id=db_client.client_id,
**client_data.programs.model_dump(exclude_unset=True), **client_data.programs.model_dump(exclude_unset=True),
) )
self.db.add(db_programs) self.db.add(db_programs)
self.db.commit() self.db.commit()
self.db.refresh(db_client)
return self._get_client_with_relations(client_data.client_id) return self._get_client_with_relations(db_client.client_id)
except IntegrityError as e: except IntegrityError as e:
self.db.rollback() self.db.rollback()

View File

@@ -12,8 +12,6 @@ from pydantic import BaseModel, Field
class CompanyCreateDTO(BaseModel): class CompanyCreateDTO(BaseModel):
"""DTO para crear una empresa""" """DTO para crear una empresa"""
id: str = Field(default="EMP", max_length=3, description="Company ID")
consecutive: bool = Field(default=True, description="Unique record control")
name: Optional[str] = Field(None, max_length=255, description="Company name") name: Optional[str] = Field(None, max_length=255, description="Company name")
rfc: Optional[str] = Field(None, max_length=30, description="Company RFC") rfc: Optional[str] = Field(None, max_length=30, description="Company RFC")
main_activity: Optional[str] = Field( main_activity: Optional[str] = Field(

View File

@@ -1,119 +1,143 @@
""" """
Endpoints API para gestión de empresa Rutas para gestión de empresa
""" """
from typing import Optional from typing import List
from core.database import get_core_db
from core.security import get_current_user, get_tenant_from_token
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from ....common.tenant_crud_routes import TenantCRUDRoutes
from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO
from .models import Company
from .service import CompanyService from .service import CompanyService
router = APIRouter(prefix="/company") # Base CRUD routes using TenantCRUDRoutes
base_router = TenantCRUDRoutes(
service=CompanyService,
create_schema=CompanyCreateDTO,
update_schema=CompanyUpdateDTO,
response_schema=CompanyResponseDTO,
prefix="/company",
tags=[],
id_name="id",
enable_list=True,
enable_filters=True,
).router
# Main router that includes base CRUD
router = APIRouter()
router.include_router(base_router)
@router.post( # Custom endpoints
"/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED @router.get(
"/my-companies",
response_model=List[CompanyResponseDTO],
summary="Get all companies for current tenant",
) )
async def create_company( async def get_my_companies(
company_data: CompanyCreateDTO,
db: Session = Depends(get_core_db), db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(get_current_user),
): ):
""" """Get all companies that belong to the current user's tenant"""
Create a new company in the system tenant_id = current_user.get("tenant_id")
Only one company can exist per system due to the unique consecutive field.
"""
service = CompanyService(db)
return service.create_company(company_data)
@router.get("/", response_model=Optional[CompanyResponseDTO])
async def get_company(
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get the registered company information
Returns the unique company in the system or None if it doesn't exist.
"""
service = CompanyService(db)
company = service.get_company()
if not company:
raise HTTPException(status_code=404, detail="No company found")
return company
@router.get("/my-companies", response_model=list[CompanyResponseDTO])
async def get_my_companies(
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get all companies that belong to the user's tenant
Returns a list of companies associated with the tenant_id from the user's token
"""
tenant_id = get_tenant_from_token(current_user)
if not tenant_id: if not tenant_id:
raise HTTPException(status_code=400, detail="Tenant ID not found in token") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
service = CompanyService(db) service = CompanyService(db)
companies = service.get_companies_by_tenant(tenant_id) companies = service.get_companies_by_tenant(tenant_id)
return companies return [CompanyResponseDTO.model_validate(company) for company in companies]
@router.get("/status/exists", response_model=dict) @router.get(
"/status/exists",
response_model=dict,
summary="Check if company exists for tenant",
)
async def check_company_exists( async def check_company_exists(
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
): ):
""" """Check if a company exists for the current tenant"""
Check if a company is registered in the system tenant_id = current_user.get("tenant_id")
""" if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
service = CompanyService(db) service = CompanyService(db)
exists = service.exists_company() exists = service.exists_company(tenant_id)
return {
"exists": exists, return {"exists": exists}
"message": "Company found" if exists else "No company registered",
}
# Specific endpoints for important fields @router.get(
@router.get("/info/basic", response_model=dict) "/info/basic/{company_id}",
async def get_company_basic_info( response_model=dict,
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) summary="Get basic company info",
)
async def get_basic_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
): ):
""" """Get basic information about a company"""
Get basic company information (name, RFC, main activity) tenant_id = current_user.get("tenant_id")
""" company_id_from_user = current_user.get("company_id")
service = CompanyService(db)
company = service.get_company() # Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company: if not company:
raise HTTPException(status_code=404, detail="No company found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return { return {
"id": company.id,
"name": company.name, "name": company.name,
"rfc": company.rfc, "rfc": company.rfc,
"main_activity": company.main_activity, "program": company.program,
"logo": company.logo,
} }
@router.get("/info/responsible", response_model=dict) @router.get(
async def get_company_responsible_info( "/info/responsible/{company_id}",
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) response_model=dict,
summary="Get responsible person info",
)
async def get_responsible_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
): ):
""" """Get responsible person information for a company"""
Get company responsible person information tenant_id = current_user.get("tenant_id")
""" company_id_from_user = current_user.get("company_id")
service = CompanyService(db)
company = service.get_company() # Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company: if not company:
raise HTTPException(status_code=404, detail="No company found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return { return {
"responsible": company.responsible, "responsible": company.responsible,
@@ -125,77 +149,35 @@ async def get_company_responsible_info(
} }
@router.get("/info/program", response_model=dict) @router.get(
async def get_company_program_info( "/info/program/{company_id}",
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) response_model=dict,
summary="Get program information",
)
async def get_program_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
): ):
""" """Get program information for a company"""
Get company program information tenant_id = current_user.get("tenant_id")
""" company_id_from_user = current_user.get("company_id")
service = CompanyService(db)
company = service.get_company() # Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company: if not company:
raise HTTPException(status_code=404, detail="No company found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return { return {
"program": company.program, "program": company.program,
"program_number": company.program_number, "program_number": company.program_number,
"prosec": company.prosec, "prosec": company.prosec,
"prosec_authorization": company.prosec_authorization, "prosec_authorization": company.prosec_authorization,
"manufacturer_id": company.manufacturer_id,
} }
@router.get("/{company_id}", response_model=CompanyResponseDTO)
async def get_company_by_id(
company_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get company by specific ID
"""
service = CompanyService(db)
company = service.get_company_by_id(company_id)
if not company:
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)
return company
@router.put("/{company_id}", response_model=CompanyResponseDTO)
async def update_company(
company_id: str,
company_data: CompanyUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Update company information
"""
service = CompanyService(db)
company = service.update_company(company_id, company_data)
if not company:
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)
return company
@router.delete("/{company_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_company(
company_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete company from the system
Note: This will completely remove the company from the system.
"""
service = CompanyService(db)
if not service.delete_company(company_id):
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)

View File

@@ -3,7 +3,7 @@ Capa de servicio para lógica de negocio de empresa
""" """
import logging import logging
from typing import List, Optional from typing import List, Optional, Tuple, Dict, Any
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@@ -21,187 +21,141 @@ class CompanyService:
def __init__(self, db: Session): def __init__(self, db: Session):
self.db = db self.db = db
def create_company(self, company_data: CompanyCreateDTO) -> CompanyResponseDTO: # Métodos para TenantCRUDRoutes
""" @staticmethod
Crea una nueva empresa en el sistema def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[Company], int]:
"""Get all companies for a tenant with pagination"""
query = db.query(Company).filter(Company.tenant_id == tenant_id)
Args: # Apply filters if provided
company_data: Datos de la empresa a crear if filters:
if filters.get("name"):
Returns: query = query.filter(
CompanyResponseDTO con información de la empresa creada Company.name.ilike(f"%{filters['name']}%")
)
Raises: if filters.get("rfc"):
HTTPException: Si ya existe una empresa o error en la creación query = query.filter(
""" Company.rfc.ilike(f"%{filters['rfc']}%")
try:
# Verificar que no exista ya una empresa (solo puede haber una por el consecutivo único)
existing = self.db.query(Company).filter(Company.id).first()
if existing:
raise HTTPException(
status_code=400,
detail="A company is already registered in the system",
) )
# Crear empresa total = query.count()
companies = query.offset(skip).limit(limit).all()
return companies, total
@staticmethod
def get_by_id(
db: Session, company_id: int, tenant_id: int, company_id_unused: int
) -> Optional[Company]:
"""Get company by ID"""
return (
db.query(Company)
.filter(
Company.id == company_id,
Company.tenant_id == tenant_id,
)
.first()
)
@staticmethod
def create(
db: Session,
company_data: CompanyCreateDTO,
tenant_id: int,
company_id: int,
) -> Company:
"""Create a new company"""
try:
db_company = Company( db_company = Company(
id=company_data.id, **company_data.model_dump(exclude_unset=True),
consecutive=company_data.consecutive, tenant_id=tenant_id
name=company_data.name,
rfc=company_data.rfc,
main_activity=company_data.main_activity,
program=company_data.program,
program_number=company_data.program_number,
prosec=company_data.prosec,
prosec_authorization=company_data.prosec_authorization,
manufacturer_id=company_data.manufacturer_id,
broker_company=company_data.broker_company,
responsible=company_data.responsible,
responsible_name=company_data.responsible_name,
responsible_last_name=company_data.responsible_last_name,
responsible_mother_last_name=company_data.responsible_mother_last_name,
responsible_rfc=company_data.responsible_rfc,
position=company_data.position,
logo=company_data.logo,
has_express_line=company_data.has_express_line,
order_format_type=company_data.order_format_type,
previous_code=company_data.previous_code,
is_service_company=company_data.is_service_company,
client_name=company_data.client_name,
subassembly_mode=company_data.subassembly_mode,
curp=company_data.curp,
inter_db_name=company_data.inter_db_name,
ctpat_svi=company_data.ctpat_svi,
trusted_exporter_number=company_data.trusted_exporter_number,
prevalidator_key=company_data.prevalidator_key,
seventh_amendment=company_data.seventh_amendment,
) )
self.db.add(db_company) db.add(db_company)
self.db.commit() db.commit()
self.db.refresh(db_company) db.refresh(db_company)
return CompanyResponseDTO.model_validate(db_company) return db_company
except IntegrityError as e: except IntegrityError as e:
self.db.rollback() db.rollback()
logger.error(f"IntegrityError creating company: {str(e)}") logger.error(f"IntegrityError creating company: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="Integrity error: A company already exists in the system", detail="Company already exists",
) )
except HTTPException:
raise
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error creating company: {str(e)}") logger.error(f"Error creating company: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating company") raise HTTPException(status_code=500, detail="Error creating company")
def get_company(self) -> Optional[CompanyResponseDTO]: @staticmethod
""" def update(
Obtiene la empresa (solo puede haber una) db: Session,
company_id: int,
Returns: tenant_id: int,
CompanyResponseDTO o None si no existe company_id_unused: int,
""" company_data: CompanyUpdateDTO,
company = self.db.query(Company).filter(Company.id).first() ) -> Optional[Company]:
if not company: """Update a company"""
return None company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_unused)
return CompanyResponseDTO.model_validate(company)
def get_company_by_id(self, company_id: str) -> Optional[CompanyResponseDTO]:
"""
Obtiene una empresa por ID
Args:
company_id: ID de la empresa
Returns:
CompanyResponseDTO o None si no existe
"""
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
return None
return CompanyResponseDTO.model_validate(company)
def update_company(
self, company_id: str, company_data: CompanyUpdateDTO
) -> Optional[CompanyResponseDTO]:
"""
Actualiza una empresa
Args:
company_id: ID de la empresa a actualizar
company_data: Datos a actualizar
Returns:
CompanyResponseDTO actualizada o None si no existe
"""
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company: if not company:
return None return None
# Actualizar solo campos proporcionados # Update only provided fields
update_data = company_data.model_dump(exclude_unset=True) update_data = company_data.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
setattr(company, field, value) setattr(company, field, value)
try: try:
self.db.commit() db.commit()
self.db.refresh(company) db.refresh(company)
return CompanyResponseDTO.model_validate(company) return company
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error updating company {company_id}: {str(e)}") logger.error(f"Error updating company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating company") raise HTTPException(status_code=500, detail="Error updating company")
def delete_company(self, company_id: str) -> bool: @staticmethod
""" def delete(
Elimina una empresa db: Session, company_id: int, tenant_id: int, company_id_unused: int
) -> bool:
Args: """Delete a company"""
company_id: ID de la empresa a eliminar company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_unused)
Returns:
True si se eliminó, False si no existe
"""
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company: if not company:
return False return False
try: try:
self.db.delete(company) db.delete(company)
self.db.commit() db.commit()
return True return True
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error deleting company {company_id}: {str(e)}") logger.error(f"Error deleting company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting company") raise HTTPException(status_code=500, detail="Error deleting company")
def exists_company(self) -> bool: # Custom methods
""" def get_companies_by_tenant(self, tenant_id: int) -> List[Company]:
Verifica si existe una empresa registrada """Get all companies for a tenant"""
return (
Returns:
True si existe una empresa, False en caso contrario
"""
return self.db.query(Company).filter(Company.id).first() is not None
def get_companies_by_tenant(self, tenant_id: int) -> List[CompanyResponseDTO]:
"""
Obtiene todas las compañías que pertenecen a un tenant específico
Args:
tenant_id: ID del tenant
Returns:
Lista de CompanyResponseDTO
"""
companies = (
self.db.query(Company) self.db.query(Company)
.filter(Company.tenant_id == tenant_id) .filter(Company.tenant_id == tenant_id)
.order_by(Company.name) .order_by(Company.name)
.all() .all()
) )
return [CompanyResponseDTO.model_validate(company) for company in companies] def exists_company(self, tenant_id: int) -> bool:
"""Check if a company exists for a tenant"""
return (
self.db.query(Company)
.filter(Company.tenant_id == tenant_id)
.first()
is not None
)

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from .dto import CountryRuleOctCreateDTO, CountryRuleOctResponseDTO from .dto import CountryRuleOctCreateDTO, CountryRuleOctResponseDTO
from .services import CountryRuleOctService from .services import CountryRuleOctService
router = APIRouter(prefix="/country-rule-oct", tags=["CountryRuleOct"]) router = APIRouter(prefix="/country-rule-oct")
@router.get("/", response_model=List[CountryRuleOctResponseDTO]) @router.get("/", response_model=List[CountryRuleOctResponseDTO])

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from .dto import DriverCreateDTO, DriverResponseDTO from .dto import DriverCreateDTO, DriverResponseDTO
from .services import DriverService from .services import DriverService
router = APIRouter(prefix="/drivers", tags=["Drivers"]) router = APIRouter(prefix="/drivers")
@router.get("/", response_model=List[DriverResponseDTO]) @router.get("/", response_model=List[DriverResponseDTO])

View File

@@ -1,19 +1,30 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class ExchangeRateBaseDTO(BaseModel): class ExchangeRateBaseDTO(BaseModel):
date: int date: int = Field(..., description="Exchange rate date")
value: Optional[float] value: Optional[float] = Field(None, description="Exchange rate value")
local_currency: Optional[str] local_currency: Optional[str] = Field(None, max_length=7, description="Local currency code")
foreign_currency: Optional[str] foreign_currency: Optional[str] = Field(None, max_length=7, description="Foreign currency code")
class ExchangeRateCreateDTO(ExchangeRateBaseDTO): class ExchangeRateCreateDTO(ExchangeRateBaseDTO):
"""Schema for creating an exchange rate"""
pass pass
class ExchangeRateUpdateDTO(ExchangeRateBaseDTO):
"""Schema for updating an exchange rate"""
date: Optional[int] = Field(None, description="Exchange rate date")
class ExchangeRateResponseDTO(ExchangeRateBaseDTO): class ExchangeRateResponseDTO(ExchangeRateBaseDTO):
"""Schema for exchange rate response"""
id: int
company_id: int
tenant_id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,100 +1,20 @@
from typing import List from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.database import get_core_db from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO, ExchangeRateUpdateDTO
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO
from .services import ExchangeRateService from .services import ExchangeRateService
router = APIRouter(prefix="/exchange-rate", tags=["ExchangeRate"]) # Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
service=ExchangeRateService,
@router.get("/", response_model=List[ExchangeRateResponseDTO]) create_schema=ExchangeRateCreateDTO,
async def list_exchange_rates( update_schema=ExchangeRateUpdateDTO,
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) response_schema=ExchangeRateResponseDTO,
): prefix="/exchange-rate",
""" tags=[],
List all ExchangeRate entries. resource_name="Exchange Rate",
""" id_name="id", # Using numeric ID
# Validate access to the tenant and company enable_list=True, # Enable GET /exchange-rate with pagination
tenant_id = current_user.get("tenant_id") enable_filters=True, # Enable filtering by date, local_currency, foreign_currency
company_id = current_user.get("company_id") default_page_size=50,
max_page_size=100,
if not tenant_id or not company_id: ).router
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(ExchangeRateService).all()
@router.get("/{date}", response_model=ExchangeRateResponseDTO)
async def read_exchange_rate(
date: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific ExchangeRate by its date.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
exchange_rate = ExchangeRateService.get_exchange_rate_by_date(db, date)
if not exchange_rate:
raise HTTPException(status_code=404, detail="ExchangeRate not found")
return exchange_rate
@router.post(
"/", response_model=ExchangeRateResponseDTO, status_code=status.HTTP_201_CREATED
)
async def create_exchange_rate(
exchange_rate_data: ExchangeRateCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new ExchangeRate entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return ExchangeRateService.create_exchange_rate(db, exchange_rate_data)
@router.delete("/{date}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_exchange_rate(
date: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete an ExchangeRate by its date.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
exchange_rate = ExchangeRateService.delete_exchange_rate(db, date)
if not exchange_rate:
raise HTTPException(status_code=404, detail="ExchangeRate not found")

View File

@@ -1,31 +1,112 @@
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import dto, models from . import dto, models
class ExchangeRateService: class ExchangeRateService:
"""Service for ExchangeRate CRUD operations with tenant support"""
@staticmethod @staticmethod
def get_exchange_rate_by_date(db: Session, date: int): def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.ExchangeRate], int]:
"""Get all exchange rates for a tenant/company with pagination"""
query = db.query(models.ExchangeRate).filter(
models.ExchangeRate.tenant_id == tenant_id,
models.ExchangeRate.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("date"):
query = query.filter(models.ExchangeRate.date == filters["date"])
if filters.get("local_currency"):
query = query.filter(
models.ExchangeRate.local_currency == filters["local_currency"]
)
if filters.get("foreign_currency"):
query = query.filter(
models.ExchangeRate.foreign_currency == filters["foreign_currency"]
)
total = query.count()
exchange_rates = query.order_by(models.ExchangeRate.date.desc()).offset(skip).limit(limit).all()
return exchange_rates, total
@staticmethod
def get_by_id(
db: Session, exchange_rate_id: int, tenant_id: int, company_id: int
) -> Optional[models.ExchangeRate]:
"""Get exchange rate by ID"""
return ( return (
db.query(models.ExchangeRate) db.query(models.ExchangeRate)
.filter(models.ExchangeRate.date == date) .filter(
models.ExchangeRate.id == exchange_rate_id,
models.ExchangeRate.tenant_id == tenant_id,
models.ExchangeRate.company_id == company_id,
)
.first() .first()
) )
@staticmethod @staticmethod
def create_exchange_rate( def create(
db: Session, exchange_rate_data: dto.ExchangeRateCreateDTO db: Session,
): exchange_rate_data: dto.ExchangeRateCreateDTO,
new_exchange_rate = models.ExchangeRate(**exchange_rate_data.dict()) tenant_id: int,
company_id: int,
) -> models.ExchangeRate:
"""Create a new exchange rate"""
new_exchange_rate = models.ExchangeRate(
**exchange_rate_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_exchange_rate) db.add(new_exchange_rate)
db.commit() db.commit()
db.refresh(new_exchange_rate) db.refresh(new_exchange_rate)
return new_exchange_rate return new_exchange_rate
@staticmethod @staticmethod
def delete_exchange_rate(db: Session, date: int): def update(
exchange_rate = ExchangeRateService.get_exchange_rate_by_date(db, date) db: Session,
if exchange_rate: exchange_rate_id: int,
db.delete(exchange_rate) tenant_id: int,
db.commit() company_id: int,
exchange_rate_data: dto.ExchangeRateUpdateDTO,
) -> Optional[models.ExchangeRate]:
"""Update an exchange rate"""
exchange_rate = ExchangeRateService.get_by_id(
db, exchange_rate_id, tenant_id, company_id
)
if not exchange_rate:
return None
# Update fields
update_data = exchange_rate_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(exchange_rate, field, value)
db.commit()
db.refresh(exchange_rate)
return exchange_rate return exchange_rate
@staticmethod
def delete(
db: Session, exchange_rate_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete an exchange rate"""
exchange_rate = ExchangeRateService.get_by_id(
db, exchange_rate_id, tenant_id, company_id
)
if not exchange_rate:
return False
db.delete(exchange_rate)
db.commit()
return True

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from .dto import FractionRuleOctaveCreateDTO, FractionRuleOctaveResponseDTO from .dto import FractionRuleOctaveCreateDTO, FractionRuleOctaveResponseDTO
from .services import FractionRuleOctaveService from .services import FractionRuleOctaveService
router = APIRouter(prefix="/fraction_rule_octave", tags=["FractionRuleOctave"]) router = APIRouter(prefix="/fraction_rule_octave")
@router.get("/", response_model=List[FractionRuleOctaveResponseDTO]) @router.get("/", response_model=List[FractionRuleOctaveResponseDTO])

View File

@@ -1,40 +1,40 @@
""" """
DTOs for GBultos. DTOs for Packages (GBultos).
""" """
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class GBultoBaseDTO(BaseModel): class PackageBaseDTO(BaseModel):
CODE: str key: str = Field(..., description="Package key (primary identifier)", max_length=5)
DESCRIPTION: Optional[str] description_es: Optional[str] = Field(None, description="Description in Spanish", max_length=40)
DESCRIPTIONI: Optional[str] description_en: Optional[str] = Field(None, description="Description in English", max_length=40)
WEIGHT_UNIT: Optional[float] weight_unit: Optional[float] = Field(None, description="Weight unit")
PLURALS: Optional[str] plurals: Optional[str] = Field(None, max_length=4)
PLURAL_IN: Optional[str] plural_in: Optional[str] = Field(None, max_length=4)
CODE_ACE: Optional[str] code_ace: Optional[str] = Field(None, max_length=4)
CODE_AAMEX: Optional[str] code_aamex: Optional[str] = Field(None, max_length=9)
class GBultoCreateDTO(GBultoBaseDTO): class PackageCreateDTO(PackageBaseDTO):
"""Schema for creating a package"""
pass pass
class GBultoUpdateDTO(BaseModel): class PackageUpdateDTO(PackageBaseDTO):
DESCRIPTION: Optional[str] """Schema for updating a package"""
DESCRIPTIONI: Optional[str] key: Optional[str] = Field(None, description="Package key (cannot be modified)", max_length=5)
WEIGHT_UNIT: Optional[float]
PLURALS: Optional[str]
PLURAL_IN: Optional[str]
CODE_ACE: Optional[str]
CODE_AAMEX: Optional[str]
class GBultoResponseDTO(GBultoBaseDTO): class PackageResponseDTO(PackageBaseDTO):
CREATED_AT: Optional[str] """Schema for package response"""
UPDATED_AT: Optional[str] id: int
company_id: int
tenant_id: int
created_at: Optional[str] = None
updated_at: Optional[str] = None
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,127 +1,24 @@
from typing import List """
Routes for managing Packages (GBultos).
"""
from core.database import get_core_db from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import GBultoCreateDTO, GBultoResponseDTO, GBultoUpdateDTO from .dto import PackageCreateDTO, PackageResponseDTO, PackageUpdateDTO
from .models import Package from .services import PackageService
from .services import GBultoService
router = APIRouter(prefix="/bultos", tags=["GBultos"]) # Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
service=PackageService,
@router.get("/", response_model=List[GBultoResponseDTO]) create_schema=PackageCreateDTO,
async def list_bultos( update_schema=PackageUpdateDTO,
skip: int = 0, response_schema=PackageResponseDTO,
limit: int = 100, prefix="/package",
db: Session = Depends(get_core_db), tags=[],
current_user: dict = Depends(get_current_user), resource_name="Package",
): id_name="id", # Using numeric ID
""" enable_list=True, # Enable GET /package with pagination
List all GBultos with pagination. enable_filters=True, # Enable filtering by key and description_es
""" default_page_size=50,
# Validate access to the tenant and company max_page_size=100,
tenant_id = current_user.get("tenant_id") ).router
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(Package).offset(skip).limit(limit).all()
@router.get("/{code}", response_model=GBultoResponseDTO)
async def read_bulto(
code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific Package by its CODE.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
bulto = GBultoService.get_bulto_by_code(db, code)
if not bulto:
raise HTTPException(status_code=404, detail="Package not found")
return bulto
@router.post("/", response_model=GBultoResponseDTO, status_code=status.HTTP_201_CREATED)
async def create_gbulto(
bulto_data: GBultoCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new Package.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return GBultoService.create_gbulto(db, bulto_data)
@router.put("/{code}", response_model=GBultoResponseDTO)
async def update_bulto(
code: str,
bulto_data: GBultoUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing Package.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
bulto = GBultoService.update_bulto(db, code, bulto_data)
if not bulto:
raise HTTPException(status_code=404, detail="Package not found")
return bulto
@router.delete("/{code}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_bulto(
code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a Package by its CODE.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
bulto = GBultoService.delete_bulto(db, code)
if not bulto:
raise HTTPException(status_code=404, detail="Package not found")

View File

@@ -1,39 +1,110 @@
"""
Service layer for Packages (GBultos).
"""
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import dto, models from . import dto, models
class GBultoService: class PackageService:
""" """Service for Package CRUD operations with tenant support"""
Service layer for GBultos.
"""
@staticmethod @staticmethod
def get_bulto_by_code(db: Session, code: str): def get_all(
return db.query(models.Package).filter(models.Package.CODE == code).first() db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.Package], int]:
"""Get all packages for a tenant/company with pagination"""
query = db.query(models.Package).filter(
models.Package.tenant_id == tenant_id,
models.Package.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("key"):
query = query.filter(
models.Package.key.ilike(f"%{filters['key']}%")
)
if filters.get("description_es"):
query = query.filter(
models.Package.description_es.ilike(f"%{filters['description_es']}%")
)
total = query.count()
packages = query.offset(skip).limit(limit).all()
return packages, total
@staticmethod @staticmethod
def create_gbulto(db: Session, gbulto_data: dto.GBultoCreateDTO): def get_by_id(
new_gbulto = models.Package(**gbulto_data.dict()) db: Session, package_id: int, tenant_id: int, company_id: int
db.add(new_gbulto) ) -> Optional[models.Package]:
"""Get package by ID"""
return (
db.query(models.Package)
.filter(
models.Package.id == package_id,
models.Package.tenant_id == tenant_id,
models.Package.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
package_data: dto.PackageCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Package:
"""Create a new package"""
new_package = models.Package(
**package_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_package)
db.commit() db.commit()
db.refresh(new_gbulto) db.refresh(new_package)
return new_gbulto return new_package
@staticmethod @staticmethod
def update_bulto(db: Session, code: str, bulto_data: dto.GBultoUpdateDTO): def update(
bulto = GBultoService.get_bulto_by_code(db, code) db: Session,
if bulto: package_id: int,
for key, value in bulto_data.dict(exclude_unset=True).items(): tenant_id: int,
setattr(bulto, key, value) company_id: int,
db.commit() package_data: dto.PackageUpdateDTO,
db.refresh(bulto) ) -> Optional[models.Package]:
return bulto """Update a package"""
package = PackageService.get_by_id(db, package_id, tenant_id, company_id)
if not package:
return None
# Update fields (excluding key if it's meant to be immutable)
update_data = package_data.model_dump(exclude_unset=True, exclude={"key"})
for field, value in update_data.items():
setattr(package, field, value)
db.commit()
db.refresh(package)
return package
@staticmethod @staticmethod
def delete_bulto(db: Session, code: str): def delete(
bulto = GBultoService.get_bulto_by_code(db, code) db: Session, package_id: int, tenant_id: int, company_id: int
if bulto: ) -> bool:
db.delete(bulto) """Delete a package"""
db.commit() package = PackageService.get_by_id(db, package_id, tenant_id, company_id)
return bulto if not package:
return False
db.delete(package)
db.commit()
return True

View File

@@ -3,7 +3,7 @@ from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from backend.api.v1.common.dto_mixins import UpdateFlagsMixin from api.v1.common.dto_mixins import UpdateFlagsMixin
class PedimentoConfigUpdatesBase(BaseModel, UpdateFlagsMixin): class PedimentoConfigUpdatesBase(BaseModel, UpdateFlagsMixin):

View File

@@ -1,20 +1,31 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class PermissionRuleOctBaseDTO(BaseModel): class PermissionRuleOctBaseDTO(BaseModel):
permission: str permission: str = Field(..., description="Permission identifier", max_length=20)
start_date: Optional[int] start_date: Optional[int] = Field(None, description="Start date")
end_date: Optional[int] end_date: Optional[int] = Field(None, description="End date")
sector: Optional[str] sector: Optional[str] = Field(None, max_length=8, description="Sector")
system: Optional[str] system: Optional[str] = Field(None, max_length=5, description="System")
class PermissionRuleOctCreateDTO(PermissionRuleOctBaseDTO): class PermissionRuleOctCreateDTO(PermissionRuleOctBaseDTO):
"""Schema for creating a permission rule OCT"""
pass pass
class PermissionRuleOctUpdateDTO(PermissionRuleOctBaseDTO):
"""Schema for updating a permission rule OCT"""
permission: Optional[str] = Field(None, description="Permission identifier", max_length=20)
class PermissionRuleOctResponseDTO(PermissionRuleOctBaseDTO): class PermissionRuleOctResponseDTO(PermissionRuleOctBaseDTO):
"""Schema for permission rule OCT response"""
id: int
company_id: int
tenant_id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,102 +1,24 @@
from typing import List from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.database import get_core_db from .dto import (
from core.security import get_current_user PermissionRuleOctCreateDTO,
from fastapi import APIRouter, Depends, HTTPException, status PermissionRuleOctResponseDTO,
from sqlalchemy.orm import Session PermissionRuleOctUpdateDTO,
)
from .dto import PermissionRuleOctCreateDTO, PermissionRuleOctResponseDTO
from .services import PermissionRuleOctService from .services import PermissionRuleOctService
router = APIRouter(prefix="/permission-rule-oct", tags=["PermissionRuleOct"]) # Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
service=PermissionRuleOctService,
@router.get("/", response_model=List[PermissionRuleOctResponseDTO]) create_schema=PermissionRuleOctCreateDTO,
async def list_permissions( update_schema=PermissionRuleOctUpdateDTO,
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) response_schema=PermissionRuleOctResponseDTO,
): prefix="/permission-rule-oct",
""" tags=[],
List all PermissionRuleOct entries. resource_name="Permission Rule OCT",
""" id_name="id", # Using numeric ID
# Validate access to the tenant and company enable_list=True, # Enable GET /permission-rule-oct with pagination
tenant_id = current_user.get("tenant_id") enable_filters=True, # Enable filtering by permission, sector, system
company_id = current_user.get("company_id") default_page_size=50,
max_page_size=100,
if not tenant_id or not company_id: ).router
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(PermissionRuleOctService).all()
@router.get("/{permission}", response_model=PermissionRuleOctResponseDTO)
async def read_permission(
permission: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific PermissionRuleOct by its permission.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
permission = PermissionRuleOctService.get_permission_by_id(db, permission)
if not permission:
raise HTTPException(status_code=404, detail="PermissionRuleOct not found")
return permission
@router.post(
"/",
response_model=PermissionRuleOctResponseDTO,
status_code=status.HTTP_201_CREATED,
)
async def create_permission(
permission_data: PermissionRuleOctCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new PermissionRuleOct entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return PermissionRuleOctService.create_permission(db, permission_data)
@router.delete("/{permission}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_permission(
permission: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a PermissionRuleOct by its permission.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
permission = PermissionRuleOctService.delete_permission(db, permission)
if not permission:
raise HTTPException(status_code=404, detail="PermissionRuleOct not found")

View File

@@ -1,29 +1,114 @@
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import dto, models from . import dto, models
class PermissionRuleOctService: class PermissionRuleOctService:
"""Service for PermissionRuleOct CRUD operations with tenant support"""
@staticmethod @staticmethod
def get_permission_by_id(db: Session, permission: str): def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.PermissionRuleOct], int]:
"""Get all permission rules OCT for a tenant/company with pagination"""
query = db.query(models.PermissionRuleOct).filter(
models.PermissionRuleOct.tenant_id == tenant_id,
models.PermissionRuleOct.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("permission"):
query = query.filter(
models.PermissionRuleOct.permission.ilike(f"%{filters['permission']}%")
)
if filters.get("sector"):
query = query.filter(
models.PermissionRuleOct.sector == filters["sector"]
)
if filters.get("system"):
query = query.filter(
models.PermissionRuleOct.system == filters["system"]
)
total = query.count()
permissions = query.offset(skip).limit(limit).all()
return permissions, total
@staticmethod
def get_by_id(
db: Session, permission_id: int, tenant_id: int, company_id: int
) -> Optional[models.PermissionRuleOct]:
"""Get permission rule OCT by ID"""
return ( return (
db.query(models.PermissionRuleOct) db.query(models.PermissionRuleOct)
.filter(models.PermissionRuleOct.permission == permission) .filter(
models.PermissionRuleOct.id == permission_id,
models.PermissionRuleOct.tenant_id == tenant_id,
models.PermissionRuleOct.company_id == company_id,
)
.first() .first()
) )
@staticmethod @staticmethod
def create_permission(db: Session, permission_data: dto.PermissionRuleOctCreateDTO): def create(
new_permission = models.PermissionRuleOct(**permission_data.dict()) db: Session,
permission_data: dto.PermissionRuleOctCreateDTO,
tenant_id: int,
company_id: int,
) -> models.PermissionRuleOct:
"""Create a new permission rule OCT"""
new_permission = models.PermissionRuleOct(
**permission_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_permission) db.add(new_permission)
db.commit() db.commit()
db.refresh(new_permission) db.refresh(new_permission)
return new_permission return new_permission
@staticmethod @staticmethod
def delete_permission(db: Session, permission: str): def update(
permission = PermissionRuleOctService.get_permission_by_id(db, permission) db: Session,
if permission: permission_id: int,
db.delete(permission) tenant_id: int,
db.commit() company_id: int,
permission_data: dto.PermissionRuleOctUpdateDTO,
) -> Optional[models.PermissionRuleOct]:
"""Update a permission rule OCT"""
permission = PermissionRuleOctService.get_by_id(
db, permission_id, tenant_id, company_id
)
if not permission:
return None
# Update fields
update_data = permission_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(permission, field, value)
db.commit()
db.refresh(permission)
return permission return permission
@staticmethod
def delete(
db: Session, permission_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a permission rule OCT"""
permission = PermissionRuleOctService.get_by_id(
db, permission_id, tenant_id, company_id
)
if not permission:
return False
db.delete(permission)
db.commit()
return True

View File

@@ -2,17 +2,30 @@
DTOs for Seal. DTOs for Seal.
""" """
from pydantic import BaseModel from typing import Optional
from pydantic import BaseModel, Field
class SealBaseDTO(BaseModel): class SealBaseDTO(BaseModel):
seal: str seal: str = Field(..., description="Seal identifier", max_length=15)
class SealCreateDTO(SealBaseDTO): class SealCreateDTO(SealBaseDTO):
"""Schema for creating a seal"""
pass pass
class SealUpdateDTO(SealBaseDTO):
"""Schema for updating a seal"""
seal: Optional[str] = Field(None, description="Seal identifier", max_length=15)
class SealResponseDTO(SealBaseDTO): class SealResponseDTO(SealBaseDTO):
"""Schema for seal response"""
id: int
company_id: int
tenant_id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -2,101 +2,23 @@
Routes for managing Seal entries. Routes for managing Seal entries.
""" """
from typing import List from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.database import get_core_db from .dto import SealCreateDTO, SealResponseDTO, SealUpdateDTO
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import SealCreateDTO, SealResponseDTO
from .services import SealService from .services import SealService
router = APIRouter(prefix="/seals", tags=["Seal"]) # Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
service=SealService,
@router.get("/", response_model=List[SealResponseDTO]) create_schema=SealCreateDTO,
async def list_seals( update_schema=SealUpdateDTO,
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) response_schema=SealResponseDTO,
): prefix="/seals",
""" tags=[],
List all Seal entries. resource_name="Seal",
""" id_name="id", # Using numeric ID
# Validate access to the tenant and company enable_list=True, # Enable GET /seals with pagination
tenant_id = current_user.get("tenant_id") enable_filters=True, # Enable filtering by seal
company_id = current_user.get("company_id") default_page_size=50,
max_page_size=100,
if not tenant_id or not company_id: ).router
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(SealService).all()
@router.get("/{seal}", response_model=SealResponseDTO)
async def read_seal(
seal: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific Seal by its seal.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
seal = SealService.get_seal_by_id(db, seal)
if not seal:
raise HTTPException(status_code=404, detail="Seal not found")
return seal
@router.post("/", response_model=SealResponseDTO, status_code=status.HTTP_201_CREATED)
async def create_seal(
seal_data: SealCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new Seal entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return SealService.create_seal(db, seal_data)
@router.delete("/{seal}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_seal(
seal: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a Seal by its seal.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
seal = SealService.delete_seal(db, seal)
if not seal:
raise HTTPException(status_code=404, detail="Seal not found")

View File

@@ -1,29 +1,106 @@
from sqlalchemy.orm import Session
from . import dto, models
""" """
Service layer for Seal. Service layer for Seal.
""" """
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session
from . import dto, models
class SealService: class SealService:
@staticmethod """Service for Seal CRUD operations with tenant support"""
def get_seal_by_id(db: Session, seal: str):
return db.query(models.Seal).filter(models.Seal.seal == seal).first()
@staticmethod @staticmethod
def create_seal(db: Session, seal_data: dto.SealCreateDTO): def get_all(
new_seal = models.Seal(**seal_data.dict()) db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.Seal], int]:
"""Get all seals for a tenant/company with pagination"""
query = db.query(models.Seal).filter(
models.Seal.tenant_id == tenant_id,
models.Seal.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("seal"):
query = query.filter(
models.Seal.seal.ilike(f"%{filters['seal']}%")
)
total = query.count()
seals = query.offset(skip).limit(limit).all()
return seals, total
@staticmethod
def get_by_id(
db: Session, seal_id: int, tenant_id: int, company_id: int
) -> Optional[models.Seal]:
"""Get seal by ID"""
return (
db.query(models.Seal)
.filter(
models.Seal.id == seal_id,
models.Seal.tenant_id == tenant_id,
models.Seal.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
seal_data: dto.SealCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Seal:
"""Create a new seal"""
new_seal = models.Seal(
**seal_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_seal) db.add(new_seal)
db.commit() db.commit()
db.refresh(new_seal) db.refresh(new_seal)
return new_seal return new_seal
@staticmethod @staticmethod
def delete_seal(db: Session, seal: str): def update(
seal = SealService.get_seal_by_id(db, seal) db: Session,
if seal: seal_id: int,
db.delete(seal) tenant_id: int,
db.commit() company_id: int,
seal_data: dto.SealUpdateDTO,
) -> Optional[models.Seal]:
"""Update a seal"""
seal = SealService.get_by_id(db, seal_id, tenant_id, company_id)
if not seal:
return None
# Update fields
update_data = seal_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(seal, field, value)
db.commit()
db.refresh(seal)
return seal return seal
@staticmethod
def delete(
db: Session, seal_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a seal"""
seal = SealService.get_by_id(db, seal_id, tenant_id, company_id)
if not seal:
return False
db.delete(seal)
db.commit()
return True

View File

@@ -1,26 +1,34 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class TrailerBaseDTO(BaseModel): class TrailerBaseDTO(BaseModel):
trailer_number: str trailer_number: str = Field(..., description="Trailer number (primary identifier)")
ace_trailer_number: Optional[str] ace_trailer_number: Optional[str] = None
trailer_type_key: Optional[str] trailer_type_key: Optional[str] = None
seal: Optional[str] seal: Optional[str] = None
entity_code: Optional[str] entity_code: Optional[str] = None
plate_number: Optional[str] plate_number: Optional[str] = None
state: Optional[str] state: Optional[str] = None
country: Optional[str] country: Optional[str] = None
container_key: Optional[str] container_key: Optional[str] = None
company_id: str
tenant_id: str
class TrailerCreateDTO(TrailerBaseDTO): class TrailerCreateDTO(TrailerBaseDTO):
"""Schema for creating a trailer"""
pass pass
class TrailerUpdateDTO(TrailerBaseDTO):
"""Schema for updating a trailer"""
trailer_number: Optional[str] = Field(None, description="Trailer number (cannot be modified)")
class TrailerResponseDTO(TrailerBaseDTO): class TrailerResponseDTO(TrailerBaseDTO):
"""Schema for trailer response"""
company_id: int
tenant_id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,30 +1,22 @@
from core.database import get_core_db from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from . import dto, services from .dto import TrailerCreateDTO, TrailerResponseDTO, TrailerUpdateDTO
from .services import TrailerService
router = APIRouter() # Create router using TenantCRUDRoutes factory
# Note: trailer_number is a string (not int) and is used as the primary key
router = TenantCRUDRoutes(
@router.get("/trailers/{trailer_number}", response_model=dto.TrailerResponseDTO) service=TrailerService,
def get_trailer(trailer_number: str, db: Session = Depends(get_core_db)): create_schema=TrailerCreateDTO,
trailer = services.TrailerService.get_trailer_by_number(db, trailer_number) update_schema=TrailerUpdateDTO,
if not trailer: response_schema=TrailerResponseDTO,
raise HTTPException(status_code=404, detail="Trailer not found") prefix="/trailers",
return trailer tags=[],
resource_name="Trailer",
id_name="trailer_number", # Using trailer_number instead of numeric ID
@router.post("/trailers", response_model=dto.TrailerResponseDTO) id_type=str, # Specify that the ID is a string
def create_trailer( enable_list=True, # Enable GET /trailers with pagination
trailer_data: dto.TrailerCreateDTO, db: Session = Depends(get_core_db) enable_filters=True, # Enable filtering by plate_number and trailer_type_key
): default_page_size=50,
return services.TrailerService.create_trailer(db, trailer_data) max_page_size=100,
).router
@router.delete("/trailers/{trailer_number}", response_model=dto.TrailerResponseDTO)
def delete_trailer(trailer_number: str, db: Session = Depends(get_core_db)):
trailer = services.TrailerService.delete_trailer(db, trailer_number)
if not trailer:
raise HTTPException(status_code=404, detail="Trailer not found")
return trailer

View File

@@ -1,29 +1,108 @@
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import dto, models from . import dto, models
class TrailerService: class TrailerService:
"""Service for Trailer CRUD operations with tenant support"""
@staticmethod @staticmethod
def get_trailer_by_number(db: Session, trailer_number: str): def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.Trailer], int]:
"""Get all trailers for a tenant/company with pagination"""
query = db.query(models.Trailer).filter(
models.Trailer.tenant_id == tenant_id,
models.Trailer.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("plate_number"):
query = query.filter(
models.Trailer.plate_number.ilike(f"%{filters['plate_number']}%")
)
if filters.get("trailer_type_key"):
query = query.filter(
models.Trailer.trailer_type_key == filters["trailer_type_key"]
)
total = query.count()
trailers = query.offset(skip).limit(limit).all()
return trailers, total
@staticmethod
def get_by_id(
db: Session, trailer_number: str, tenant_id: int, company_id: int
) -> Optional[models.Trailer]:
"""Get trailer by trailer_number"""
return ( return (
db.query(models.Trailer) db.query(models.Trailer)
.filter(models.Trailer.trailer_number == trailer_number) .filter(
models.Trailer.trailer_number == trailer_number,
models.Trailer.tenant_id == tenant_id,
models.Trailer.company_id == company_id,
)
.first() .first()
) )
@staticmethod @staticmethod
def create_trailer(db: Session, trailer_data: dto.TrailerCreateDTO): def create(
new_trailer = models.Trailer(**trailer_data.dict()) db: Session,
trailer_data: dto.TrailerCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Trailer:
"""Create a new trailer"""
new_trailer = models.Trailer(
**trailer_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_trailer) db.add(new_trailer)
db.commit() db.commit()
db.refresh(new_trailer) db.refresh(new_trailer)
return new_trailer return new_trailer
@staticmethod @staticmethod
def delete_trailer(db: Session, trailer_number: str): def update(
trailer = TrailerService.get_trailer_by_number(db, trailer_number) db: Session,
if trailer: trailer_number: str,
db.delete(trailer) tenant_id: int,
db.commit() company_id: int,
trailer_data: dto.TrailerUpdateDTO,
) -> Optional[models.Trailer]:
"""Update a trailer"""
trailer = TrailerService.get_by_id(db, trailer_number, tenant_id, company_id)
if not trailer:
return None
# Update fields (excluding trailer_number as it's the primary key)
update_data = trailer_data.model_dump(
exclude_unset=True, exclude={"trailer_number"}
)
for field, value in update_data.items():
setattr(trailer, field, value)
db.commit()
db.refresh(trailer)
return trailer return trailer
@staticmethod
def delete(
db: Session, trailer_number: str, tenant_id: int, company_id: int
) -> bool:
"""Delete a trailer"""
trailer = TrailerService.get_by_id(db, trailer_number, tenant_id, company_id)
if not trailer:
return False
db.delete(trailer)
db.commit()
return True

View File

@@ -1,36 +1,44 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class TransporterBaseDTO(BaseModel): class TransporterBaseDTO(BaseModel):
transporter_key: str transporter_key: str = Field(..., description="Transporter key (primary identifier)")
name: Optional[str] name: Optional[str] = None
short_name: Optional[str] short_name: Optional[str] = None
responsible: Optional[str] responsible: Optional[str] = None
rfc: Optional[str] rfc: Optional[str] = None
streets: Optional[str] streets: Optional[str] = None
postal_code: Optional[str] postal_code: Optional[str] = None
city: Optional[str] city: Optional[str] = None
state: Optional[str] state: Optional[str] = None
country: Optional[str] country: Optional[str] = None
loader_code: Optional[str] loader_code: Optional[str] = None
caat_code: Optional[str] caat_code: Optional[str] = None
transport_code: Optional[str] transport_code: Optional[str] = None
transport_interface_type: Optional[str] transport_interface_type: Optional[str] = None
ftp_server: Optional[str] ftp_server: Optional[str] = None
ftp_user: Optional[str] ftp_user: Optional[str] = None
ftp_password: Optional[str] ftp_password: Optional[str] = None
ftp_directory: Optional[str] ftp_directory: Optional[str] = None
filler_code: Optional[str] filler_code: Optional[str] = None
company_id: str
tenant_id: str
class TransporterCreateDTO(TransporterBaseDTO): class TransporterCreateDTO(TransporterBaseDTO):
"""Schema for creating a transporter"""
pass pass
class TransporterUpdateDTO(TransporterBaseDTO):
"""Schema for updating a transporter"""
transporter_key: Optional[str] = Field(None, description="Transporter key (cannot be modified)")
class TransporterResponseDTO(TransporterBaseDTO): class TransporterResponseDTO(TransporterBaseDTO):
"""Schema for transporter response"""
company_id: int
tenant_id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,52 +1,22 @@
from typing import List from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.database import get_core_db from .dto import TransporterCreateDTO, TransporterResponseDTO, TransporterUpdateDTO
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import TransporterCreateDTO, TransporterResponseDTO
from .services import TransporterService from .services import TransporterService
router = APIRouter(prefix="/transporters", tags=["Transporters"]) # Create router using TenantCRUDRoutes factory
# Note: transporter_key is a string (not int) and is used as the primary key
router = TenantCRUDRoutes(
@router.get("/", response_model=List[TransporterResponseDTO]) service=TransporterService,
async def list_transporters( create_schema=TransporterCreateDTO,
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) update_schema=TransporterUpdateDTO,
): response_schema=TransporterResponseDTO,
return db.query(TransporterService).all() prefix="/transporters",
tags=[],
resource_name="Transporter",
@router.get("/{transporter_key}", response_model=TransporterResponseDTO) id_name="transporter_key", # Using transporter_key instead of numeric ID
async def read_transporter( id_type=str, # Specify that the ID is a string
transporter_key: str, enable_list=True, # Enable GET /transporters with pagination
db: Session = Depends(get_core_db), enable_filters=True, # Enable filtering by name and rfc
current_user: dict = Depends(get_current_user), default_page_size=50,
): max_page_size=100,
transporter = TransporterService.get_transporter_by_key(db, transporter_key) ).router
if not transporter:
raise HTTPException(status_code=404, detail="Transporter not found")
return transporter
@router.post(
"/", response_model=TransporterResponseDTO, status_code=status.HTTP_201_CREATED
)
async def create_transporter(
transporter_data: TransporterCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
return TransporterService.create_transporter(db, transporter_data)
@router.delete("/{transporter_key}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_transporter(
transporter_key: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
transporter = TransporterService.delete_transporter(db, transporter_key)
if not transporter:
raise HTTPException(status_code=404, detail="Transporter not found")

View File

@@ -1,29 +1,112 @@
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import dto, models from . import dto, models
class TransporterService: class TransporterService:
"""Service for Transporter CRUD operations with tenant support"""
@staticmethod @staticmethod
def get_transporter_by_key(db: Session, transporter_key: str): def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.Transporter], int]:
"""Get all transporters for a tenant/company with pagination"""
query = db.query(models.Transporter).filter(
models.Transporter.tenant_id == tenant_id,
models.Transporter.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("name"):
query = query.filter(
models.Transporter.name.ilike(f"%{filters['name']}%")
)
if filters.get("rfc"):
query = query.filter(
models.Transporter.rfc.ilike(f"%{filters['rfc']}%")
)
total = query.count()
transporters = query.offset(skip).limit(limit).all()
return transporters, total
@staticmethod
def get_by_id(
db: Session, transporter_key: str, tenant_id: int, company_id: int
) -> Optional[models.Transporter]:
"""Get transporter by transporter_key"""
return ( return (
db.query(models.Transporter) db.query(models.Transporter)
.filter(models.Transporter.transporter_key == transporter_key) .filter(
models.Transporter.transporter_key == transporter_key,
models.Transporter.tenant_id == tenant_id,
models.Transporter.company_id == company_id,
)
.first() .first()
) )
@staticmethod @staticmethod
def create_transporter(db: Session, transporter_data: dto.TransporterCreateDTO): def create(
new_transporter = models.Transporter(**transporter_data.dict()) db: Session,
transporter_data: dto.TransporterCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Transporter:
"""Create a new transporter"""
new_transporter = models.Transporter(
**transporter_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_transporter) db.add(new_transporter)
db.commit() db.commit()
db.refresh(new_transporter) db.refresh(new_transporter)
return new_transporter return new_transporter
@staticmethod @staticmethod
def delete_transporter(db: Session, transporter_key: str): def update(
transporter = TransporterService.get_transporter_by_key(db, transporter_key) db: Session,
if transporter: transporter_key: str,
db.delete(transporter) tenant_id: int,
db.commit() company_id: int,
transporter_data: dto.TransporterUpdateDTO,
) -> Optional[models.Transporter]:
"""Update a transporter"""
transporter = TransporterService.get_by_id(
db, transporter_key, tenant_id, company_id
)
if not transporter:
return None
# Update fields (excluding transporter_key as it's the primary key)
update_data = transporter_data.model_dump(
exclude_unset=True, exclude={"transporter_key"}
)
for field, value in update_data.items():
setattr(transporter, field, value)
db.commit()
db.refresh(transporter)
return transporter return transporter
@staticmethod
def delete(
db: Session, transporter_key: str, tenant_id: int, company_id: int
) -> bool:
"""Delete a transporter"""
transporter = TransporterService.get_by_id(
db, transporter_key, tenant_id, company_id
)
if not transporter:
return False
db.delete(transporter)
db.commit()
return True

View File

@@ -19,7 +19,7 @@ from .dto import (
) )
from .service import UserTenantService from .service import UserTenantService
router = APIRouter(prefix="/user-tenants", tags=["User-Tenant Relations"]) router = APIRouter(prefix="/user-tenants")
@router.post("/add", response_model=UserTenantResponseDTO) @router.post("/add", response_model=UserTenantResponseDTO)

View File

@@ -1,43 +1,51 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class VehicleBaseDTO(BaseModel): class VehicleBaseDTO(BaseModel):
vehicle_key: str vehicle_key: str = Field(..., description="Vehicle key (primary identifier)")
ace_vehicle_key: Optional[str] ace_vehicle_key: Optional[str] = None
transporter_key: Optional[str] transporter_key: Optional[str] = None
transport_identifier: Optional[str] transport_identifier: Optional[str] = None
transport_type: Optional[str] transport_type: Optional[str] = None
entity_code: Optional[str] entity_code: Optional[str] = None
transponder_number: Optional[str] transponder_number: Optional[str] = None
dot_number: Optional[str] dot_number: Optional[str] = None
plate_number: Optional[str] plate_number: Optional[str] = None
city: Optional[str] city: Optional[str] = None
state: Optional[str] state: Optional[str] = None
country: Optional[str] country: Optional[str] = None
seal: Optional[str] seal: Optional[str] = None
insurance_company_name: Optional[str] insurance_company_name: Optional[str] = None
insurance_number: Optional[str] insurance_number: Optional[str] = None
insurance_amount: Optional[float] insurance_amount: Optional[float] = None
insurance_date: Optional[int] insurance_date: Optional[int] = None
box_number: Optional[str] box_number: Optional[str] = None
brand: Optional[str] brand: Optional[str] = None
year: Optional[str] year: Optional[str] = None
series: Optional[str] series: Optional[str] = None
description: Optional[str] description: Optional[str] = None
engine_number: Optional[str] engine_number: Optional[str] = None
sct_permission: Optional[str] sct_permission: Optional[str] = None
color: Optional[str] color: Optional[str] = None
container_key: Optional[str] container_key: Optional[str] = None
company_id: str
tenant_id: str
class VehicleCreateDTO(VehicleBaseDTO): class VehicleCreateDTO(VehicleBaseDTO):
"""Schema for creating a vehicle"""
pass pass
class VehicleUpdateDTO(VehicleBaseDTO):
"""Schema for updating a vehicle"""
vehicle_key: Optional[str] = Field(None, description="Vehicle key (cannot be modified)")
class VehicleResponseDTO(VehicleBaseDTO): class VehicleResponseDTO(VehicleBaseDTO):
"""Schema for vehicle response"""
company_id: int
tenant_id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,52 +1,22 @@
from typing import List from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.database import get_core_db from .dto import VehicleCreateDTO, VehicleResponseDTO, VehicleUpdateDTO
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import VehicleCreateDTO, VehicleResponseDTO
from .services import VehicleService from .services import VehicleService
router = APIRouter(prefix="/vehicles", tags=["Vehicles"]) # Create router using TenantCRUDRoutes factory
# Note: vehicle_key is a string (not int) and is used as the primary key
router = TenantCRUDRoutes(
@router.get("/", response_model=List[VehicleResponseDTO]) service=VehicleService,
async def list_vehicles( create_schema=VehicleCreateDTO,
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) update_schema=VehicleUpdateDTO,
): response_schema=VehicleResponseDTO,
return db.query(VehicleService).all() prefix="/vehicles",
tags=[],
resource_name="Vehicle",
@router.get("/{vehicle_key}", response_model=VehicleResponseDTO) id_name="vehicle_key", # Using vehicle_key instead of numeric ID
async def read_vehicle( id_type=str, # Specify that the ID is a string
vehicle_key: str, enable_list=True, # Enable GET /vehicles with pagination
db: Session = Depends(get_core_db), enable_filters=True, # Enable filtering by plate_number and transport_type
current_user: dict = Depends(get_current_user), default_page_size=50,
): max_page_size=100,
vehicle = VehicleService.get_vehicle_by_key(db, vehicle_key) ).router
if not vehicle:
raise HTTPException(status_code=404, detail="Vehicle not found")
return vehicle
@router.post(
"/", response_model=VehicleResponseDTO, status_code=status.HTTP_201_CREATED
)
async def create_vehicle(
vehicle_data: VehicleCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
return VehicleService.create_vehicle(db, vehicle_data)
@router.delete("/{vehicle_key}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_vehicle(
vehicle_key: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
vehicle = VehicleService.delete_vehicle(db, vehicle_key)
if not vehicle:
raise HTTPException(status_code=404, detail="Vehicle not found")

View File

@@ -1,29 +1,106 @@
from typing import Optional, Tuple, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from . import dto, models from . import dto, models
class VehicleService: class VehicleService:
"""Service for Vehicle CRUD operations with tenant support"""
@staticmethod @staticmethod
def get_vehicle_by_key(db: Session, vehicle_key: str): def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.Vehicle], int]:
"""Get all vehicles for a tenant/company with pagination"""
query = db.query(models.Vehicle).filter(
models.Vehicle.tenant_id == tenant_id,
models.Vehicle.company_id == company_id,
)
# Apply filters if provided
if filters:
if filters.get("plate_number"):
query = query.filter(
models.Vehicle.plate_number.ilike(f"%{filters['plate_number']}%")
)
if filters.get("transport_type"):
query = query.filter(
models.Vehicle.transport_type == filters["transport_type"]
)
total = query.count()
vehicles = query.offset(skip).limit(limit).all()
return vehicles, total
@staticmethod
def get_by_id(
db: Session, vehicle_key: str, tenant_id: int, company_id: int
) -> Optional[models.Vehicle]:
"""Get vehicle by vehicle_key"""
return ( return (
db.query(models.Vehicle) db.query(models.Vehicle)
.filter(models.Vehicle.vehicle_key == vehicle_key) .filter(
models.Vehicle.vehicle_key == vehicle_key,
models.Vehicle.tenant_id == tenant_id,
models.Vehicle.company_id == company_id,
)
.first() .first()
) )
@staticmethod @staticmethod
def create_vehicle(db: Session, vehicle_data: dto.VehicleCreateDTO): def create(
new_vehicle = models.Vehicle(**vehicle_data.dict()) db: Session,
vehicle_data: dto.VehicleCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Vehicle:
"""Create a new vehicle"""
new_vehicle = models.Vehicle(
**vehicle_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_vehicle) db.add(new_vehicle)
db.commit() db.commit()
db.refresh(new_vehicle) db.refresh(new_vehicle)
return new_vehicle return new_vehicle
@staticmethod @staticmethod
def delete_vehicle(db: Session, vehicle_key: str): def update(
vehicle = VehicleService.get_vehicle_by_key(db, vehicle_key) db: Session,
if vehicle: vehicle_key: str,
db.delete(vehicle) tenant_id: int,
db.commit() company_id: int,
vehicle_data: dto.VehicleUpdateDTO,
) -> Optional[models.Vehicle]:
"""Update a vehicle"""
vehicle = VehicleService.get_by_id(db, vehicle_key, tenant_id, company_id)
if not vehicle:
return None
# Update fields (excluding vehicle_key as it's the primary key)
update_data = vehicle_data.model_dump(exclude_unset=True, exclude={"vehicle_key"})
for field, value in update_data.items():
setattr(vehicle, field, value)
db.commit()
db.refresh(vehicle)
return vehicle return vehicle
@staticmethod
def delete(
db: Session, vehicle_key: str, tenant_id: int, company_id: int
) -> bool:
"""Delete a vehicle"""
vehicle = VehicleService.get_by_id(db, vehicle_key, tenant_id, company_id)
if not vehicle:
return False
db.delete(vehicle)
db.commit()
return True