diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/dto.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/dto.py index 1a8f15c2..498d859a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/dto.py @@ -48,6 +48,9 @@ class TariffFractionResponseDTO(BaseModel): umt: Optional[str] = None adv_impo: Optional[str] = None adv_expo: Optional[str] = None + dof: Optional[str] = None + aplica_ieps: Optional[str] = None + um_code: Optional[str] = None model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py index 4e7bd2f3..da1c2881 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py @@ -38,7 +38,11 @@ async def list_tariff_fractions( if search: filters["search"] = search - items, total = TariffFractionService.get_all( + # Updated to async call with Sitar integration + # WARNING: Using async def with blocking DB dependency (Session) run in threadpool by FastAPI. + # Service.get_all calls Sitar (async) or DB (sync). + # This should be fine. + items, total = await TariffFractionService.get_all( db, skip, page_size, filters ) @@ -55,7 +59,7 @@ async def list_tariff_fractions( "/{tariff_fraction_id}", response_model=TariffFractionResponseDTO, summary="Get Tariff Fraction by ID", - description="Get a specific tariff fraction by ID", + description="Get a specific tariff fraction by ID (Lookups in Local DB for legacy compatibility)", ) async def get_tariff_fraction( tariff_fraction_id: int, @@ -68,55 +72,3 @@ async def get_tariff_fraction( raise HTTPException(status_code=404, detail="Tariff fraction not found") return TariffFractionResponseDTO.model_validate(item) - -@router.post( - "/", - response_model=TariffFractionResponseDTO, - summary="Create Tariff Fraction", - description="Create a new tariff fraction (admin only)", - status_code=201, -) -async def create_tariff_fraction( - data: TariffFractionCreateDTO, - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - item = TariffFractionService.create(db, data) - return TariffFractionResponseDTO.model_validate(item) - - -@router.put( - "/{tariff_fraction_id}", - response_model=TariffFractionResponseDTO, - summary="Update Tariff Fraction", - description="Update an existing tariff fraction (admin only)", -) -async def update_tariff_fraction( - tariff_fraction_id: int, - data: TariffFractionUpdateDTO, - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - item = TariffFractionService.update(db, tariff_fraction_id, data) - if not item: - from fastapi import HTTPException - raise HTTPException(status_code=404, detail="Tariff fraction not found") - return TariffFractionResponseDTO.model_validate(item) - - -@router.delete( - "/{tariff_fraction_id}", - summary="Delete Tariff Fraction", - description="Delete a tariff fraction (admin only)", - status_code=204, -) -async def delete_tariff_fraction( - tariff_fraction_id: int, - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - success = TariffFractionService.delete(db, tariff_fraction_id) - if not success: - from fastapi import HTTPException - raise HTTPException(status_code=404, detail="Tariff fraction not found") - diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py index 3dd13594..fd719085 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py @@ -7,26 +7,165 @@ from typing import List, Optional, Tuple, Dict, Any from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException +import zlib import logging from .models import TariffFraction from .dto import TariffFractionCreateDTO, TariffFractionUpdateDTO +from api.v1.modules.sitar.fracciones.service import FraccionesService +from api.v1.modules.sitar.fracciones.schemas import FraccionesResponse logger = logging.getLogger(__name__) +class TariffFractionMapper: + """Helper to map Sitar responses to Local domain objects""" + + @staticmethod + def to_domain(fraccion: FraccionesResponse) -> TariffFraction: + # Generate ID: Use SYSID if available, else composite hash of code + nico + if fraccion.SYSID: + fake_id = fraccion.SYSID + else: + # Composite key for uniqueness if SYSID missing + unique_str = f"{fraccion.FRACCION}-{fraccion.NICO}" + fake_id = zlib.crc32(unique_str.encode('utf-8')) + + # UX Enhauncement: Sitar API returns empty strings for some fields. + # We fill them with fallbacks so the frontend table isn't 90% empty. + code_val = fraccion.FRACCION + + # Formatting Logic: if FRACCIONPUNTO is empty, try to format code_val + formatted_fraction = code_val + if fraccion.FRACCIONPUNTO: + formatted_fraction = fraccion.FRACCIONPUNTO + elif code_val and code_val.isdigit() and len(code_val) == 8: + # Standard 8 digit format: XX.XX.XX.XX + formatted_fraction = f"{code_val[:2]}.{code_val[2:4]}.{code_val[4:6]}.{code_val[6:]}" + elif code_val and code_val.isdigit() and len(code_val) == 6: + # 6 digit (subheading): XX.XX.XX + formatted_fraction = f"{code_val[:2]}.{code_val[2:4]}.{code_val[4:]}" + + fraction_val = formatted_fraction + description_val = fraccion.DESCRIPCION if fraccion.DESCRIPCION else "(Sin descripción)" + + tf = TariffFraction( + id=fake_id, + code=code_val, + fraction=fraction_val, + description=description_val, + nico=fraccion.NICO, + # MAP CHANGE: UMT now maps to Abbreviation (e.g., Pza, Kg) + umt=fraccion.UMABREVIACION, + adv_impo=fraccion.ADVIMPOTXT, + adv_expo=fraccion.ADVEXPOTXT + ) + # Dynamically attach non-model attributes for DTO + tf.dof = fraccion.DOF + tf.aplica_ieps = fraccion.APLICAIEPS + # MAP CHANGE: New field for the numeric code (e.g., 01, 06) + tf.um_code = fraccion.UMCLAVE + + return tf + + class TariffFractionService: """Service para gestionar fracciones arancelarias (catálogo global)""" @staticmethod - def get_all( + async def get_all( db: Session, skip: int = 0, limit: int = 100, filters: Optional[Dict[str, Any]] = None, ) -> Tuple[List[TariffFraction], int]: - """Obtiene todas las fracciones arancelarias con filtros opcionales""" + """ + Obtiene fracciones arancelarias. + Estrategia: Sitar API -> Fallback Local DB + """ + # 1. Try Sitar API + try: + sitar_service = FraccionesService.get_instance() + + # Map filters + sitar_fraccion = None + sitar_nico = None + has_filters = False + + if filters: + if filters.get("search"): + term = filters["search"] + # Heuristic: if search starts with digit (after removing dots), treat as code/fraccion/nico + # This covers "0101", "01.01", "020691A" + clean_term = term.replace(".", "") + if clean_term and clean_term[0].isdigit(): + sitar_fraccion = clean_term + has_filters = True + else: + # Attempt description search via API first + logger.info(f"Search term '{term}' identified as text. Attempting API description search.") + pass + + if filters.get("code"): + sitar_fraccion = filters["code"] + has_filters = True + if filters.get("fraction"): + sitar_fraccion = filters["fraction"] + has_filters = True + if filters.get("nico"): + sitar_nico = filters["nico"] + has_filters = True + + # Determine description filter + sitar_description = None + # Only use description if we didn't use it as code above + if filters and filters.get("search"): + clean_term = filters["search"].replace(".", "") + if not (clean_term and clean_term[0].isdigit()): + sitar_description = filters["search"] + has_filters = True + + # Note: Sitar search might not return total count. + # We fetch page items. Pagination might be tricky if Sitar doesn't return total. + # Assuming Sitar returns a list. + sitar_items = await sitar_service.search( + fraccion=sitar_fraccion, + nico=sitar_nico, + description=sitar_description, + nivel=5, # User requested filtering by level 5 + skip=skip, + limit=limit + ) + + # STRICT API USAGE: + # We do NOT fallback to local DB on empty list, as user requested strict API consumption. + # We also do NOT attempt enrichment as codes mismatch (API uses '010191A' vs Local '01012101'). + + # Map items + items = [TariffFractionMapper.to_domain(item) for item in sitar_items] + + # Estimate total (Sitar service doesn't return total currently) + # If we got full limit, assume there are more. + total = len(items) + skip + if len(items) == limit: + total += 1 # Indicate more pages + + return items, total + + except Exception as e: + logger.error(f"Error fetching from Sitar API: {e}") + # STRICT API USAGE: Propagate error, do NOT fallback to local DB. + raise e + + @staticmethod + async def _get_all_local_async( + db: Session, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[TariffFraction], int]: + """Lógica original de consulta local""" query = db.query(TariffFraction) # Aplicar filtros @@ -64,21 +203,24 @@ class TariffFractionService: db: Session, tariff_fraction_id: int, ) -> Optional[TariffFraction]: - """Obtiene una fracción arancelaria por ID""" - + """ + Obtiene por ID. + Como Sitar no usa estos IDs, consultamos Local DB directamente para compatibilidad legacy. + Si se necesitara obtener detalle de Sitar, se requeriría otro identificador (Code). + """ return ( db.query(TariffFraction) .filter(TariffFraction.id == tariff_fraction_id) .first() ) + # WRITE OPERATIONS - DEPRECATED / LOCAL ONLY (Optional: Remove or Keep for Fallback Maintenance) + @staticmethod def get_by_code( db: Session, code: str, ) -> Optional[TariffFraction]: - """Obtiene una fracción arancelaria por código""" - return ( db.query(TariffFraction) .filter(TariffFraction.code == code) @@ -90,8 +232,6 @@ class TariffFractionService: db: Session, tariff_fraction_data: TariffFractionCreateDTO, ) -> TariffFraction: - """Crea una nueva fracción arancelaria""" - try: tariff_fraction = TariffFraction( **tariff_fraction_data.model_dump(), @@ -114,8 +254,6 @@ class TariffFractionService: tariff_fraction_id: int, tariff_fraction_data: TariffFractionUpdateDTO, ) -> Optional[TariffFraction]: - """Actualiza una fracción arancelaria existente""" - tariff_fraction = TariffFractionService.get_by_id( db, tariff_fraction_id ) @@ -144,8 +282,6 @@ class TariffFractionService: db: Session, tariff_fraction_id: int, ) -> bool: - """Elimina una fracción arancelaria""" - tariff_fraction = TariffFractionService.get_by_id( db, tariff_fraction_id ) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index d3d85344..d5552c7e 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -17,21 +17,8 @@ from .dto import ( ) from .service import USTariffFractionService -# Create base router with generic CRUD routes (disabled list because we'll create a custom one) -base_router = TenantCRUDRoutes( - service=USTariffFractionService, - create_schema=USTariffFractionCreateDTO, - update_schema=USTariffFractionUpdateDTO, - response_schema=USTariffFractionResponseDTO, - prefix="/us-tariff-fractions", - tags=["a76 / general catalogs / us tariff fractions"], - resource_name="USTariffFraction", - id_name="us_tariff_fraction_id", - enable_list=False, # Disable default list, we'll add custom one - enable_filters=False, - default_page_size=50, - max_page_size=10000, -) +# Create base router with generic CRUD routes - REMOVED strictly read-only from Sitar +# Writes are disabled at API level, but Service still supports fallback writes if needed internally router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"]) @@ -57,7 +44,8 @@ async def list_us_tariff_fractions( if search: filters["search"] = search - items, total = USTariffFractionService.get_all( + # Updated to async call with Sitar integration + items, total = await USTariffFractionService.get_all( db, tenant_id, company_id, skip, page_size, filters ) @@ -69,5 +57,23 @@ async def list_us_tariff_fractions( "pages": (total + page_size - 1) // page_size, } -# Include other CRUD routes from base router -router.include_router(base_router.router) + +@router.get( + "/{us_tariff_fraction_id}", + response_model=USTariffFractionResponseDTO, + summary="Get US Tariff Fraction by ID", + description="Get a specific US tariff fraction by ID (Lookups in Local DB for legacy compatibility)", +) +async def get_us_tariff_fraction( + us_tariff_fraction_id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + + item = USTariffFractionService.get_by_id(db, tenant_id, company_id, us_tariff_fraction_id) + if not item: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="US Tariff fraction not found") + return USTariffFractionResponseDTO.model_validate(item) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index 41815d68..877dfcc5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -6,19 +6,68 @@ from typing import List, Optional, Tuple, Dict, Any from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException +import zlib import logging +import re +from decimal import Decimal from .models import USTariffFraction from .dto import USTariffFractionCreateDTO, USTariffFractionUpdateDTO +from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService +from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse logger = logging.getLogger(__name__) +class USTariffFractionMapper: + """Helper to map Sitar USA responses to Local domain objects""" + + @staticmethod + def to_domain(fraccion: FraccionesUSAResponse, tenant_id: int, company_id: int) -> USTariffFraction: + # Generate a deterministic numeric ID based on the unique code + # We use CRC32 to get a consistent integer implementation-independent + fake_id = zlib.crc32((fraccion.FRACCION_SIN_PUNTO or "").encode('utf-8')) + + # Parse numeric values safely + ad_valorem = None + if fraccion.TARIFA1: + try: + # Extract numbers from string like "5.2%" or similar if present + # Assuming TARIFA1 might be clean number or percentage string + clean_val = re.sub(r'[^\d.]', '', str(fraccion.TARIFA1)) + if clean_val: + ad_valorem = Decimal(clean_val) + except: + pass + + fixed_cost = None + if fraccion.ESPECIFICO: + try: + clean_val = re.sub(r'[^\d.]', '', str(fraccion.ESPECIFICO)) + if clean_val: + fixed_cost = Decimal(clean_val) + except: + pass + + return USTariffFraction( + id=fake_id, # Updated to use fake_id instead of sitar consecutive if needed, or consistent hash + tenant_id=tenant_id, + company_id=company_id, + code=fraccion.FRACCION_SIN_PUNTO or "", + prefix=None, # Not mapped from Sitar response currently + type_code=None, + ad_valorem=ad_valorem, + fixed_cost=fixed_cost, + unit_of_measure=fraccion.UNIDADCANTIDAD, + description=fraccion.DESCRIPCION + ) + + class USTariffFractionService: """Service para gestionar fracciones arancelarias americanas""" @staticmethod - def get_all( + async def get_all( db: Session, tenant_id: int, company_id: int, @@ -26,8 +75,60 @@ class USTariffFractionService: limit: int = 100, filters: Optional[Dict[str, Any]] = None, ) -> Tuple[List[USTariffFraction], int]: - """Obtiene todas las fracciones arancelarias americanas con filtros opcionales""" + """ + Obtiene todas las fracciones arancelarias americanas con filtros opcionales. + Estrategia: Sitar API -> Fallback Local DB + """ + # 1. Try Sitar API + try: + sitar_service = FraccionesUSAService.get_instance() + + sitar_fraccion = None + has_filters = False + + if filters and filters.get("search"): + term = filters["search"] + # Sitar only filters by fraction code + if term.replace(".", "").isdigit(): + sitar_fraccion = term + has_filters = True + + sitar_items = await sitar_service.search( + fraccion=sitar_fraccion, + skip=skip, + limit=limit + ) + + # If Sitar returns empty list AND we didn't have specific filters, attempt fallback + if not sitar_items and not has_filters: + logger.warning("Sitar return empty list for USA broad query. Attempting fallback to local DB.") + return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) + + # Map items + items = [USTariffFractionMapper.to_domain(item, tenant_id, company_id) for item in sitar_items] + + # Estimate total + total = len(items) + skip + if len(items) == limit: + total += 1 + + return items, total + + except Exception as e: + logger.error(f"Error fetching USA Fractions from Sitar API, falling back to local DB: {e}") + return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) + + @staticmethod + def _get_all_local( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[USTariffFraction], int]: + """Lógica original de consulta local""" query = db.query(USTariffFraction).filter( USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id, @@ -53,7 +154,10 @@ class USTariffFractionService: def get_by_id( db: Session, tenant_id: int, company_id: int, fraction_id: int ) -> Optional[USTariffFraction]: - """Obtiene una fracción arancelaria americana por ID""" + """ + Obtiene por ID. + Legacy: Consulta Local DB. + """ return ( db.query(USTariffFraction) .filter( @@ -64,6 +168,8 @@ class USTariffFractionService: .first() ) + # WRITE OPERATIONS - DEPRECATED / LOCAL ONLY + @staticmethod def create( db: Session, @@ -71,7 +177,6 @@ class USTariffFractionService: company_id: int, fraction_data: USTariffFractionCreateDTO, ) -> USTariffFraction: - """Crea una nueva fracción arancelaria americana""" try: db_fraction = USTariffFraction( tenant_id=tenant_id, @@ -98,7 +203,6 @@ class USTariffFractionService: fraction_id: int, fraction_data: USTariffFractionUpdateDTO, ) -> Optional[USTariffFraction]: - """Actualiza una fracción arancelaria americana existente""" db_fraction = USTariffFractionService.get_by_id( db, tenant_id, company_id, fraction_id ) @@ -117,7 +221,6 @@ class USTariffFractionService: def delete( db: Session, tenant_id: int, company_id: int, fraction_id: int ) -> bool: - """Elimina una fracción arancelaria americana""" db_fraction = USTariffFractionService.get_by_id( db, tenant_id, company_id, fraction_id ) diff --git a/backend/api/v1/modules/sitar/common/base_service.py b/backend/api/v1/modules/sitar/common/base_service.py index b18009dd..0795a539 100644 --- a/backend/api/v1/modules/sitar/common/base_service.py +++ b/backend/api/v1/modules/sitar/common/base_service.py @@ -22,7 +22,7 @@ class SitarAPIBaseService: self.base_url = os.getenv("SITAR_API_URL") self.username = os.getenv("SITAR_API_USER") self.password = os.getenv("SITAR_API_PASSWORD") - self.timeout = 10.0 + self.timeout = 30.0 if not all([self.base_url, self.username, self.password]): raise ValueError( @@ -101,4 +101,20 @@ class SitarAPIBaseService: headers=headers, ) response.raise_for_status() + + # DEBUG LOGGING for SITAR inspection + if "fracciones" in url: + import logging + logger = logging.getLogger(__name__) + logger.info(f"SITAR API Response Headers for {url}: {dict(response.headers)}") + try: + data = response.json() + if isinstance(data, dict): + logger.info(f"SITAR API Response Body Keys: {list(data.keys())}") + elif isinstance(data, list) and len(data) > 0: + logger.info(f"SITAR API Response List Item Keys: {list(data[0].keys())}") + return data + except Exception: + pass + return response.json() diff --git a/backend/api/v1/modules/sitar/fracciones/schemas.py b/backend/api/v1/modules/sitar/fracciones/schemas.py index c4811a3d..a657c68b 100644 --- a/backend/api/v1/modules/sitar/fracciones/schemas.py +++ b/backend/api/v1/modules/sitar/fracciones/schemas.py @@ -33,6 +33,7 @@ class FraccionesResponse(BaseModel): APLICAIEPS: Optional[str] = Field(None, max_length=1) NIVEL: Optional[int] = None NICO: Optional[str] = Field(None, max_length=14) + SYSID: Optional[int] = None class Config: from_attributes = True diff --git a/backend/api/v1/modules/sitar/fracciones/service.py b/backend/api/v1/modules/sitar/fracciones/service.py index 0fbe6b1c..640cf969 100644 --- a/backend/api/v1/modules/sitar/fracciones/service.py +++ b/backend/api/v1/modules/sitar/fracciones/service.py @@ -21,6 +21,8 @@ class FraccionesService(SitarAPIBaseService): self, fraccion: Optional[str] = None, nico: Optional[str] = None, + description: Optional[str] = None, + nivel: Optional[int] = None, skip: int = 0, limit: int = 100, ) -> List[FraccionesResponse]: @@ -30,6 +32,10 @@ class FraccionesService(SitarAPIBaseService): params["fraccion"] = fraccion if nico: params["nico"] = nico + if description: + params["descripcion"] = description + if nivel is not None: + params["nivel"] = nivel data = await self._make_request("GET", "/api/v1/fracciones/", params=params) return [FraccionesResponse(**item) for item in data] diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/tariff-fractions.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/tariff-fractions.ts index 7983e362..556dd6c7 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/tariff-fractions.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/tariff-fractions.ts @@ -11,8 +11,10 @@ export interface TariffFraction { umt: string | null; adv_impo: string | null; adv_expo: string | null; - created_at: string | null; updated_at: string | null; + dof: string | null; + aplica_ieps: string | null; + um_code: string | null; } export interface TariffFractionCreate { diff --git a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte index 3e48edfc..e0977939 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte @@ -143,16 +143,30 @@ let showUnitDialog = $state(false); let searchUnit = $state(''); + import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte'; + let showFractionDialog = $state(false); - let tariffFractions = $state([]); - let searchFraction = $state(''); - let currentPage = $state(1); - let totalFractions = $state(0); - let hasMoreFractions = $state(true); - let isLoadingFractions = $state(false); - let searchTimeout: ReturnType; + // Removing inline tariff fraction state + // let tariffFractions = ... + + function openFractionSearch() { + showFractionDialog = true; + } let showUSFractionDialog = $state(false); + // ... + + // loadFractions removed + + function selectFraction(fraction: TariffFraction) { + formData.fraction = fraction.fraction; + formData.fraction_umt = (fraction.umt ?? '') as string; + formData.fraction_uma_key = (fraction.nico ?? '') as string; + // Actualizar tarifa de importación + formData.import_tariff_code = fraction.fraction || ''; + formData.import_tariff_type = (fraction.umt ?? '') as string; + showFractionDialog = false; + } let usTariffFractions = $state([]); let searchUSFraction = $state(''); let currentUSPage = $state(1); @@ -262,56 +276,6 @@ searchUnit = ''; } - async function openFractionSearch() { - showFractionDialog = true; - searchFraction = ''; - tariffFractions = []; - currentPage = 1; - totalFractions = 0; - hasMoreFractions = true; - await loadFractions('', 1); - } - - async function loadFractions(search: string, page: number = currentPage) { - const companyId = companyStore.activeCompany?.id; - if (!companyId || isLoadingFractions) return; - - isLoadingFractions = true; - try { - const filters = search ? { search } : {}; - const pageSize = 100; - - const response = await getTariffFractions(page, pageSize, companyId, filters); - - if (response.data) { - if (page === 1) { - tariffFractions = [...response.data.items]; - } else { - tariffFractions = [...tariffFractions, ...response.data.items]; - } - - totalFractions = response.data.total; - currentPage = page; - hasMoreFractions = tariffFractions.length < response.data.total; - } - } catch (error) { - console.error('Error cargando fracciones arancelarias:', error); - } finally { - isLoadingFractions = false; - } - } - - function selectFraction(fraction: TariffFraction) { - formData.fraction = fraction.fraction; - formData.fraction_umt = (fraction.umt ?? '') as string; - formData.fraction_uma_key = (fraction.nico ?? '') as string; - // Actualizar tarifa de importación - formData.import_tariff_code = fraction.fraction || ''; - formData.import_tariff_type = (fraction.umt ?? '') as string; - showFractionDialog = false; - searchFraction = ''; - } - async function openUSFractionSearch() { showUSFractionDialog = true; searchUSFraction = ''; @@ -565,7 +529,7 @@
-
+
@@ -596,7 +560,7 @@ -
+
- + {formData.material_description || ''}
{#if validationErrors.material_key} -

{validationErrors.material_key}

+

{validationErrors.material_key}

{/if}
@@ -635,7 +599,7 @@ onblur={() => validateField('description_es')} /> {#if validationErrors.description_es} -

{validationErrors.description_es}

+

{validationErrors.description_es}

{/if}
@@ -651,12 +615,12 @@
-
+
-
+
- + {formData.unit_of_measure_description || ''}
{#if validationErrors.unit_of_measure} -

{validationErrors.unit_of_measure}

+

{validationErrors.unit_of_measure}

{/if}
@@ -683,7 +647,7 @@ -
+
{#if validationErrors.fraction} -

{validationErrors.fraction}

+

{validationErrors.fraction}

{/if}
-
+
-
+
-
+
-
+
-
+
- - - + - + CATALOGO DE FRACCIONES AMERICANAS @@ -950,9 +859,9 @@ class="mt-1" />
-
+
- + @@ -964,7 +873,7 @@ {#each usTariffFractions as fraction (fraction.id)} selectUSFraction(fraction)} > @@ -996,7 +905,7 @@ - + CATALOGO DE DEPRECIACION @@ -1010,9 +919,9 @@ class="mt-1" /> -
+
Código Prefijo
{fraction.code}
- + @@ -1022,7 +931,7 @@ {#each depreciationCatalog as item (item.id)} selectDepreciation(item)} > @@ -1052,7 +961,7 @@ - + CATALOGO FDA @@ -1066,9 +975,9 @@ class="mt-1" /> -
+
Fracción Descripción
{item.fraction}
- + @@ -1077,7 +986,7 @@ {#each fdaCatalog as item (item.id)} selectFDA(item)} > @@ -1106,7 +1015,7 @@ - + CATALOGO DE CARTA PORTE @@ -1120,9 +1029,9 @@ class="mt-1" /> -
+
Clave FDA Descripción
{item.fda_key}
- + @@ -1131,7 +1040,7 @@ {#each cartaPorteCatalog as item (item.id)} { formData.carta_porte_code = item.code; showCartaPorteDialog = false; diff --git a/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte b/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte new file mode 100644 index 00000000..b20dfff8 --- /dev/null +++ b/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte @@ -0,0 +1,171 @@ + + + + + + CATALOGO DE FRACCIONES SITAR - SCAII + +
+
+ +
+ + { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + tariffFractions = []; + currentPage = 1; + totalFractions = 0; + hasMoreFractions = true; + loadFractions(searchFraction, 1); + }, 500); + }} + /> +
+
+
{ + const target = e.currentTarget; + if ( + target.scrollHeight - target.scrollTop <= target.clientHeight + 50 && + hasMoreFractions && + !isLoadingFractions + ) { + loadFractions(searchFraction, currentPage + 1); + } + }} + > +
Código Descripción
+ + + + + + + + + + + + + + + {#each tariffFractions as fraction (fraction.id)} + { + onSelect(fraction); + open = false; + }} + > + + + + + + + + + + + {:else} + + + + {/each} + +
ClaveFracciónNICODescripciónU.M.TAdv. ImpoAdv. ExpoDOFAplica IEPS
{fraction.um_code}{fraction.fraction}{fraction.nico}{fraction.description}{fraction.umt}{fraction.adv_impo}{fraction.adv_expo}{fraction.dof}{fraction.aplica_ieps}
+ {#if isLoadingFractions} +
+ + Cargando fracciones... +
+ {:else} + No hay fracciones disponibles + {/if} +
+
+ {#if isLoadingFractions && tariffFractions.length > 0} +
+ +
+ {/if} +
+
+ +
+ + diff --git a/frontend/src/lib/components/dashboard/goods/modales/fraction-selector-dialog.svelte b/frontend/src/lib/components/dashboard/goods/modales/fraction-selector-dialog.svelte index ea9819ed..04811fb0 100644 --- a/frontend/src/lib/components/dashboard/goods/modales/fraction-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/modales/fraction-selector-dialog.svelte @@ -1,156 +1,14 @@ - - - - Seleccionar Fracción Arancelaria - - Seleccione la fracción arancelaria del catálogo. - - - -
- - -
- -
- {#if loading} -
- -

Cargando catálogo...

-
- {:else if filteredItems.length === 0} -
-

No se encontraron fracciones.

-
- {:else} - - - - Código - Fracción - NICO - Descripción - - - - {#each filteredItems as item} - handleSelect(item)} - > - - {item.code} - - -
- - - {item.fraction} - -
-
- - {item.nico || '-'} - - - {item.description || '-'} - -
- {/each} -
-
- {/if} -
- - -
- {filteredItems.length} registros encontrados -
- -
-
-
+ diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte index 89ce0247..04811fb0 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte @@ -1,204 +1,14 @@ - - - - FRACCIONES ARANCELARIAS - - -
-
- - -
-

- Mostrando {fractions.length} de {currentPage * pageSize} resultados -

-
- -
- {#if loading} -
- -
- {:else if error} -
-

{error}

-
- {:else} -
- - - - - - - - - - - - {#each fractions as fraction, i} - handleSelect(fraction)} - > - - - - - - - {/each} - {#if fractions.length === 0 && !loading} - - - - {/if} - -
CódigoFracciónDescripciónNICOUMT
{fraction.code || ''}{fraction.fraction || ''}{fraction.description || ''}{fraction.nico || ''}{fraction.umt || ''}
- No se encontraron resultados -
-
- - {#if loadingMore} -
- - Cargando más... -
- {/if} - - {#if !hasMore && fractions.length > 0} -
Todos los resultados cargados
- {/if} - {/if} -
- -
- -
-
-
+ diff --git a/frontend/src/lib/stores/company.svelte.ts b/frontend/src/lib/stores/company.svelte.ts index 9f04bcdb..b45a6df3 100644 --- a/frontend/src/lib/stores/company.svelte.ts +++ b/frontend/src/lib/stores/company.svelte.ts @@ -76,11 +76,12 @@ class CompanyStore { this._loading = true; try { - const response = await fetch('/api/v1/a76/company/my-companies', { - credentials: 'include' - }); - if (response.ok) { - const newCompanies = await response.json(); + // Importamos dinámicamente para evitar dependencias circulares si las hubiera + const { api } = await import('$lib/api'); + const response = await api.get('/v1/a76/company/my-companies'); + + if (response.data) { + const newCompanies = response.data; // Detectar si el tenant ha cambiado if (newCompanies.length > 0) { @@ -88,7 +89,6 @@ class CompanyStore { // Si el tenant cambió, limpiar el store primero if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) { - this.clear(); } @@ -102,7 +102,7 @@ class CompanyStore { this.setActiveCompany(this._companies[0], true); // silent=true para inicialización } } else { - console.error('Error loading companies:', response.statusText); + console.error('Error loading companies:', response.error); // Si falla la carga (ej: 401), limpiar el store if (response.status === 401) { this.clear(); diff --git a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte index a5abd0d8..9ad3119a 100644 --- a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte @@ -13,14 +13,15 @@ import { obtenerAtajosListaFracciones } from '$lib/config/shortcuts/dashboard/general_catalogs/tariff_fractions/list'; import { onMount } from 'svelte'; + // Estado let tariffFractions = $state([]); - let filteredFractions = $state([]); let searchQuery = $state(''); let isLoading = $state(false); let currentPage = $state(1); let totalPages = $state(1); let totalRecords = $state(0); const pageSize = 50; + let searchTimeout: NodeJS.Timeout; // Atajos useShortcuts( @@ -35,24 +36,24 @@ loadTariffFractions(); }); - // Filtrar fracciones cuando cambia la búsqueda + // Efecto para búsqueda con debounce $effect(() => { - if (searchQuery.trim() === '') { - filteredFractions = tariffFractions; - } else { - const query = searchQuery.toLowerCase(); - filteredFractions = tariffFractions.filter( - (fraction) => - fraction.code.toLowerCase().includes(query) || - fraction.fraction.toLowerCase().includes(query) || - (fraction.description ?? '').toLowerCase().includes(query) || - (fraction.nico ?? '').toLowerCase().includes(query) || - (fraction.umt ?? '').toLowerCase().includes(query) - ); - } + // Limpiar timeout anterior + clearTimeout(searchTimeout); + + // Setup nuevo timeout + searchTimeout = setTimeout(() => { + // Resetear a página 1 cuando cambia la búsqueda + if (currentPage !== 1) { + currentPage = 1; + } + loadTariffFractions(1, searchQuery); + }, 500); + + return () => clearTimeout(searchTimeout); }); - async function loadTariffFractions(page: number = 1) { + async function loadTariffFractions(page: number = 1, search: string = '') { const companyId = companyStore.activeCompany?.id; if (!companyId) { console.error('No hay compañía activa'); @@ -61,10 +62,15 @@ isLoading = true; try { - const response = await getTariffFractions(page, pageSize, companyId); + // Preparar filtros + const filters: Record = {}; + if (search.trim()) { + filters.search = search.trim(); + } + + const response = await getTariffFractions(page, pageSize, companyId, filters); if (response.data) { tariffFractions = response.data.items; - filteredFractions = response.data.items; totalPages = response.data.pages; totalRecords = response.data.total; currentPage = response.data.page; @@ -78,31 +84,31 @@ async function goToPage(page: number) { if (page >= 1 && page <= totalPages && page !== currentPage) { - await loadTariffFractions(page); + await loadTariffFractions(page, searchQuery); } }
- + Catálogo de Fracciones SITAR - SCAII -

Nomenclatura arancelaria mexicana completa

+

Nomenclatura arancelaria mexicana completa

-
-
+
+
@@ -120,13 +126,13 @@ Cargando...
{:else} - Mostrando {filteredFractions.length} de {totalRecords} fracciones arancelarias + Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias {#if searchQuery} (filtrado) {/if} {/if}
- {#if !searchQuery && totalPages > 1} + {#if totalPages > 1}
Página {currentPage} de {totalPages}
@@ -134,9 +140,9 @@
-
+
- + Código Fracción @@ -148,9 +154,9 @@ - {#if filteredFractions.length === 0} + {#if tariffFractions.length === 0} - + {#if isLoading} Cargando fracciones arancelarias... {:else if searchQuery} @@ -161,7 +167,7 @@ {:else} - {#each filteredFractions as fraction (fraction.id)} + {#each tariffFractions as fraction (fraction.id)} {fraction.code} - {#if !searchQuery && totalPages > 1} + {#if totalPages > 1}