diff --git a/backend/api/v1/modules/a76/classes/dto.py b/backend/api/v1/modules/a76/classes/dto.py index 7fc3ccc0..00c141b4 100644 --- a/backend/api/v1/modules/a76/classes/dto.py +++ b/backend/api/v1/modules/a76/classes/dto.py @@ -196,4 +196,33 @@ class ClassSearchDTO(BaseModel): ) class Config: - from_attributes = True \ No newline at end of file + 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) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/classes/routes.py b/backend/api/v1/modules/a76/classes/routes.py index c59dd575..329294b0 100644 --- a/backend/api/v1/modules/a76/classes/routes.py +++ b/backend/api/v1/modules/a76/classes/routes.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index a4c3fd1b..1e3ba949 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/invoices/catalog_service.py b/backend/api/v1/modules/a76/invoices/catalog_service.py new file mode 100644 index 00000000..9ad64b0b --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/catalog_service.py @@ -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={} + ) diff --git a/backend/api/v1/modules/a76/invoices/routes.py b/backend/api/v1/modules/a76/invoices/routes.py index 05bd8d89..bf95bf9f 100644 --- a/backend/api/v1/modules/a76/invoices/routes.py +++ b/backend/api/v1/modules/a76/invoices/routes.py @@ -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, diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 1ac300c9..c658e9ae 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -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 + diff --git a/backend/api/v1/modules/a76/pedmientos/catalog_service.py b/backend/api/v1/modules/a76/pedmientos/catalog_service.py new file mode 100644 index 00000000..b78bd8b8 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/catalog_service.py @@ -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 + ) diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py index fbc8f887..b912cf39 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py @@ -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) diff --git a/backend/api/v1/modules/a76/pedmientos/schemas.py b/backend/api/v1/modules/a76/pedmientos/schemas.py new file mode 100644 index 00000000..fb673d64 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/schemas.py @@ -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 diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index a87291f7..3f0eee9b 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -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"] diff --git a/frontend/src/lib/api/dashboard/a76/classes.ts b/frontend/src/lib/api/dashboard/a76/classes.ts index aaad2231..236c2cdb 100644 --- a/frontend/src/lib/api/dashboard/a76/classes.ts +++ b/frontend/src/lib/api/dashboard/a76/classes.ts @@ -101,5 +101,21 @@ export const classesApi = { */ createFA: (data: any, company_id: number): Promise> => { 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> => { + 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()}`); } }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 99973e13..ea5c2194 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -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(`/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(`/v1/a76/invoices/${invoiceId}/edition-data?${params.toString()}`); + }, + // --- Nested Resources --- /** diff --git a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte index 36f524bf..ebde6753 100644 --- a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte +++ b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte @@ -49,22 +49,24 @@ const filteredClasses = $derived( classes.filter((c) => { // Filtro por código de clase - const matchesCode = !searchTerm || - c.class_code.toLowerCase().includes(searchTerm.toLowerCase()); - + const matchesCode = + !searchTerm || c.class_code.toLowerCase().includes(searchTerm.toLowerCase()); + // Filtro por descripción (español o inglés) - const matchesDescription = !searchDescription || + const matchesDescription = + !searchDescription || (c.description_es?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false) || (c.description_en?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false); - + // Filtro por tipo de material - const matchesType = !searchType || - (c.material_key?.toLowerCase().includes(searchType.toLowerCase()) ?? false); - + const matchesType = + !searchType || (c.material_key?.toLowerCase().includes(searchType.toLowerCase()) ?? false); + // Filtro por fracción arancelaria - const matchesFraction = !searchFraction || + const matchesFraction = + !searchFraction || (c.fraction?.toLowerCase().includes(searchFraction.toLowerCase()) ?? false); - + return matchesCode && matchesDescription && matchesType && matchesFraction; }) ); @@ -79,48 +81,23 @@ async function loadClasses() { const companyId = companyStore.activeCompany?.id; - if (!companyId) { + if (!companyId) { return; } isLoading = true; - try { - const response = await classesApi.list({ + try { + // Use the new consolidated endpoint that fetches classes + FA data in ONE query + const response = await classesApi.getWithFAData({ company_id: companyId, page: 1, page_size: 1000 }); - if (!response.data) return; - - // Para cada clase base, intentar cargar sus datos de activo fijo - const classesWithFA = await Promise.all( - response.data.items.map(async (baseClass) => { - try { - const faResponse = await faClassesApi.list({ - company_id: companyId, - class_id: baseClass.id, - page: 1, - page_size: 1 - }); - - const faData = faResponse.data?.items[0]; - - return { - ...baseClass, - fa_class_id: faData?.id, - depreciation_rate: faData?.depreciation_rate, - fda_code: faData?.fda_code, - class_enabled: faData?.class_enabled - } as FixedAssetClassExtended; - } catch (error) { - // Si no tiene FA class, solo retornar la clase base - return baseClass as FixedAssetClassExtended; - } - }) - ); - - classes = classesWithFA; + if (response.data) { + // Data already comes with FA fields embedded + classes = response.data as FixedAssetClassExtended[]; + } } catch (error) { console.error('Error cargando clases:', error); toast.error('Error al cargar las clases de activo fijo'); @@ -144,12 +121,12 @@ }; } - async function saveFixedAssetClass(formData: any) { + async function saveFixedAssetClass(formData: any) { const companyId = companyStore.activeCompany?.id; - + // CAMBIO: Usar $state.snapshot para obtener una copia real, no reactiva - const data = $state.snapshot(formData); - + const data = $state.snapshot(formData); + if (!companyId) { toast.error('No hay empresa seleccionada'); throw new Error('No hay empresa seleccionada'); @@ -157,7 +134,7 @@ // Validar campos obligatorios const missingFields: string[] = []; - + if (!data.class_code?.trim()) { missingFields.push('Código de clase'); } @@ -174,156 +151,164 @@ missingFields.push('Fracción arancelaria'); } - if (missingFields.length > 0) { - const fieldsList = missingFields.join(', '); - validationError = `Debe completar los siguientes campos obligatorios: ${fieldsList}`; - toast.error(validationError, { - duration: 8000 - }); - throw new Error(`Campos obligatorios faltantes: ${fieldsList}`); - } - - // Limpiar error de validación si todo está bien - validationError = ''; - - try { - // Usar el endpoint combinado /fa que crea ambos registros en una transacción - const payload = { - class_code: data.class_code.trim(), - description_es: data.description_es.trim(), - description_en: data.description_en?.trim() || '', - material_key: data.material_key.trim(), - unit_of_measure: data.unit_of_measure.trim(), - fraction: data.fraction.trim(), - us_fraction: data.us_fraction?.trim() || '', - sub_key: data.sub_key || '', - physical_review: data.physical_review ? 1 : 0, - iva_exempt_fraction: data.iva_exempt_fraction || '', - // FA-specific fields - import_tariff_code: data.import_tariff_code || null, - import_tariff_type: data.import_tariff_type || null, - export_tariff_code: data.export_tariff_code || null, - export_tariff_type: data.export_tariff_type || null, - depreciation_rate: data.annual_depreciation_rate || null, - fda_code: data.fda_key || null, - eccn_code: data.eccn_code || null, - class_enabled: true - }; - - const response = await classesApi.createFA(payload, companyId); - - if (response.error) { - console.error('Server error:', response.error); - - // Manejar diferentes formatos de error - let errorMessage = response.error; - let isDuplicateError = false; - - // Detectar si es un error de código duplicado - if (errorMessage.includes('código') && errorMessage.includes('ya está en uso')) { - isDuplicateError = true; - } - - // Mensaje más específico para errores de duplicado - if (isDuplicateError) { - validationError = `⚠️ ${errorMessage}\n\nPor favor, cambie el código de clase a uno diferente.`; - } else { - validationError = `⚠️ ${errorMessage}`; - } - - toast.error(errorMessage, { duration: 8000 }); - throw new Error(errorMessage); + if (missingFields.length > 0) { + const fieldsList = missingFields.join(', '); + validationError = `Debe completar los siguientes campos obligatorios: ${fieldsList}`; + toast.error(validationError, { + duration: 8000 + }); + throw new Error(`Campos obligatorios faltantes: ${fieldsList}`); } + // Limpiar error de validación si todo está bien validationError = ''; - toast.success('✅ Clase de activo fijo creada correctamente'); - return response.data; - } catch (error: any) { - console.error('Error saving fixed asset class:', error); - // El toast ya se mostró arriba, solo re-lanzar el error - throw error; + try { + // Usar el endpoint combinado /fa que crea ambos registros en una transacción + const payload = { + class_code: data.class_code.trim(), + description_es: data.description_es.trim(), + description_en: data.description_en?.trim() || '', + material_key: data.material_key.trim(), + unit_of_measure: data.unit_of_measure.trim(), + fraction: data.fraction.trim(), + us_fraction: data.us_fraction?.trim() || '', + sub_key: data.sub_key || '', + physical_review: data.physical_review ? 1 : 0, + iva_exempt_fraction: data.iva_exempt_fraction || '', + // FA-specific fields + import_tariff_code: data.import_tariff_code || null, + import_tariff_type: data.import_tariff_type || null, + export_tariff_code: data.export_tariff_code || null, + export_tariff_type: data.export_tariff_type || null, + depreciation_rate: data.annual_depreciation_rate || null, + fda_code: data.fda_key || null, + eccn_code: data.eccn_code || null, + class_enabled: true + }; + + const response = await classesApi.createFA(payload, companyId); + + if (response.error) { + console.error('Server error:', response.error); + + // Manejar diferentes formatos de error + let errorMessage = response.error; + let isDuplicateError = false; + + // Detectar si es un error de código duplicado + if (errorMessage.includes('código') && errorMessage.includes('ya está en uso')) { + isDuplicateError = true; + } + + // Mensaje más específico para errores de duplicado + if (isDuplicateError) { + validationError = `⚠️ ${errorMessage}\n\nPor favor, cambie el código de clase a uno diferente.`; + } else { + validationError = `⚠️ ${errorMessage}`; + } + + toast.error(errorMessage, { duration: 8000 }); + throw new Error(errorMessage); + } + + validationError = ''; + toast.success('✅ Clase de activo fijo creada correctamente'); + return response.data; + } catch (error: any) { + console.error('Error saving fixed asset class:', error); + // El toast ya se mostró arriba, solo re-lanzar el error + throw error; } } async function updateFixedAssetClass(formData: any) { - - const companyId = companyStore.activeCompany?.id; - - // CAMBIO 1: Usar $state.snapshot para obtener una copia real, no reactiva - // Esto garantiza que aunque el hijo borre el formulario, 'data' mantenga los valores - const data = $state.snapshot(formData); - - if (!companyId || !selectedClass) { - toast.error('No hay empresa o clase seleccionada'); - return; - } + const companyId = companyStore.activeCompany?.id; - // CAMBIO 2: Validar sobre 'data' (la copia muerta) - const missingFields: string[] = []; - if (!data.class_code?.trim()) missingFields.push('Código de clase'); - if (!data.description_es?.trim()) missingFields.push('Descripción en español'); - if (!data.material_key?.trim()) missingFields.push('Tipo de activo fijo'); - if (!data.unit_of_measure?.trim()) missingFields.push('Unidad de medida comercial'); - if (!data.fraction?.trim()) missingFields.push('Fracción arancelaria'); - - if (missingFields.length > 0) { - const errorMsg = `Campos obligatorios faltantes: ${missingFields.join(', ')}`; - validationError = `⚠️ ${errorMsg}`; - toast.error(errorMsg); - // Lanzamos el error para que el 'onSave' del Dialog no cierre la ventana - throw new Error(errorMsg); - } - - validationError = ''; + // CAMBIO 1: Usar $state.snapshot para obtener una copia real, no reactiva + // Esto garantiza que aunque el hijo borre el formulario, 'data' mantenga los valores + const data = $state.snapshot(formData); - try { - // CAMBIO 3: Usar siempre 'data' para los payloads - const a76Response = await classesApi.update(selectedClass.id, { - class_code: data.class_code.trim(), - description_es: data.description_es.trim(), - description_en: data.description_en?.trim() || '', - material_key: data.material_key.trim(), - unit_of_measure: data.unit_of_measure.trim(), - fraction: data.fraction.trim(), - us_fraction: data.us_fraction || '', - physical_review: data.physical_review ? 1 : 0, - iva_exempt_fraction: data.iva_exempt_fraction || '' - }, companyId); + if (!companyId || !selectedClass) { + toast.error('No hay empresa o clase seleccionada'); + return; + } - if (selectedClass.fa_class_id) { - await faClassesApi.update(selectedClass.fa_class_id, { - depreciation_rate: data.annual_depreciation_rate || null, - fda_code: data.fda_key || null - }, companyId); - } else { - await faClassesApi.create({ - class_id: selectedClass.id, - depreciation_rate: data.annual_depreciation_rate || null, - fda_code: data.fda_key || null, - class_enabled: true - }, companyId); - } + // CAMBIO 2: Validar sobre 'data' (la copia muerta) + const missingFields: string[] = []; + if (!data.class_code?.trim()) missingFields.push('Código de clase'); + if (!data.description_es?.trim()) missingFields.push('Descripción en español'); + if (!data.material_key?.trim()) missingFields.push('Tipo de activo fijo'); + if (!data.unit_of_measure?.trim()) missingFields.push('Unidad de medida comercial'); + if (!data.fraction?.trim()) missingFields.push('Fracción arancelaria'); - toast.success('Clase actualizada correctamente'); - return { a76: a76Response.data }; + if (missingFields.length > 0) { + const errorMsg = `Campos obligatorios faltantes: ${missingFields.join(', ')}`; + validationError = `⚠️ ${errorMsg}`; + toast.error(errorMsg); + // Lanzamos el error para que el 'onSave' del Dialog no cierre la ventana + throw new Error(errorMsg); + } - } catch (error: any) { - console.error('Error updating fixed asset class:', error); + validationError = ''; + + try { + // CAMBIO 3: Usar siempre 'data' para los payloads + const a76Response = await classesApi.update( + selectedClass.id, + { + class_code: data.class_code.trim(), + description_es: data.description_es.trim(), + description_en: data.description_en?.trim() || '', + material_key: data.material_key.trim(), + unit_of_measure: data.unit_of_measure.trim(), + fraction: data.fraction.trim(), + us_fraction: data.us_fraction || '', + physical_review: data.physical_review ? 1 : 0, + iva_exempt_fraction: data.iva_exempt_fraction || '' + }, + companyId + ); + + if (selectedClass.fa_class_id) { + await faClassesApi.update( + selectedClass.fa_class_id, + { + depreciation_rate: data.annual_depreciation_rate || null, + fda_code: data.fda_key || null + }, + companyId + ); + } else { + await faClassesApi.create( + { + class_id: selectedClass.id, + depreciation_rate: data.annual_depreciation_rate || null, + fda_code: data.fda_key || null, + class_enabled: true + }, + companyId + ); + } + + toast.success('Clase actualizada correctamente'); + return { a76: a76Response.data }; + } catch (error: any) { + console.error('Error updating fixed asset class:', error); console.error('Error response:', error?.response); console.error('Error response data:', error?.response?.data); console.error('Error response detail:', error?.response?.data?.detail); console.error('Error type:', typeof error?.response?.data?.detail); - + let errorMessage = 'Error al actualizar la clase'; let isDuplicateError = false; - + // Extract error message from response if (error?.response?.data?.detail) { if (Array.isArray(error.response.data.detail)) { - errorMessage = error.response.data.detail.map((e: any) => - `${e.loc ? e.loc.join(' → ') : ''}: ${e.msg || e}` - ).join(', '); + errorMessage = error.response.data.detail + .map((e: any) => `${e.loc ? e.loc.join(' → ') : ''}: ${e.msg || e}`) + .join(', '); } else if (typeof error.response.data.detail === 'string') { errorMessage = error.response.data.detail; // Detectar si es un error de código duplicado @@ -336,23 +321,23 @@ } else if (error?.message) { errorMessage = error.message; } - + console.error('Final error message:', errorMessage); console.error('Is duplicate error:', isDuplicateError); - - // Mensaje más específico para errores de duplicado + + // Mensaje más específico para errores de duplicado if (isDuplicateError) { validationError = `⚠️ ${errorMessage}\n\nPor favor, cambie el código de clase a uno diferente.`; } else { validationError = `⚠️ ${errorMessage}`; } - + toast.error(errorMessage, { duration: 8000 }); - + console.error('Toast shown, about to throw error'); - throw error; - } -} + throw error; + } + } function handleNew() { selectedClass = null; formData = { @@ -383,7 +368,7 @@ async function confirmDelete() { if (!selectedClass) return; - + const companyId = companyStore.activeCompany?.id; if (!companyId) { toast.error('No hay empresa seleccionada'); @@ -391,16 +376,16 @@ } const classToDelete = selectedClass; - + try { // El backend ahora elimina automáticamente la extensión FA si existe await classesApi.delete(classToDelete.id, companyId); toast.success(`Clase ${classToDelete.class_code} eliminada correctamente`); - + // Recargar lista await loadClasses(); - + selectedClass = null; showDeleteDialog = false; formData = { @@ -420,48 +405,46 @@ } } - // Keyboard Shortcuts - import { useShortcuts } from '$lib/hooks/use-shortcuts'; + // Keyboard Shortcuts + import { useShortcuts } from '$lib/hooks/use-shortcuts'; - useShortcuts('Goods / Classes', [ - { - key: 'Alt+Shift+N', - description: 'New Class', - action: () => { - handleNew(); - validationError = ''; - showInsertDialog = true; - } - }, - { - key: 'Alt+Shift+R', - description: 'Refresh', - action: handleRefresh - }, - { - key: 'Alt+Shift+D', - description: 'Delete', - action: handleDelete - } - ]); + useShortcuts('Goods / Classes', [ + { + key: 'Alt+Shift+N', + description: 'New Class', + action: () => { + handleNew(); + validationError = ''; + showInsertDialog = true; + } + }, + { + key: 'Alt+Shift+R', + description: 'Refresh', + action: handleRefresh + }, + { + key: 'Alt+Shift+D', + description: 'Delete', + action: handleDelete + } + ]); -
+

