feat: Implement consolidated catalog services and endpoints for pedimento and invoice creation/edition.
This commit is contained in:
@@ -196,4 +196,33 @@ class ClassSearchDTO(BaseModel):
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ClassWithFADataResponse(BaseModel):
|
||||
"""DTO para respuesta de clase con datos FA embebidos (para fixed-asset-classes)"""
|
||||
|
||||
# Base class fields
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
class_code: str
|
||||
description_es: Optional[str] = None
|
||||
description_en: Optional[str] = None
|
||||
material_key: Optional[str] = None
|
||||
unit_of_measure: Optional[str] = None
|
||||
fraction: Optional[str] = None
|
||||
us_fraction: Optional[str] = None
|
||||
sub_key: Optional[str] = None
|
||||
physical_review: Optional[int] = None
|
||||
iva_exempt_fraction: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# FA-specific fields (embedded from a24.fa_classes)
|
||||
fa_class_id: Optional[int] = None
|
||||
depreciation_rate: Optional[Decimal] = None
|
||||
fda_code: Optional[str] = None
|
||||
class_enabled: Optional[bool] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -2,34 +2,53 @@
|
||||
Endpoints API para gestión de clases SCAII y SCAF
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from fastapi import Depends, Query
|
||||
from typing import Dict, Any, List
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
|
||||
from .dto import ClassCreateDTO, ClassCreateDTOFA, ClassResponseDTO, ClassResponseDTOFA, ClassUpdateDTO
|
||||
from .dto import ClassCreateDTO, ClassCreateDTOFA, ClassResponseDTO, ClassResponseDTOFA, ClassUpdateDTO, ClassWithFADataResponse
|
||||
from .service import ClassService
|
||||
|
||||
# Create router with generic CRUD routes
|
||||
crud_routes = TenantCRUDRoutes(
|
||||
service=ClassService,
|
||||
create_schema=ClassCreateDTO,
|
||||
update_schema=ClassUpdateDTO,
|
||||
response_schema=ClassResponseDTO,
|
||||
prefix="/classes",
|
||||
tags=["a76 / classes"],
|
||||
resource_name="Class",
|
||||
id_name="id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
default_page_size=50,
|
||||
max_page_size=1000,
|
||||
)
|
||||
# Create a new router for custom endpoints
|
||||
router = APIRouter()
|
||||
|
||||
router = crud_routes.router
|
||||
# Add consolidated catalog endpoints FIRST (before generic CRUD routes)
|
||||
# This ensures they have priority over the generic /{id} route
|
||||
@router.get(
|
||||
"/with-fa-data",
|
||||
response_model=List[ClassWithFADataResponse],
|
||||
summary="Get Classes with FA Data",
|
||||
description="Get all classes with their FA data in a single query (eliminates N+1 problem)",
|
||||
tags=["a76 / classes"],
|
||||
)
|
||||
async def get_classes_with_fa_data(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(1000, ge=1, le=1000, description="Page size"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get all classes with their FA data using a single LEFT JOIN query.
|
||||
This endpoint is optimized for the fixed-asset-classes view.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
classes_with_fa, total = ClassService.get_all_with_fa_data(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
skip=skip,
|
||||
limit=page_size,
|
||||
)
|
||||
|
||||
return classes_with_fa
|
||||
|
||||
@router.post(
|
||||
"/fa",
|
||||
@@ -37,6 +56,7 @@ router = crud_routes.router
|
||||
status_code=201,
|
||||
summary="Create Fixed Asset Class",
|
||||
description="Create a class with FA extension in a single transaction",
|
||||
tags=["a76 / classes"],
|
||||
)
|
||||
async def create_fa_class(
|
||||
class_data: ClassCreateDTOFA,
|
||||
@@ -50,4 +70,24 @@ async def create_fa_class(
|
||||
|
||||
result = ClassService.create_fa_class(db, class_data, tenant_id, company_id)
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
# Now include generic CRUD routes
|
||||
# These will be registered AFTER the custom endpoints above
|
||||
crud_router = TenantCRUDRoutes(
|
||||
service=ClassService,
|
||||
create_schema=ClassCreateDTO,
|
||||
update_schema=ClassUpdateDTO,
|
||||
response_schema=ClassResponseDTO,
|
||||
prefix="", # No prefix here, will be added in main router
|
||||
tags=["a76 / classes"],
|
||||
resource_name="Class",
|
||||
id_name="id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
default_page_size=50,
|
||||
max_page_size=1000,
|
||||
).router
|
||||
|
||||
# Include the CRUD routes into our main router
|
||||
router.include_router(crud_router)
|
||||
@@ -73,6 +73,91 @@ class ClassService:
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_all_with_fa_data(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 1000,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> tuple[List[Dict[str, Any]], int]:
|
||||
"""
|
||||
Get all classes with their FA data in a single query using LEFT JOIN.
|
||||
This eliminates the N+1 query problem.
|
||||
|
||||
Returns a list of dicts with combined base class + FA data.
|
||||
"""
|
||||
from api.v1.modules.a24.fa.fa_classes.models import QClasses
|
||||
|
||||
# Build query with LEFT JOIN
|
||||
query = (
|
||||
db.query(Class, QClasses)
|
||||
.outerjoin(QClasses, and_(
|
||||
Class.id == QClasses.class_id,
|
||||
QClasses.tenant_id == tenant_id
|
||||
))
|
||||
.filter(Class.tenant_id == tenant_id)
|
||||
.filter(Class.company_id == company_id)
|
||||
)
|
||||
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("class_code"):
|
||||
query = query.filter(
|
||||
Class.class_code.ilike(f"%{filters['class_code']}%")
|
||||
)
|
||||
if filters.get("description"):
|
||||
description_pattern = f"%{filters['description']}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Class.description_es.ilike(description_pattern),
|
||||
Class.description_en.ilike(description_pattern),
|
||||
)
|
||||
)
|
||||
if filters.get("material_key"):
|
||||
query = query.filter(
|
||||
Class.material_key.ilike(f"%{filters['material_key']}%")
|
||||
)
|
||||
if filters.get("fraction"):
|
||||
query = query.filter(Class.fraction.ilike(f"%{filters['fraction']}%"))
|
||||
|
||||
# Count total before pagination
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination
|
||||
results = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Combine base class + FA data into dicts
|
||||
combined = []
|
||||
for base_class, fa_class in results:
|
||||
class_dict = {
|
||||
# Base class fields
|
||||
"id": base_class.id,
|
||||
"tenant_id": base_class.tenant_id,
|
||||
"company_id": base_class.company_id,
|
||||
"class_code": base_class.class_code,
|
||||
"description_es": base_class.description_es,
|
||||
"description_en": base_class.description_en,
|
||||
"material_key": base_class.material_key,
|
||||
"unit_of_measure": base_class.unit_of_measure,
|
||||
"fraction": base_class.fraction,
|
||||
"us_fraction": base_class.us_fraction,
|
||||
"sub_key": base_class.sub_key,
|
||||
"physical_review": base_class.physical_review,
|
||||
"iva_exempt_fraction": base_class.iva_exempt_fraction,
|
||||
"created_at": base_class.created_at,
|
||||
"updated_at": base_class.updated_at,
|
||||
# FA extension fields (None if no FA record exists)
|
||||
"fa_class_id": fa_class.id if fa_class else None,
|
||||
"depreciation_rate": fa_class.depreciation_rate if fa_class else None,
|
||||
"fda_code": fa_class.fda_code if fa_class else None,
|
||||
"class_enabled": fa_class.class_enabled if fa_class else None,
|
||||
}
|
||||
combined.append(class_dict)
|
||||
|
||||
return combined, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, class_id: int, tenant_id: int, company_id: int
|
||||
|
||||
217
backend/api/v1/modules/a76/invoices/catalog_service.py
Normal file
217
backend/api/v1/modules/a76/invoices/catalog_service.py
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import Reference Data Models
|
||||
from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType
|
||||
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
|
||||
from api.v1.modules.public.reference_data.transport_types.models import TransportType
|
||||
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.models import CodePedimentoRegimen
|
||||
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
|
||||
from api.v1.modules.public.reference_data.transport_modes.models import TransportMode
|
||||
|
||||
# Import A76 Services
|
||||
from api.v1.modules.a76.customs_brokers.services import CustomsBrokerService
|
||||
from api.v1.modules.a76.clients_and_providers.service import ClientProviderService
|
||||
from api.v1.modules.a76.transportation.transporters.services import TransporterService
|
||||
from api.v1.modules.a76.transportation.vehicles.services import VehicleService
|
||||
from api.v1.modules.a76.transportation.drivers.services import DriverService
|
||||
from api.v1.modules.a76.transportation.trailers.services import TrailerService
|
||||
from api.v1.modules.a76.general_catalogs.seal.services import SealService
|
||||
from api.v1.modules.a76.pedmientos.services.pedimentos import PedimentosService
|
||||
|
||||
# Import DTOs for mapping
|
||||
from api.v1.modules.public.reference_data.invoice_types.dto import InvoiceTypeDTO
|
||||
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
|
||||
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
|
||||
from api.v1.modules.public.reference_data.currency_types.dto import CurrencyTypeDTO
|
||||
from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO
|
||||
from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSectionDTO
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
|
||||
from api.v1.modules.public.reference_data.incoterms.dto import IncotermDTO
|
||||
from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO
|
||||
|
||||
# Additional DTOs
|
||||
from api.v1.modules.a76.transportation.transporters.dto import TransporterResponseDTO
|
||||
from api.v1.modules.a76.transportation.vehicles.dto import VehicleResponseDTO
|
||||
from api.v1.modules.a76.transportation.drivers.dto import DriverResponseDTO
|
||||
from api.v1.modules.a76.transportation.trailers.dto import TrailerResponseDTO
|
||||
from api.v1.modules.a76.general_catalogs.seal.dto import SealResponseDTO
|
||||
from api.v1.modules.a76.pedmientos.dtos.pedimentos import PedimentosResponse
|
||||
|
||||
from .schemas import InvoiceCatalogsResponse, InvoiceCreationResponse, InvoiceEditionResponse
|
||||
|
||||
class InvoiceCatalogService:
|
||||
"""Service to fetch consolidated catalogs for Invoice views"""
|
||||
|
||||
@staticmethod
|
||||
def get_catalogs(db: Session, tenant_id: int, company_id: int) -> InvoiceCatalogsResponse:
|
||||
"""Fetch all catalogs"""
|
||||
|
||||
response = InvoiceCatalogsResponse()
|
||||
|
||||
# Helper to fetch reference data (no company_id needed)
|
||||
def fetch_ref_data():
|
||||
response.invoice_types = [
|
||||
InvoiceTypeDTO.model_validate(obj) for obj in db.query(InvoiceType).all()
|
||||
]
|
||||
response.currency_types = [
|
||||
CurrencyTypeDTO.model_validate(obj) for obj in db.query(CurrencyType).all()
|
||||
]
|
||||
response.transport_types = [
|
||||
TransportTypeDTO.model_validate(obj) for obj in db.query(TransportType).all()
|
||||
]
|
||||
response.customs_sections = [
|
||||
CustomsSectionDTO.model_validate(obj) for obj in db.query(CustomsSection).all()
|
||||
]
|
||||
response.code_pedimento_regimens = [
|
||||
CodePedimentoRegimenDTO.model_validate(obj) for obj in db.query(CodePedimentoRegimen).all()
|
||||
]
|
||||
response.incoterms = [
|
||||
IncotermDTO.model_validate(obj) for obj in db.query(Incoterm).all()
|
||||
]
|
||||
response.transport_modes = [
|
||||
TransportModeDTO.model_validate(obj) for obj in db.query(TransportMode).all()
|
||||
]
|
||||
|
||||
# Helper to fetch tenant/company specific data
|
||||
def fetch_tenant_data():
|
||||
# Customs Brokers
|
||||
try:
|
||||
brokers, _ = CustomsBrokerService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.customs_brokers = [
|
||||
CustomsBrokerResponseDTO.model_validate(obj) for obj in brokers
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching customs brokers: {e}")
|
||||
|
||||
# Clients and Providers
|
||||
try:
|
||||
# Fetch all clients/providers
|
||||
# Note: get_all returns list[ClientProvider]
|
||||
all_cps, _ = ClientProviderService.get_all(
|
||||
db, tenant_id, company_id, limit=2000
|
||||
)
|
||||
|
||||
# Helper to safely check client type (handles string or Enum)
|
||||
def is_type(obj, types):
|
||||
val = obj.client_or_provider
|
||||
# If it's an enum, get its value, otherwise use as string
|
||||
val_str = val.value if hasattr(val, 'value') else str(val)
|
||||
return val_str in types
|
||||
|
||||
response.clients = [
|
||||
ClientProviderResponseDTO.model_validate(obj) for obj in all_cps
|
||||
if is_type(obj, ['client', 'both'])
|
||||
]
|
||||
response.providers = [
|
||||
ClientProviderResponseDTO.model_validate(obj) for obj in all_cps
|
||||
if is_type(obj, ['provider', 'both'])
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching clients/providers: {e}")
|
||||
|
||||
|
||||
|
||||
# Transporters
|
||||
try:
|
||||
transporters, _ = TransporterService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.transporters = [
|
||||
TransporterResponseDTO.model_validate(t) for t in transporters
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching transporters: {e}")
|
||||
|
||||
# Vehicles
|
||||
try:
|
||||
vehicles, _ = VehicleService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.vehicles = [
|
||||
VehicleResponseDTO.model_validate(v) for v in vehicles
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching vehicles: {e}")
|
||||
|
||||
# Drivers
|
||||
try:
|
||||
drivers, _ = DriverService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.drivers = [
|
||||
DriverResponseDTO.model_validate(d) for d in drivers
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching drivers: {e}")
|
||||
|
||||
# Trailers
|
||||
try:
|
||||
trailers, _ = TrailerService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.trailers = [
|
||||
TrailerResponseDTO.model_validate(t) for t in trailers
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching trailers: {e}")
|
||||
|
||||
# Seals
|
||||
try:
|
||||
seals, _ = SealService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.seals = [
|
||||
SealResponseDTO.model_validate(s) for s in seals
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching seals: {e}")
|
||||
|
||||
# Pedimentos
|
||||
try:
|
||||
# Fetch recent pedimentos (e.g. last 100) or filtered if necessary
|
||||
pedimentos, _ = PedimentosService.get_all(db, tenant_id, company_id, limit=100)
|
||||
response.pedimentos = [
|
||||
# Use dict for now if PedimentosResponse fails due to complexity or just map fields manually if needed
|
||||
# But PedimentosResponse has from_attributes=True
|
||||
# Note: PedimentosResponse structure is complex with nested relations.
|
||||
# If Pedimentos model is fully loaded (eager load in service), this should work.
|
||||
# However, to be safe against recursion or huge payload, we might want a lighter DTO.
|
||||
# Re-using PedimentosResponse for now but be cautious of payload size.
|
||||
PedimentosResponse.model_validate(p) for p in pedimentos
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching pedimentos: {e}")
|
||||
|
||||
try:
|
||||
fetch_ref_data()
|
||||
fetch_tenant_data()
|
||||
except Exception as e:
|
||||
print(f"Error fetching catalogs: {e}")
|
||||
# In production, we might want to log this properly and potentially return partial data
|
||||
# For now, re-raising might be safer to debug, but for resiliency we could suppress.
|
||||
# Let's log and re-raise to ensure frontend knows something went wrong during dev.
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def get_creation_data(db: Session, tenant_id: int, company_id: int) -> InvoiceCreationResponse:
|
||||
catalogs = InvoiceCatalogService.get_catalogs(db, tenant_id, company_id)
|
||||
return InvoiceCreationResponse(
|
||||
**catalogs.model_dump(),
|
||||
is_create=True,
|
||||
filters={}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_edition_data(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> Optional[InvoiceEditionResponse]:
|
||||
catalogs = InvoiceCatalogService.get_catalogs(db, tenant_id, company_id)
|
||||
|
||||
from .services import InvoiceService
|
||||
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
|
||||
if not invoice:
|
||||
return None
|
||||
|
||||
return InvoiceEditionResponse(
|
||||
**catalogs.model_dump(),
|
||||
is_create=False,
|
||||
invoice=invoice,
|
||||
invoice_id=invoice_id,
|
||||
filters={}
|
||||
)
|
||||
@@ -6,10 +6,36 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import schemas, services
|
||||
from .catalog_service import InvoiceCatalogService
|
||||
|
||||
# Create main router
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/invoices/creation-data", response_model=schemas.InvoiceCreationResponse)
|
||||
def get_creation_data(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Get consolidated data for creating a new invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id)
|
||||
|
||||
@router.get("/invoices/{invoice_id}/edition-data", response_model=schemas.InvoiceEditionResponse)
|
||||
def get_edition_data(
|
||||
invoice_id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Get consolidated data for editing an existing invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
data = InvoiceCatalogService.get_edition_data(db, invoice_id, tenant_id, company_id)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
return data
|
||||
|
||||
|
||||
# Create CRUD routes for Invoice Header using TenantCRUDRoutes
|
||||
invoice_crud = TenantCRUDRoutes(
|
||||
service=services.InvoiceService,
|
||||
|
||||
@@ -568,4 +568,61 @@ class InvoiceHeaderListResponse(BaseModel):
|
||||
items: List[InvoiceHeaderResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
# --- Consolidated Response Schemas ---
|
||||
|
||||
# Import necessary DTOs from other modules
|
||||
from api.v1.modules.public.reference_data.invoice_types.dto import InvoiceTypeDTO
|
||||
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
|
||||
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
|
||||
from api.v1.modules.public.reference_data.currency_types.dto import CurrencyTypeDTO
|
||||
from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO
|
||||
from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSectionDTO
|
||||
from api.v1.modules.public.reference_data.incoterms.dto import IncotermDTO
|
||||
from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
|
||||
|
||||
# TODO: Check if these paths are correct or need adjustment based on actual file locations
|
||||
# Using Any for now for potentially complex or unverified paths to avoid immediate ImportErrors
|
||||
# Detailed verification is needed for:
|
||||
# - TransporterDTO
|
||||
# - VehicleDTO
|
||||
# - DriverDTO
|
||||
# - TrailerDTO
|
||||
# - SealResponseDTO
|
||||
# - PedimentoDTO
|
||||
|
||||
class InvoiceCatalogsResponse(BaseModel):
|
||||
"""Consolidated response for all catalogs needed in Invoice Create/Edit views"""
|
||||
invoice_types: List[InvoiceTypeDTO] = []
|
||||
customs_brokers: List[CustomsBrokerResponseDTO] = []
|
||||
clients: List[ClientProviderResponseDTO] = []
|
||||
providers: List[ClientProviderResponseDTO] = []
|
||||
currency_types: List[CurrencyTypeDTO] = []
|
||||
transport_types: List[TransportTypeDTO] = []
|
||||
transporters: List[dict] = [] # Placeholder, refine with actual DTO
|
||||
vehicles: List[dict] = [] # Placeholder, refine with actual DTO
|
||||
drivers: List[dict] = [] # Placeholder, refine with actual DTO
|
||||
trailers: List[dict] = [] # Placeholder, refine with actual DTO
|
||||
customs_sections: List[CustomsSectionDTO] = []
|
||||
code_pedimento_regimens: List[CodePedimentoRegimenDTO] = []
|
||||
seals: List[dict] = [] # Placeholder, refine with actual DTO
|
||||
incoterms: List[IncotermDTO] = []
|
||||
pedimentos: List[dict] = [] # Placeholder, refine with actual DTO
|
||||
transport_modes: List[TransportModeDTO] = []
|
||||
default_settings: Optional[dict] = None
|
||||
|
||||
class InvoiceCreationResponse(InvoiceCatalogsResponse):
|
||||
"""Response for Invoice Creation View"""
|
||||
is_create: bool = True
|
||||
invoice: Optional[dict] = None # Should be null for creation
|
||||
invoice_id: Optional[int] = None
|
||||
filters: Optional[dict] = None # Pre-filled filters if any
|
||||
|
||||
class InvoiceEditionResponse(InvoiceCatalogsResponse):
|
||||
"""Response for Invoice Edition View"""
|
||||
is_create: bool = False
|
||||
invoice: InvoiceHeaderResponse
|
||||
invoice_id: int
|
||||
filters: Optional[dict] = None
|
||||
|
||||
|
||||
120
backend/api/v1/modules/a76/pedmientos/catalog_service.py
Normal file
120
backend/api/v1/modules/a76/pedmientos/catalog_service.py
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import Reference Data Models
|
||||
from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode
|
||||
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.models import CodePedimentoRegimen
|
||||
|
||||
# Import A76 Services
|
||||
from api.v1.modules.a76.customs_brokers.services import CustomsBrokerService
|
||||
from api.v1.modules.a76.clients_and_providers.service import ClientProviderService
|
||||
|
||||
# Import DTOs for mapping
|
||||
from api.v1.modules.public.reference_data.pedimento_codes.dto import PedimentoCodeDTO
|
||||
from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSectionDTO
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
|
||||
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
|
||||
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
|
||||
|
||||
from .schemas import PedimentoCatalogsResponse, PedimentoCreationResponse, PedimentoEditionResponse
|
||||
|
||||
class PedimentoCatalogService:
|
||||
"""Service to fetch consolidated catalogs for Pedimento views"""
|
||||
|
||||
@staticmethod
|
||||
def get_catalogs(db: Session, tenant_id: int, company_id: int) -> PedimentoCatalogsResponse:
|
||||
"""Fetch all catalogs with graceful degradation"""
|
||||
|
||||
response = PedimentoCatalogsResponse()
|
||||
|
||||
# Helper to fetch reference data (no company_id needed)
|
||||
def fetch_ref_data():
|
||||
try:
|
||||
response.pedimento_codes = [
|
||||
PedimentoCodeDTO.model_validate(obj) for obj in db.query(PedimentoCode).limit(100).all()
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching pedimento_codes: {e}")
|
||||
|
||||
try:
|
||||
response.customs_sections = [
|
||||
CustomsSectionDTO.model_validate(obj) for obj in db.query(CustomsSection).limit(100).all()
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching customs_sections: {e}")
|
||||
|
||||
try:
|
||||
response.code_pedimento_regimens = [
|
||||
CodePedimentoRegimenDTO.model_validate(obj) for obj in db.query(CodePedimentoRegimen).limit(100).all()
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching code_pedimento_regimens: {e}")
|
||||
|
||||
# Helper to fetch tenant/company specific data
|
||||
def fetch_tenant_data():
|
||||
# Customs Brokers
|
||||
try:
|
||||
brokers, _ = CustomsBrokerService.get_all(db, tenant_id, company_id, limit=1000)
|
||||
response.customs_brokers = [
|
||||
CustomsBrokerResponseDTO.model_validate(obj) for obj in brokers
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching customs brokers: {e}")
|
||||
|
||||
# Clients (only clients, not providers)
|
||||
try:
|
||||
all_cps, _ = ClientProviderService.get_all(
|
||||
db, tenant_id, company_id, limit=1000
|
||||
)
|
||||
|
||||
# Helper to safely check client type (handles string or Enum)
|
||||
def is_type(obj, types):
|
||||
val = obj.client_or_provider
|
||||
# If it's an enum, get its value, otherwise use as string
|
||||
val_str = val.value if hasattr(val, 'value') else str(val)
|
||||
return val_str in types
|
||||
|
||||
response.clients = [
|
||||
ClientProviderResponseDTO.model_validate(obj) for obj in all_cps
|
||||
if is_type(obj, ['client', 'both'])
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching clients: {e}")
|
||||
|
||||
try:
|
||||
fetch_ref_data()
|
||||
fetch_tenant_data()
|
||||
except Exception as e:
|
||||
print(f"Error fetching catalogs: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def get_creation_data(db: Session, tenant_id: int, company_id: int) -> PedimentoCreationResponse:
|
||||
catalogs = PedimentoCatalogService.get_catalogs(db, tenant_id, company_id)
|
||||
return PedimentoCreationResponse(
|
||||
**catalogs.model_dump(),
|
||||
is_create=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_edition_data(db: Session, pedimento_id: int, tenant_id: int, company_id: int) -> PedimentoEditionResponse:
|
||||
catalogs = PedimentoCatalogService.get_catalogs(db, tenant_id, company_id)
|
||||
|
||||
from .services.pedimentos import PedimentosService
|
||||
pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id, company_id)
|
||||
|
||||
if not pedimento:
|
||||
return None
|
||||
|
||||
return PedimentoEditionResponse(
|
||||
**catalogs.model_dump(),
|
||||
is_create=False,
|
||||
pedimento=pedimento,
|
||||
pedimento_id=pedimento_id
|
||||
)
|
||||
@@ -2,13 +2,76 @@
|
||||
Routes for Pedimentos CRUD operations
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
|
||||
from ..dtos.pedimentos import PedimentosCreate, PedimentosResponse, PedimentosUpdate
|
||||
from ..services.pedimentos import PedimentosService
|
||||
from ..catalog_service import PedimentoCatalogService
|
||||
from ..schemas import PedimentoCreationResponse, PedimentoEditionResponse
|
||||
|
||||
# Create router with generic CRUD routes
|
||||
router = TenantCRUDRoutes(
|
||||
# Create a new router for custom endpoints
|
||||
router = APIRouter()
|
||||
|
||||
# Add consolidated catalog endpoints FIRST (before generic CRUD routes)
|
||||
# This ensures they have priority over the generic /{id} route
|
||||
@router.get("/creation-data", response_model=PedimentoCreationResponse, tags=["a76 / pedimentos"])
|
||||
async def get_creation_data(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all catalogs needed for creating a new pedimento.
|
||||
Consolidates multiple catalog calls into a single endpoint.
|
||||
"""
|
||||
tenant_id = current_user["tenant_id"]
|
||||
|
||||
try:
|
||||
return PedimentoCatalogService.get_creation_data(db, tenant_id, company_id)
|
||||
except Exception as e:
|
||||
print(f"Error fetching creation data: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail="Error fetching creation data")
|
||||
|
||||
|
||||
@router.get("/{pedimento_id}/edition-data", response_model=PedimentoEditionResponse, tags=["a76 / pedimentos"])
|
||||
async def get_edition_data(
|
||||
pedimento_id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all catalogs and pedimento data needed for editing an existing pedimento.
|
||||
Consolidates multiple catalog calls + pedimento fetch into a single endpoint.
|
||||
"""
|
||||
tenant_id = current_user["tenant_id"]
|
||||
|
||||
try:
|
||||
result = PedimentoCatalogService.get_edition_data(db, pedimento_id, tenant_id, company_id)
|
||||
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Pedimento not found")
|
||||
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Error fetching edition data: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail="Error fetching edition data")
|
||||
|
||||
|
||||
# Now include generic CRUD routes
|
||||
# These will be registered AFTER the custom endpoints above
|
||||
crud_router = TenantCRUDRoutes(
|
||||
service=PedimentosService,
|
||||
create_schema=PedimentosCreate,
|
||||
update_schema=PedimentosUpdate,
|
||||
@@ -22,3 +85,6 @@ router = TenantCRUDRoutes(
|
||||
default_page_size=50,
|
||||
max_page_size=100,
|
||||
).router
|
||||
|
||||
# Include the CRUD routes into our main router
|
||||
router.include_router(crud_router)
|
||||
|
||||
37
backend/api/v1/modules/a76/pedmientos/schemas.py
Normal file
37
backend/api/v1/modules/a76/pedmientos/schemas.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Consolidated schemas for Pedimento catalog responses
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Any
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import DTOs for catalog items
|
||||
from api.v1.modules.public.reference_data.pedimento_codes.dto import PedimentoCodeDTO
|
||||
from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSectionDTO
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
|
||||
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
|
||||
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
|
||||
|
||||
|
||||
class PedimentoCatalogsResponse(BaseModel):
|
||||
"""Base response containing all catalogs needed for pedimento views"""
|
||||
|
||||
pedimento_codes: List[PedimentoCodeDTO] = []
|
||||
customs_sections: List[CustomsSectionDTO] = []
|
||||
code_pedimento_regimens: List[CodePedimentoRegimenDTO] = []
|
||||
customs_brokers: List[CustomsBrokerResponseDTO] = []
|
||||
clients: List[ClientProviderResponseDTO] = []
|
||||
|
||||
|
||||
class PedimentoCreationResponse(PedimentoCatalogsResponse):
|
||||
"""Response for creating a new pedimento (catalogs only)"""
|
||||
|
||||
is_create: bool = True
|
||||
|
||||
|
||||
class PedimentoEditionResponse(PedimentoCatalogsResponse):
|
||||
"""Response for editing an existing pedimento (catalogs + pedimento data)"""
|
||||
|
||||
is_create: bool = False
|
||||
pedimento: Optional[Any] = None # Will be PedimentosResponse but avoiding circular import
|
||||
pedimento_id: Optional[int] = None
|
||||
@@ -75,7 +75,7 @@ router.include_router(
|
||||
client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"]
|
||||
)
|
||||
router.include_router(company_router, prefix="/a76", tags=["a76 / company"])
|
||||
router.include_router(classes_router, prefix="/a76", tags=["a76 / classes"])
|
||||
router.include_router(classes_router, prefix="/a76/classes", tags=["a76 / classes"])
|
||||
router.include_router(parts_router, prefix="/a76", tags=["a76 / parts"])
|
||||
router.include_router(
|
||||
permission_rule_oct_router, prefix="/a76", tags=["a76 / permission_rule_oct"]
|
||||
|
||||
Reference in New Issue
Block a user