Merge pull request 'feature/SSR-views' (#137) from feature/SSR-views into development
Reviewed-on: ADUANASOFT/anexo76#137
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
|
||||
@@ -51,7 +51,7 @@ router.include_router(invoice_settings_router)
|
||||
router.include_router(item_presets_router, prefix="/a76/item-presets", tags=["a76 / item_presets"])
|
||||
router.include_router(pedimentos_router, prefix="/a76")
|
||||
router.include_router(client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"])
|
||||
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"])
|
||||
router.include_router(fraction_rule_octave_router, prefix="/a76", tags=["a76 / fraction_rule_octave"])
|
||||
|
||||
@@ -101,5 +101,21 @@ export const classesApi = {
|
||||
*/
|
||||
createFA: (data: any, company_id: number): Promise<ApiResponse<any>> => {
|
||||
return api.post(`/v1/a76/classes/fa?company_id=${company_id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all classes with FA data in a single query (optimized, eliminates N+1)
|
||||
*/
|
||||
getWithFAData: (params: {
|
||||
company_id: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<ApiResponse<A76Class[]>> => {
|
||||
const query = new URLSearchParams({
|
||||
company_id: params.company_id.toString(),
|
||||
page: (params.page || 1).toString(),
|
||||
page_size: (params.page_size || 1000).toString()
|
||||
});
|
||||
return api.get(`/v1/a76/classes/with-fa-data?${query.toString()}`);
|
||||
}
|
||||
};
|
||||
@@ -392,6 +392,26 @@ export const invoicesApi = {
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene datos para la creación de una factura (catálogos consolidados)
|
||||
*/
|
||||
getCreationData: (companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<any>(`/v1/a76/invoices/creation-data?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene datos para la edición de una factura (catálogos consolidados + factura)
|
||||
*/
|
||||
getEditionData: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<any>(`/v1/a76/invoices/${invoiceId}/edition-data?${params.toString()}`);
|
||||
},
|
||||
|
||||
// --- Nested Resources ---
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,358 +26,110 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
||||
parsedOperationType = operationTypeParam;
|
||||
}
|
||||
|
||||
// Cargar datos de referencia necesarios
|
||||
const invoiceTypesPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/invoice-types/?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
try {
|
||||
let invoiceData: any = null;
|
||||
let catalogsData: any = {};
|
||||
let defaultSettings: any = null;
|
||||
let isCreate = false;
|
||||
|
||||
const customsBrokersPromise = authenticatedFetch(
|
||||
`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
// Fetch Default Settings separately if applicable (for creation)
|
||||
const settingsPromise = (params.id === 'new' && parsedOperationType && invoiceTypeParam && companyId)
|
||||
? authenticatedFetch(
|
||||
`v1/a76/invoice-settings/${invoiceTypeParam}?operation_type=${parsedOperationType}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
).then(res => res.ok ? res.json() : null).catch(err => {
|
||||
console.error('Error fetching defaults:', err);
|
||||
return null;
|
||||
})
|
||||
: Promise.resolve(null);
|
||||
|
||||
// Cargar clientes y proveedores
|
||||
const clientsPromise = authenticatedFetch(
|
||||
`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const providersPromise = authenticatedFetch(
|
||||
`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Cargar tipos de moneda, transporte, etc.
|
||||
const currencyTypesPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/currency-types/?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const transportTypesPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/transport-types/?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const transportersPromise = authenticatedFetch(
|
||||
`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const vehiclesPromise = authenticatedFetch(
|
||||
`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const driversPromise = authenticatedFetch(
|
||||
`v1/a76/transportation/drivers/?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const trailersPromise = authenticatedFetch(
|
||||
`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const customsSectionsPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/customs-sections/?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const codePedimentoRegimensPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/code-pedimento-regimens/?page=1&page_size=1000',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const sealsPromise = authenticatedFetch(
|
||||
`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const incotermsPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/incoterms/?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const pedimentosPromise = authenticatedFetch(
|
||||
`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const transportModesPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/transport-modes/?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Si el ID es "new", es una creación
|
||||
if (params.id === 'new') {
|
||||
try {
|
||||
// Fetch default settings if type, operation and company are present
|
||||
const settingsPromise = (parsedOperationType && invoiceTypeParam && companyId)
|
||||
? authenticatedFetch(
|
||||
`v1/a76/invoice-settings/${invoiceTypeParam}?operation_type=${parsedOperationType}&company_id=${companyId}`,
|
||||
if (params.id === 'new') {
|
||||
isCreate = true;
|
||||
// Call Consolidated Creation Endpoint
|
||||
const [creationResponse, settingsResult] = await Promise.all([
|
||||
authenticatedFetch(
|
||||
`v1/a76/invoices/creation-data?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
).catch((err) => {
|
||||
console.error('Error fetching defaults in server load:', err);
|
||||
return null;
|
||||
})
|
||||
: Promise.resolve(null);
|
||||
|
||||
const [
|
||||
invoiceTypesResponse,
|
||||
customsBrokersResponse,
|
||||
clientsResponse,
|
||||
providersResponse,
|
||||
currencyTypesResponse,
|
||||
transportTypesResponse,
|
||||
transportersResponse,
|
||||
vehiclesResponse,
|
||||
driversResponse,
|
||||
trailersResponse,
|
||||
customsSectionsResponse,
|
||||
codePedimentoRegimensResponse,
|
||||
sealsResponse,
|
||||
incotermsResponse,
|
||||
pedimentosResponse,
|
||||
transportModesResponse,
|
||||
settingsResponse
|
||||
] = await Promise.all([
|
||||
invoiceTypesPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
providersPromise,
|
||||
currencyTypesPromise,
|
||||
transportTypesPromise,
|
||||
transportersPromise,
|
||||
vehiclesPromise,
|
||||
driversPromise,
|
||||
trailersPromise,
|
||||
customsSectionsPromise,
|
||||
codePedimentoRegimensPromise,
|
||||
sealsPromise,
|
||||
incotermsPromise,
|
||||
pedimentosPromise,
|
||||
transportModesPromise,
|
||||
),
|
||||
settingsPromise
|
||||
]);
|
||||
|
||||
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
||||
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
||||
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
||||
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
|
||||
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
|
||||
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
||||
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
|
||||
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
|
||||
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
|
||||
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
|
||||
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
|
||||
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
|
||||
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
||||
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||
const transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
|
||||
|
||||
let defaultSettings = null;
|
||||
if (settingsResponse && settingsResponse.ok) {
|
||||
const settingsData = await settingsResponse.json();
|
||||
defaultSettings = settingsData.settings || null;
|
||||
if (creationResponse.ok) {
|
||||
catalogsData = await creationResponse.json();
|
||||
} else {
|
||||
console.error('Error fetching creation data:', creationResponse.status);
|
||||
// We continuing with empty catalogs might be better than crashing?
|
||||
// But UI will likely be broken. Let's rely on empty arrays initialization below.
|
||||
}
|
||||
|
||||
return {
|
||||
invoice: null,
|
||||
invoiceId: null,
|
||||
isCreate: true,
|
||||
invoiceTypes: invoiceTypes.items || [],
|
||||
customsBrokers: customsBrokers.items || [],
|
||||
clients: clients.items || [],
|
||||
providers: providers.items || [],
|
||||
currencyTypes: currencyTypes.items || [],
|
||||
transportTypes: transportTypes.items || [],
|
||||
transporters: transporters.items || [],
|
||||
vehicles: vehicles.items || [],
|
||||
drivers: drivers.items || [],
|
||||
trailers: trailers.items || [],
|
||||
customsSections: customsSections.items || [],
|
||||
codePedimentoRegimens: codePedimentoRegimens.items || [],
|
||||
seals: seals.items || [],
|
||||
incoterms: incoterms.items || [],
|
||||
pedimentos: pedimentos.items || [],
|
||||
transportModes: transportModes.items || [],
|
||||
defaultSettings,
|
||||
// Filtros desde query parameters para preselección
|
||||
filters: {
|
||||
operation_type: parsedOperationType,
|
||||
invoice_type: invoiceTypeParam || null
|
||||
if (settingsResult && settingsResult.settings) {
|
||||
defaultSettings = settingsResult.settings;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Edition Mode
|
||||
const invoiceId = parseInt(params.id);
|
||||
if (isNaN(invoiceId)) {
|
||||
throw error(400, 'ID de factura inválido');
|
||||
}
|
||||
|
||||
// Call Consolidated Edition Endpoint
|
||||
const editionResponse = await authenticatedFetch(
|
||||
`v1/a76/invoices/${invoiceId}/edition-data?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
if (!editionResponse.ok) {
|
||||
if (editionResponse.status === 404) {
|
||||
throw error(404, 'Factura no encontrada');
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading data for new invoice:', err);
|
||||
// En caso de error, devolver estructura mínima para que la página pueda cargar
|
||||
return {
|
||||
invoice: null,
|
||||
invoiceId: null,
|
||||
isCreate: true,
|
||||
invoiceTypes: [],
|
||||
customsBrokers: [],
|
||||
clients: [],
|
||||
providers: [],
|
||||
currencyTypes: [],
|
||||
transportTypes: [],
|
||||
transporters: [],
|
||||
vehicles: [],
|
||||
drivers: [],
|
||||
trailers: [],
|
||||
customsSections: [],
|
||||
codePedimentoRegimens: [],
|
||||
seals: [],
|
||||
incoterms: [],
|
||||
pedimentos: [],
|
||||
transportModes: [],
|
||||
filters: {
|
||||
operation_type: parsedOperationType,
|
||||
invoice_type: invoiceTypeParam || null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
throw error(editionResponse.status, 'Error al cargar la factura');
|
||||
}
|
||||
|
||||
const invoiceId = parseInt(params.id);
|
||||
if (isNaN(invoiceId)) {
|
||||
throw error(400, 'ID de factura inválido');
|
||||
}
|
||||
|
||||
try {
|
||||
// Cargar la factura desde el backend
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/invoices/${invoiceId}?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw error(response.status, 'Error al cargar la factura');
|
||||
const editionData = await editionResponse.json();
|
||||
catalogsData = editionData; // It includes catalogs + invoice
|
||||
invoiceData = editionData.invoice;
|
||||
}
|
||||
|
||||
const invoice = await response.json();
|
||||
|
||||
// Cargar también los datos de referencia para edición
|
||||
const [
|
||||
invoiceTypesResponse,
|
||||
customsBrokersResponse,
|
||||
clientsResponse,
|
||||
providersResponse,
|
||||
currencyTypesResponse,
|
||||
transportTypesResponse,
|
||||
transportersResponse,
|
||||
vehiclesResponse,
|
||||
driversResponse,
|
||||
trailersResponse,
|
||||
customsSectionsResponse,
|
||||
codePedimentoRegimensResponse,
|
||||
sealsResponse,
|
||||
incotermsResponse,
|
||||
pedimentosResponse,
|
||||
transportModesResponse
|
||||
] = await Promise.all([
|
||||
invoiceTypesPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
providersPromise,
|
||||
currencyTypesPromise,
|
||||
transportTypesPromise,
|
||||
transportersPromise,
|
||||
vehiclesPromise,
|
||||
driversPromise,
|
||||
trailersPromise,
|
||||
customsSectionsPromise,
|
||||
codePedimentoRegimensPromise,
|
||||
sealsPromise,
|
||||
incotermsPromise,
|
||||
pedimentosPromise,
|
||||
transportModesPromise
|
||||
]);
|
||||
|
||||
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
||||
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
||||
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
||||
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
|
||||
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
|
||||
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
||||
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
|
||||
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
|
||||
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
|
||||
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
|
||||
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
|
||||
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
|
||||
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
||||
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||
const transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
|
||||
|
||||
// Map snake_case response to camelCase props expected by Svelte Page
|
||||
// Providing empty arrays as defaults if something is missing
|
||||
return {
|
||||
invoice,
|
||||
invoiceId,
|
||||
isCreate: false,
|
||||
invoiceTypes: invoiceTypes.items || [],
|
||||
customsBrokers: customsBrokers.items || [],
|
||||
clients: clients.items || [],
|
||||
providers: providers.items || [],
|
||||
currencyTypes: currencyTypes.items || [],
|
||||
transportTypes: transportTypes.items || [],
|
||||
transporters: transporters.items || [],
|
||||
vehicles: vehicles.items || [],
|
||||
drivers: drivers.items || [],
|
||||
trailers: trailers.items || [],
|
||||
customsSections: customsSections.items || [],
|
||||
codePedimentoRegimens: codePedimentoRegimens.items || [],
|
||||
seals: seals.items || [],
|
||||
incoterms: incoterms.items || [],
|
||||
pedimentos: pedimentos.items || [],
|
||||
transportModes: transportModes.items || [],
|
||||
// Filtros desde query parameters para preselección
|
||||
invoice: invoiceData,
|
||||
invoiceId: params.id === 'new' ? null : parseInt(params.id),
|
||||
isCreate: isCreate,
|
||||
invoiceTypes: catalogsData.invoice_types || [],
|
||||
customsBrokers: catalogsData.customs_brokers || [],
|
||||
clients: catalogsData.clients || [],
|
||||
providers: catalogsData.providers || [],
|
||||
currencyTypes: catalogsData.currency_types || [],
|
||||
transportTypes: catalogsData.transport_types || [],
|
||||
transporters: catalogsData.transporters || [],
|
||||
vehicles: catalogsData.vehicles || [],
|
||||
drivers: catalogsData.drivers || [],
|
||||
trailers: catalogsData.trailers || [],
|
||||
customsSections: catalogsData.customs_sections || [],
|
||||
codePedimentoRegimens: catalogsData.code_pedimento_regimens || [],
|
||||
seals: catalogsData.seals || [],
|
||||
incoterms: catalogsData.incoterms || [],
|
||||
pedimentos: catalogsData.pedimentos || [],
|
||||
transportModes: catalogsData.transport_modes || [],
|
||||
defaultSettings: defaultSettings,
|
||||
filters: {
|
||||
operation_type: parsedOperationType,
|
||||
invoice_type: invoiceTypeParam || null
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading invoice:', err);
|
||||
throw error(500, 'Error al cargar la factura');
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Error in load function:', err);
|
||||
if (err && err.status === 404) throw err; // Propagate 404
|
||||
throw error(500, 'Error interno al cargar la página');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosEdicionFactura } from '$lib/config/shortcuts/dashboard/invoices/edit';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
|
||||
let companyStore: any = $state(undefined);
|
||||
@@ -241,7 +242,7 @@
|
||||
|
||||
// Estados para saber si existen datos previos
|
||||
let observationExists = $state(!!data.defaultSettings?.observationFormData);
|
||||
let itemsExists = $state(!!(data.defaultSettings?.itemsFormData?.items?.length));
|
||||
let itemsExists = $state(!!data.defaultSettings?.itemsFormData?.items?.length);
|
||||
let othersExists = $state(!!data.defaultSettings?.othersFormData);
|
||||
let continuationExists = $state(!!data.defaultSettings?.continuationFormData);
|
||||
|
||||
@@ -525,7 +526,7 @@
|
||||
}
|
||||
|
||||
let isLoadingDefaults = $state(false);
|
||||
|
||||
|
||||
// Compute initial key from source data to avoid state reference warning
|
||||
const initialLoadedKey = data.defaultSettings?.InvoiceTopFieldsFormData
|
||||
? `${data.defaultSettings.InvoiceTopFieldsFormData.invoice_type || ''}-${data.defaultSettings.InvoiceTopFieldsFormData.operation_type || ''}`
|
||||
@@ -552,22 +553,12 @@
|
||||
|
||||
isLoadingDefaults = true;
|
||||
try {
|
||||
const token = data.user?.token;
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`/api/v1/a76/invoice-settings/${InvoiceTopFieldsFormData.invoice_type}?operation_type=${InvoiceTopFieldsFormData.operation_type}&company_id=${companyStore.activeCompany.id}`,
|
||||
{ headers }
|
||||
const res = await api.get(
|
||||
`/v1/a76/invoice-settings/${InvoiceTopFieldsFormData.invoice_type}?operation_type=${InvoiceTopFieldsFormData.operation_type}&company_id=${companyStore.activeCompany.id}`
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
const settingsData = await res.json();
|
||||
const settings = settingsData.settings || {};
|
||||
if (!res.error && res.data) {
|
||||
const settings = res.data.settings || {};
|
||||
|
||||
// Mark as loaded even if empty to prevent retries for the same combination
|
||||
lastLoadedKey = currentKey;
|
||||
@@ -593,7 +584,7 @@
|
||||
InvoiceTopFieldsFormData.operation_type || data.filters?.operation_type;
|
||||
|
||||
observationExists = !!settings.observationFormData;
|
||||
itemsExists = !!(itemsFormData.items?.length);
|
||||
itemsExists = !!itemsFormData.items?.length;
|
||||
othersExists = !!settings.othersFormData;
|
||||
continuationExists = !!settings.continuationFormData;
|
||||
|
||||
|
||||
@@ -16,72 +16,44 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
|
||||
throw error(400, 'No se encontró una compañía seleccionada');
|
||||
}
|
||||
|
||||
// Cargar pedimento_codes (datos de referencia)
|
||||
const pedimentoCodesPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/pedimento-codes?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Cargar customs_sections (datos de referencia)
|
||||
const customsSectionsPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/customs-sections?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Cargar customs_brokers (datos de referencia)
|
||||
const customsBrokersPromise = authenticatedFetch(
|
||||
`v1/a76/customs-brokers?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Cargar clientes (para el select de client_id)
|
||||
const clientsPromise = authenticatedFetch(
|
||||
`v1/a76/clients-providers?company_id=${companyId}&type=client&page=1&page_size=1000`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Cargar code-pedimento-regimens (para interdependencia de campos)
|
||||
const codePedimentoRegimensPromise = authenticatedFetch(
|
||||
'v1/public/reference_data/code-pedimento-regimens?page=1&page_size=100',
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
// Si el ID es "new", es una creación
|
||||
if (params.id === 'new') {
|
||||
try {
|
||||
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
|
||||
pedimentoCodesPromise,
|
||||
customsSectionsPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
codePedimentoRegimensPromise
|
||||
]);
|
||||
// Call consolidated creation endpoint
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/pedimentos/creation-data?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
|
||||
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
|
||||
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
||||
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
||||
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
|
||||
if (!response.ok) {
|
||||
console.error('Error fetching creation data:', response.status);
|
||||
// Graceful degradation: return empty data
|
||||
return {
|
||||
pedimento: null,
|
||||
pedimentoId: null,
|
||||
isCreate: true,
|
||||
pedimentoCodes: [],
|
||||
customsSections: [],
|
||||
customsBrokers: [],
|
||||
clients: [],
|
||||
codePedimentoRegimens: [],
|
||||
error: 'Error al cargar catálogos. Verifique la conexión con el backend.'
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
pedimento: null,
|
||||
pedimentoId: null,
|
||||
isCreate: true,
|
||||
pedimentoCodes: pedimentoCodes.items || [],
|
||||
customsSections: customsSections.items || [],
|
||||
customsBrokers: customsBrokers.items || [],
|
||||
clients: clients.items || [],
|
||||
codePedimentoRegimens: codePedimentoRegimens.items || []
|
||||
pedimentoCodes: data.pedimento_codes || [],
|
||||
customsSections: data.customs_sections || [],
|
||||
customsBrokers: data.customs_brokers || [],
|
||||
clients: data.clients || [],
|
||||
codePedimentoRegimens: data.code_pedimento_regimens || []
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('❌ Error loading new pedimento data:', e);
|
||||
@@ -106,11 +78,9 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
|
||||
// Cargar el pedimento desde el backend usando authenticatedFetch
|
||||
// Call consolidated edition endpoint
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/pedimentos/${pedimentoId}?company_id=${companyId}`,
|
||||
`v1/a76/pedimentos/${pedimentoId}/edition-data?company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -126,32 +96,17 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
|
||||
throw error(response.status, 'Error al cargar el pedimento');
|
||||
}
|
||||
|
||||
const pedimento = await response.json();
|
||||
|
||||
// Cargar pedimento_codes y customs_sections
|
||||
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
|
||||
pedimentoCodesPromise,
|
||||
customsSectionsPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
codePedimentoRegimensPromise
|
||||
]);
|
||||
|
||||
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
|
||||
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
|
||||
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
||||
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
||||
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
pedimento,
|
||||
pedimentoId,
|
||||
pedimento: data.pedimento,
|
||||
pedimentoId: data.pedimento_id,
|
||||
isCreate: false,
|
||||
pedimentoCodes: pedimentoCodes.items || [],
|
||||
customsSections: customsSections.items || [],
|
||||
customsBrokers: customsBrokers.items || [],
|
||||
clients: clients.items || [],
|
||||
codePedimentoRegimens: codePedimentoRegimens.items || []
|
||||
pedimentoCodes: data.pedimento_codes || [],
|
||||
customsSections: data.customs_sections || [],
|
||||
customsBrokers: data.customs_brokers || [],
|
||||
clients: data.clients || [],
|
||||
codePedimentoRegimens: data.code_pedimento_regimens || []
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error loading pedimento:', e);
|
||||
|
||||
Reference in New Issue
Block a user