CATALOGO DE CLASES DE ACTIVO FIJO

-

- Gestiona y consulta las clases de activo fijo -

+

Gestiona y consulta las clases de activo fijo

-
+
-
+
-
-
+
+

Filtros

@@ -471,11 +454,7 @@
- +
@@ -487,254 +466,300 @@
- +
- +
-
-
+
+

Listado de Clases

Mostrando de {filteredClasses.length} registros -
- -
- - - - - - - - - - - - - - {#if isLoading} + +
+
- - ClaseDescripción EspañolDescripción InglésTipoU.MFracción U.M.T. Fracción US
+ - + + + + + + + + + - {:else if filteredClasses.length === 0} - - - - {:else} - {#each filteredClasses as cls (cls.id)} - selectClass(cls)} - > - - - - - - - + + + {#if isLoading} + + - {/each} - {/if} - -
Cargando... + + ClaseDescripción EspañolDescripción InglésTipoU.MFracciónU.M.T.Fracción US
- No hay clases de activo fijo registradas -
- - - - {cls.class_code} - - {cls.description_es || ''}{cls.description_en || ''} - - {cls.material_key || ''} - - {cls.unit_of_measure || ''}{cls.fraction || ''} - {cls.us_fraction || '-'}
Cargando...
+ {:else if filteredClasses.length === 0} + + + No hay clases de activo fijo registradas + + + {:else} + {#each filteredClasses as cls (cls.id)} + selectClass(cls)} + > + + + + + + {cls.class_code} + + + {cls.description_es || ''} + {cls.description_en || ''} + + + {cls.material_key || ''} + + + {cls.unit_of_measure || ''} + {cls.fraction || ''} - + {cls.us_fraction || '-'} + + {/each} + {/if} + + +
-
-
-
-

Código de Clase

-

- {formData.class_code || '---'} -

-
+
+
+

+ Código de Clase +

+

+ {formData.class_code || '---'} +

+
-
-
-
- -

{formData.description_es || 'Sin descripción'}

-
-
- -

{formData.description_en || 'No translation available'}

-
+
+
+
+ +

+ {formData.description_es || 'Sin descripción'} +

- -
-
- -
- - {formData.material_key || '-'} -
-
-
- - {formData.unit_of_measure || '-'} -
-
- -
- -

- {formData.fraction || '0000.00.00'} +

+ +

+ {formData.description_en || 'No translation available'}

+ +
+
+ +
+ + {formData.material_key || '-'} +
+
+
+ + {formData.unit_of_measure || '-'} +
+
+ +
+ +

+ {formData.fraction || '0000.00.00'} +

+
+
-
-
+
+
- - - + +
- - + + {selectedClass ? 'Editar' : 'Nueva'} Clase de Activo Fijo - + {#if validationError} -
+
-
+
!
-

Error de Validación

-

{validationError}

+

Error de Validación

+

{validationError}

-
{/if} - +
- validationError = ''} + onClearError={() => (validationError = '')} onSave={async (data: Partial) => { // Evitar múltiples clics - if (isSaving) { + if (isSaving) { return; } isSaving = true; - validationError = ''; - + validationError = ''; + try { const cleanData = $state.snapshot(data); const companyId = companyStore.activeCompany?.id; - + if (!companyId) { throw new Error('No hay empresa seleccionada'); } let response; - + if (selectedClass?.id) { - // === ACTUALIZACIÓN === - - response = await classesApi.update(selectedClass.id, { - class_code: cleanData.class_code?.trim() || '', - description_es: cleanData.description_es?.trim() || '', - description_en: cleanData.description_en?.trim() || '', - material_key: cleanData.material_key?.trim() || '', - unit_of_measure: cleanData.unit_of_measure?.trim() || '', - fraction: cleanData.fraction?.trim() || '', - us_fraction: cleanData.us_fraction || '', - physical_review: cleanData.physical_review ? 1 : 0, - iva_exempt_fraction: cleanData.iva_exempt_fraction || '' - }, companyId); - + // === ACTUALIZACIÓN === + + response = await classesApi.update( + selectedClass.id, + { + class_code: cleanData.class_code?.trim() || '', + description_es: cleanData.description_es?.trim() || '', + description_en: cleanData.description_en?.trim() || '', + material_key: cleanData.material_key?.trim() || '', + unit_of_measure: cleanData.unit_of_measure?.trim() || '', + fraction: cleanData.fraction?.trim() || '', + us_fraction: cleanData.us_fraction || '', + physical_review: cleanData.physical_review ? 1 : 0, + iva_exempt_fraction: cleanData.iva_exempt_fraction || '' + }, + companyId + ); + // ¡IMPORTANTE! fetchApi NO lanza excepciones, retorna { error, status } if (response.error) { console.error('❌ Error en respuesta de actualización:', response); throw new Error(response.error); } - } else { - // === CREACIÓN === - const payload = { + // === CREACIÓN === + const payload = { class_code: cleanData.class_code?.trim() || '', description_es: cleanData.description_es?.trim() || '', description_en: cleanData.description_en?.trim() || '', @@ -748,24 +773,25 @@ depreciation_rate: cleanData.depreciation_rate || null, fda_code: cleanData.fda_code || null, class_enabled: true - }; - + }; + response = await classesApi.createFA(payload, companyId); - + if (response.error) { console.error('❌ Error del servidor:', response.error); throw new Error(response.error); - } + } } - // === ÉXITO TOTAL === + // === ÉXITO TOTAL === const wasUpdate = !!selectedClass?.id; await loadClasses(); showInsertDialog = false; selectedClass = null; validationError = ''; - toast.success(wasUpdate ? 'Clase actualizada correctamente' : 'Clase creada correctamente'); - + toast.success( + wasUpdate ? 'Clase actualizada correctamente' : 'Clase creada correctamente' + ); } catch (error: any) { // === ERROR === console.error('========================================'); @@ -775,9 +801,9 @@ console.error('Error.response.data:', error?.response?.data); console.error('Error.detail:', error?.detail); console.error('========================================'); - + let errorMsg = 'Error al guardar'; - + // Primero intentar con error.detail (fetch directo) if (error?.detail) { if (typeof error.detail === 'string') { @@ -798,41 +824,48 @@ else if (error?.message) { errorMsg = error.message; } - + console.error('📝 Mensaje de error extraído:', errorMsg); - + validationError = errorMsg; console.error('🔴 validationError asignado:', validationError); console.error('🔴 showInsertDialog permanece:', showInsertDialog); console.error('========================================'); - + // NO cerramos el diálogo, permanece abierto } finally { - isSaving = false; + isSaving = false; } }} - onCancel={() => { + onCancel={() => { showInsertDialog = false; selectedClass = null; }} />
- - - + @@ -848,17 +881,17 @@

- ¿Estás seguro que deseas eliminar la clase {selectedClass?.class_code}? + ¿Estás seguro que deseas eliminar la clase {selectedClass?.class_code}?

-

+

{selectedClass?.description_es}

-

- Esta acción no se puede deshacer. -

+

Esta acción no se puede deshacer.

- + diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts index 6efe0aff..eb71d203 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts @@ -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'); } }; diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index 4b41e85f..9273d583 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -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.success && 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; diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts index 2df3627f..2a67bd94 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts @@ -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);