Refactor code structure for improved readability and maintainability

This commit is contained in:
Galindo97
2026-01-07 08:44:10 -06:00
parent 7e72b559ef
commit 7c9c295c5c
33 changed files with 11963 additions and 1 deletions

View File

@@ -0,0 +1,3 @@
"""
Depreciation Catalog Module
"""

View File

@@ -0,0 +1,38 @@
"""
Depreciation Catalog DTOs
"""
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
class DepreciationCatalogCreate(BaseModel):
"""DTO for creating a depreciation catalog entry"""
fraction: str = Field(..., max_length=10)
description: str = Field(..., max_length=500)
depreciation_rate: float = Field(..., ge=0, le=100)
model_config = ConfigDict(from_attributes=True)
class DepreciationCatalogUpdate(BaseModel):
"""DTO for updating a depreciation catalog entry"""
fraction: str | None = Field(None, max_length=10)
description: str | None = Field(None, max_length=500)
depreciation_rate: float | None = Field(None, ge=0, le=100)
model_config = ConfigDict(from_attributes=True)
class DepreciationCatalogResponse(BaseModel):
"""DTO for depreciation catalog response"""
id: int
tenant_id: int
company_id: int
fraction: str
description: str
depreciation_rate: float
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,25 @@
"""
Depreciation Catalog Model
"""
from sqlalchemy import Column, Integer, String, Numeric, ForeignKey, PrimaryKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
class DepreciationCatalog(Base, TenantScopedMixin, TimestampMixin):
"""Depreciation Catalog Model"""
__tablename__ = 'depreciation_catalog'
__table_args__ = (
PrimaryKeyConstraint("id", name="depreciation_catalog_pkey"),
{'schema': 'a76'}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Fields
fraction: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
description: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
depreciation_rate: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)

View File

@@ -0,0 +1,67 @@
"""
Depreciation Catalog Routes
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from typing import Any, Dict, Optional
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 api.v1.modules.a76.general_catalogs.depreciation_catalog.service import DepreciationCatalogService
from api.v1.modules.a76.general_catalogs.depreciation_catalog.dto import (
DepreciationCatalogCreate,
DepreciationCatalogUpdate,
DepreciationCatalogResponse
)
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
base_router = TenantCRUDRoutes(
service=DepreciationCatalogService,
create_schema=DepreciationCatalogCreate,
update_schema=DepreciationCatalogUpdate,
response_schema=DepreciationCatalogResponse,
prefix="/depreciation-catalog",
tags=["a76 / general catalogs / depreciation catalog"],
resource_name="DepreciationCatalog",
id_name="depreciation_catalog_id",
enable_list=False, # Disable default list, we'll add custom one
enable_filters=False,
default_page_size=100,
max_page_size=1000,
)
router = APIRouter(prefix="/depreciation-catalog", tags=["a76 / general catalogs / depreciation catalog"])
# Custom list endpoint with search filter
@router.get(
"/",
response_model=Dict[str, Any],
summary="List Depreciation Catalog",
description="Get paginated list of Depreciation Catalog entries with optional search filter",
)
async def list_depreciation_catalog(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(100, ge=1, le=1000, description="Page size"),
search: Optional[str] = Query(None, description="Search in fraction or description"),
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)
items, total = DepreciationCatalogService.get_all(
db, tenant_id, company_id, page, page_size, search
)
return {
"items": [DepreciationCatalogResponse.model_validate(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size,
}
# Include other CRUD routes from base router
router.include_router(base_router.router)

View File

@@ -0,0 +1,34 @@
"""
Datos semilla para Catálogo de Depreciación
Basado en artículo 34 de la Ley del Impuesto Sobre la Renta
"""
from decimal import Decimal
# Formato: (fraction, description, depreciation_rate)
seed = [
("I", "a) Para inmuebles declarados como monumentos arqueológicos, artísticos, históricos o patrimoniales, conforme a la Ley Federal sobre Monumentos y Zonas Arqueológicos, Artísticos e Históricos, que cuenten con el certificado de restauración expedido por el Instituto Nacional de Antropología e Historia o el Instituto Nacional de Bellas Artes.", Decimal("10.00")),
("I", "b) En los demás casos.", Decimal("5.00")),
("II", "a) Para bombas de suministro de combustible a trenes.", Decimal("3.00")),
("II", "b) Para vías férreas.", Decimal("5.00")),
("II", "c) Para carros de ferrocarril, locomotoras, armones y autoarmones.", Decimal("6.00")),
("II", "d) Para maquinaria niveladora de vías, desclavadoras, esmeriles para vías, gatos de motor para levantar la vía, removedora, insertadora y taladradora de durmientes.", Decimal("7.00")),
("II", "e) Para el equipo de comunicación, señalización y telemando.", Decimal("10.00")),
("III", "Para mobiliario y equipo de oficina.", Decimal("10.00")),
("IV", "Para embarcaciones.", Decimal("6.00")),
("IX", "Para semovientes y vegetales.", Decimal("100.00")),
("V", "a) Para los dedicados a la aerofumigación agrícola.", Decimal("25.00")),
("V", "b) Para los demás.", Decimal("10.00")),
("VI", "Para automóviles, autobuses, camiones de carga, tractocamiones, montacargas y remolques.", Decimal("25.00")),
("VII", "Para computadoras personales de escritorio y portátiles; servidores; impresoras, lectores ópticos, graficadores, lectores de código de barras, digitalizadores, unidades de almacenamiento externo y concentradores de redes de cómputo.", Decimal("30.00")),
("VIII", "Para dados, troqueles, moldes, matrices y herramental.", Decimal("35.00")),
("X", "a) Para torres de transmisión y cables, excepto los de fibra óptica.", Decimal("5.00")),
("X", "b) Para sistemas de radio, incluyendo equipo de transmisión y manejo que utiliza el espectro radioeléctrico, tales como el de radiotransmisión de microonda digital o analógica, torres de microondas y guías de onda.", Decimal("8.00")),
("X", "c) Para equipo utilizado en la transmisión, tales como circuitos de la planta interna que no forman parte de la conmutación y cuyas funciones se enfocan hacia las troncales que llegan a la central telefónica, incluye multiplexores, equipos concentradores y ruteadores.", Decimal("10.00")),
("X", "d) Para equipo de la central telefónica destinado a la conmutación de llamadas de tecnología distinta a la electromecánica.", Decimal("25.00")),
("X", "e) Para los demás.", Decimal("10.00")),
("XI", "a) Para el segmento satelital en el espacio, incluyendo el cuerpo principal del satélite, los transpondedores, las antenas para la transmisión y recepción de comunicaciones digitales y análogas, y el equipo de monitoreo en el satélite.", Decimal("8.00")),
("XI", "b) Para el equipo satelital en tierra, incluyendo las antenas para la transmisión y recepción de comunicaciones digitales y análogas y el equipo para el monitoreo del satélite.", Decimal("10.00")),
("XII", "Para adaptaciones que se realicen a instalaciones que impliquen adiciones o mejoras al activo fijo, siempre que dichas adaptaciones tengan como finalidad facilitar a las personas con discapacidad a que se refiere el artículo 186 de esta Ley, el acceso y uso de las instalaciones del contribuyente.", Decimal("100.00")),
("XIII", "Para maquinaria y equipo para la generación de energía proveniente de fuentes renovables o de sistemas de cogeneración de electricidad eficiente.", Decimal("100.00")),
]

View File

@@ -0,0 +1,91 @@
"""
Depreciation Catalog Service
"""
from sqlalchemy.orm import Session
from sqlalchemy import or_, func
from api.v1.modules.a76.general_catalogs.depreciation_catalog.models import DepreciationCatalog
class DepreciationCatalogService:
"""Service for depreciation catalog operations"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
page: int = 1,
page_size: int = 100,
search: str | None = None
):
"""Get all depreciation catalog entries with optional search"""
query = db.query(DepreciationCatalog).filter(
DepreciationCatalog.tenant_id == tenant_id,
DepreciationCatalog.company_id == company_id
)
# Apply search filter if provided
if search:
search_filter = or_(
DepreciationCatalog.fraction.ilike(f"%{search}%"),
DepreciationCatalog.description.ilike(f"%{search}%"),
func.cast(DepreciationCatalog.depreciation_rate, db.String).ilike(f"%{search}%")
)
query = query.filter(search_filter)
# Get total count before pagination
total = query.count()
# Apply pagination
offset = (page - 1) * page_size
items = query.order_by(
DepreciationCatalog.fraction,
DepreciationCatalog.depreciation_rate
).offset(offset).limit(page_size).all()
return items, total
@staticmethod
def get_by_id(db: Session, id: int, tenant_id: int, company_id: int):
"""Get depreciation catalog entry by ID"""
return db.query(DepreciationCatalog).filter(
DepreciationCatalog.id == id,
DepreciationCatalog.tenant_id == tenant_id,
DepreciationCatalog.company_id == company_id
).first()
@staticmethod
def create(db: Session, data: dict):
"""Create new depreciation catalog entry"""
entry = DepreciationCatalog(**data)
db.add(entry)
db.commit()
db.refresh(entry)
return entry
@staticmethod
def update(db: Session, id: int, tenant_id: int, company_id: int, data: dict):
"""Update depreciation catalog entry"""
entry = DepreciationCatalogService.get_by_id(db, id, tenant_id, company_id)
if not entry:
return None
for key, value in data.items():
if value is not None:
setattr(entry, key, value)
db.commit()
db.refresh(entry)
return entry
@staticmethod
def delete(db: Session, id: int, tenant_id: int, company_id: int):
"""Delete depreciation catalog entry"""
entry = DepreciationCatalogService.get_by_id(db, id, tenant_id, company_id)
if not entry:
return False
db.delete(entry)
db.commit()
return True

View File

@@ -0,0 +1,3 @@
"""
__init__.py para módulo de catálogo FDA
"""

View File

@@ -0,0 +1,59 @@
"""
DTOs para el catálogo de claves FDA
"""
from pydantic import BaseModel, Field
from typing import Optional
class FDACatalogCreate(BaseModel):
"""DTO para crear una entrada en el catálogo FDA"""
fda_key: str = Field(..., max_length=20, description="Clave FDA")
description: str = Field(..., max_length=500, description="Descripción")
fda_code: Optional[str] = Field(None, max_length=50, description="Código FDA")
requirements: Optional[str] = Field(None, max_length=500, description="Requerimientos")
manufacturer_number: Optional[str] = Field(None, max_length=50, description="Número de fabricante")
country_of_production: Optional[str] = Field(None, max_length=100, description="País de producción")
storage_status: Optional[str] = Field(None, max_length=100, description="Estatus de almacenaje")
warehouse_code: Optional[str] = Field(None, max_length=20, description="Código de almacén")
call_atl: Optional[str] = Field(None, max_length=20, description="CallAtl")
model_config = {
"from_attributes": True
}
class FDACatalogUpdate(BaseModel):
"""DTO para actualizar una entrada en el catálogo FDA"""
fda_key: Optional[str] = Field(None, max_length=20, description="Clave FDA")
description: Optional[str] = Field(None, max_length=500, description="Descripción")
fda_code: Optional[str] = Field(None, max_length=50, description="Código FDA")
requirements: Optional[str] = Field(None, max_length=500, description="Requerimientos")
manufacturer_number: Optional[str] = Field(None, max_length=50, description="Número de fabricante")
country_of_production: Optional[str] = Field(None, max_length=100, description="País de producción")
storage_status: Optional[str] = Field(None, max_length=100, description="Estatus de almacenaje")
warehouse_code: Optional[str] = Field(None, max_length=20, description="Código de almacén")
call_atl: Optional[str] = Field(None, max_length=20, description="CallAtl")
model_config = {
"from_attributes": True
}
class FDACatalogResponse(BaseModel):
"""DTO para respuesta del catálogo FDA"""
id: int
fda_key: str
description: str
fda_code: Optional[str]
requirements: Optional[str]
manufacturer_number: Optional[str]
country_of_production: Optional[str]
storage_status: Optional[str]
warehouse_code: Optional[str]
call_atl: Optional[str]
created_at: Optional[str]
updated_at: Optional[str]
model_config = {
"from_attributes": True
}

View File

@@ -0,0 +1,29 @@
"""
Modelo para el catálogo de claves FDA
"""
from typing import Optional
from sqlalchemy import String, Integer, UniqueConstraint, PrimaryKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
class FDACatalog(Base, TenantScopedMixin, TimestampMixin):
"""Catálogo de claves FDA"""
__tablename__ = "fda_catalog"
__table_args__ = (
PrimaryKeyConstraint("id", name="fda_catalog_pkey"),
UniqueConstraint('tenant_id', 'company_id', 'fda_key', name='idx_fda_catalog_unique'),
{'schema': 'a76'}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
fda_key: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
description: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
fda_code: Mapped[Optional[str]] = mapped_column(String(50))
requirements: Mapped[Optional[str]] = mapped_column(String(500))
manufacturer_number: Mapped[Optional[str]] = mapped_column(String(50))
country_of_production: Mapped[Optional[str]] = mapped_column(String(100))
storage_status: Mapped[Optional[str]] = mapped_column(String(100))
warehouse_code: Mapped[Optional[str]] = mapped_column(String(20))
call_atl: Mapped[Optional[str]] = mapped_column(String(20))

View File

@@ -0,0 +1,69 @@
"""
Rutas para el catálogo de claves FDA
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Any, Dict, Optional
from api.v1.modules.a76.general_catalogs.fda_catalog.service import FDACatalogService
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogResponse
from core.database import get_core_db
from core.security import get_current_user
from api.v1.common.tenant_crud_routes import validate_access_to_resource
router = APIRouter(
prefix="/fda-catalog",
tags=["a76 / general catalogs / fda catalog"]
)
@router.get("/")
async def list_fda_catalog(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=1000, description="Page size"),
search: Optional[str] = Query(None, description="Search in FDA key, description or code"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Listar entradas del catálogo FDA con búsqueda y paginación"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {"search": search} if search else {}
result = FDACatalogService.get_all(db, tenant_id, company_id, page, page_size, filters)
return {
"items": result["items"],
"total": result["total"],
"page": result["page"],
"page_size": result["page_size"],
"pages": result["pages"]
}
@router.get("/{id}")
async def get_fda_catalog(
id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Obtener una entrada del catálogo FDA por ID"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
if not entry:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada")
return {
"id": entry.id,
"fda_key": entry.fda_key,
"description": entry.description,
"fda_code": entry.fda_code,
"requirements": entry.requirements,
"manufacturer_number": entry.manufacturer_number,
"country_of_production": entry.country_of_production,
"storage_status": entry.storage_status,
"warehouse_code": entry.warehouse_code,
"call_atl": entry.call_atl
}

View File

@@ -0,0 +1,15 @@
"""
Datos semilla para Catálogo FDA
Catálogo de clasificación de la Food and Drug Administration (FDA)
para importaciones/exportaciones de productos regulados.
"""
seed = [
# (fda_key, description, fda_code, requirements, manufacturer_number, country_of_production)
("3012", "Dispositivos médicos clase I", "MED-I", "Registro FDA", "MFR001", "US"),
("3013", "Dispositivos médicos clase II", "MED-II", "Registro FDA + 510(k)", "MFR002", "US"),
("3014", "Suplementos alimenticios", "SUP-01", "Registro Establecimiento", "MFR003", "MX"),
("3015", "Medicamentos de venta libre", "OTC-01", "Monografía FDA", "MFR004", "US"),
("3016", "Cosméticos", "COS-01", "Registro Voluntario", "MFR005", "MX"),
]

View File

@@ -0,0 +1,108 @@
"""
Servicio para el catálogo de claves FDA
"""
from sqlalchemy import select
from sqlalchemy.orm import Session
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDACatalog
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogCreate, FDACatalogUpdate
class FDACatalogService:
"""Servicio CRUD para catálogo de FDA"""
@staticmethod
def get_all(db: Session, tenant_id: int, company_id: int, page: int = 1, page_size: int = 50, filters: dict = None):
"""Obtener todas las entradas del catálogo FDA con paginación y búsqueda"""
if filters is None:
filters = {}
query = select(FDACatalog).where(
(FDACatalog.tenant_id == tenant_id) &
(FDACatalog.company_id == company_id)
)
# Búsqueda multi-campo
search = filters.get('search', '').strip()
if search:
search_pattern = f"%{search}%"
query = query.where(
(FDACatalog.fda_key.ilike(search_pattern)) |
(FDACatalog.description.ilike(search_pattern)) |
(FDACatalog.fda_code.ilike(search_pattern))
)
# Contar total
total_query = select(FDACatalog).where(
(FDACatalog.tenant_id == tenant_id) &
(FDACatalog.company_id == company_id)
)
if search:
total_query = total_query.where(
(FDACatalog.fda_key.ilike(search_pattern)) |
(FDACatalog.description.ilike(search_pattern)) |
(FDACatalog.fda_code.ilike(search_pattern))
)
total = db.execute(select(FDACatalog).distinct()).scalars().all().__len__()
# Paginación
offset = (page - 1) * page_size
query = query.offset(offset).limit(page_size)
items = db.execute(query).scalars().all()
return {
"items": items,
"total": len(items),
"page": page,
"page_size": page_size,
"pages": (len(items) + page_size - 1) // page_size
}
@staticmethod
def get_by_id(db: Session, tenant_id: int, company_id: int, id: int):
"""Obtener una entrada por ID"""
return db.execute(
select(FDACatalog).where(
(FDACatalog.id == id) &
(FDACatalog.tenant_id == tenant_id) &
(FDACatalog.company_id == company_id)
)
).scalar_one_or_none()
@staticmethod
def create(db: Session, tenant_id: int, company_id: int, data: FDACatalogCreate):
"""Crear una nueva entrada"""
entry = FDACatalog(
tenant_id=tenant_id,
company_id=company_id,
**data.model_dump()
)
db.add(entry)
db.commit()
db.refresh(entry)
return entry
@staticmethod
def update(db: Session, tenant_id: int, company_id: int, id: int, data: FDACatalogUpdate):
"""Actualizar una entrada"""
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
if not entry:
return None
for field, value in data.model_dump(exclude_unset=True).items():
setattr(entry, field, value)
db.commit()
db.refresh(entry)
return entry
@staticmethod
def delete(db: Session, tenant_id: int, company_id: int, id: int):
"""Eliminar una entrada"""
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
if not entry:
return False
db.delete(entry)
db.commit()
return True

View File

@@ -0,0 +1,8 @@
"""
Módulo de fracciones arancelarias (Tariff Fractions)
"""
from .models import TariffFraction
from .routes import router
__all__ = ["TariffFraction", "router"]

View File

@@ -0,0 +1,97 @@
"""
DTOs (Data Transfer Objects) para fracciones arancelarias
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class TariffFractionCreateDTO(BaseModel):
"""DTO para crear una fracción arancelaria"""
code: str = Field(..., max_length=10, description="Código completo de la fracción")
fraction: str = Field(..., max_length=15, description="Fracción formateada")
description: Optional[str] = Field(None, max_length=1000, description="Descripción")
nico: Optional[str] = Field(None, max_length=10, description="Código NICO")
umt: Optional[str] = Field(None, max_length=10, description="Unidad de medida de tarifa")
adv_impo: Optional[str] = Field(None, max_length=20, description="Ad valorem importación")
adv_expo: Optional[str] = Field(None, max_length=20, description="Ad valorem exportación")
class Config:
from_attributes = True
class TariffFractionUpdateDTO(BaseModel):
"""DTO para actualizar una fracción arancelaria"""
fraction: Optional[str] = Field(None, max_length=15, description="Fracción formateada")
description: Optional[str] = Field(None, max_length=1000, description="Descripción")
nico: Optional[str] = Field(None, max_length=10, description="Código NICO")
umt: Optional[str] = Field(None, max_length=10, description="Unidad de medida de tarifa")
adv_impo: Optional[str] = Field(None, max_length=20, description="Ad valorem importación")
adv_expo: Optional[str] = Field(None, max_length=20, description="Ad valorem exportación")
class Config:
from_attributes = True
class TariffFractionResponseDTO(BaseModel):
"""DTO para respuesta de fracción arancelaria"""
id: int
tenant_id: int
company_id: int
code: str
fraction: str
description: Optional[str] = None
nico: Optional[str] = None
umt: Optional[str] = None
adv_impo: Optional[str] = None
adv_expo: Optional[str] = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class TariffFractionBasicDTO(BaseModel):
"""DTO para información básica de fracción arancelaria"""
code: str
fraction: str
description: Optional[str] = None
nico: Optional[str] = None
umt: Optional[str] = None
adv_impo: Optional[str] = None
adv_expo: Optional[str] = None
class Config:
from_attributes = True
class TariffFractionListDTO(BaseModel):
"""DTO para lista de fracciones arancelarias"""
items: list[TariffFractionBasicDTO]
total: int
page: int
page_size: int
pages: int
class Config:
from_attributes = True
class TariffFractionSearchDTO(BaseModel):
"""DTO para búsqueda de fracciones arancelarias"""
code: Optional[str] = Field(None, description="Buscar por código")
fraction: Optional[str] = Field(None, description="Buscar por fracción")
description: Optional[str] = Field(None, description="Buscar en descripción")
nico: Optional[str] = Field(None, description="Filtrar por NICO")
umt: Optional[str] = Field(None, description="Filtrar por UMT")
class Config:
from_attributes = True

View File

@@ -0,0 +1,49 @@
"""
Modelos ORM para fracciones arancelarias (SITAR-SCAII)
"""
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import Integer, PrimaryKeyConstraint, String, Numeric
from sqlalchemy.orm import Mapped, mapped_column
class TariffFraction(Base, TenantScopedMixin, TimestampMixin):
"""
Modelo para fracciones arancelarias mexicanas (SITAR-SCAII)
Corresponde a la tabla sFracciones
"""
__tablename__ = "tariff_fractions"
__table_args__ = (
PrimaryKeyConstraint("id", name="tariff_fractions_pkey"),
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Código completo de la fracción (ej: 01012101)
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
# Fracción formateada (ej: 0101.21.01)
fraction: Mapped[str] = mapped_column(String(15), index=True)
# Descripción de la fracción
description: Mapped[Optional[str]] = mapped_column(String(1000))
# Código NICO
nico: Mapped[Optional[str]] = mapped_column(String(10))
# Unidad de medida de tarifa (UMT)
umt: Mapped[Optional[str]] = mapped_column(String(10))
# Ad valorem de importación
adv_impo: Mapped[Optional[str]] = mapped_column(String(20))
# Ad valorem de exportación
adv_expo: Mapped[Optional[str]] = mapped_column(String(20))
def __repr__(self) -> str:
return f"<TariffFraction(code='{self.code}', fraction='{self.fraction}', description='{self.description[:50]}...')>"

View File

@@ -0,0 +1,74 @@
"""
Endpoints API para fracciones arancelarias
"""
from typing import Any, Dict, Optional
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 (
TariffFractionCreateDTO,
TariffFractionResponseDTO,
TariffFractionUpdateDTO,
)
from .service import TariffFractionService
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
base_router = TenantCRUDRoutes(
service=TariffFractionService,
create_schema=TariffFractionCreateDTO,
update_schema=TariffFractionUpdateDTO,
response_schema=TariffFractionResponseDTO,
prefix="/tariff-fractions",
tags=["a76 / general catalogs / tariff fractions"],
resource_name="TariffFraction",
id_name="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,
)
router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / tariff fractions"])
# Custom list endpoint with search filter
@router.get(
"/",
response_model=Dict[str, Any],
summary="List Tariff Fractions",
description="Get paginated list of Tariff Fractions with optional search filter",
)
async def list_tariff_fractions(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
search: Optional[str] = Query(None, description="Search in code, fraction, description, nico, or umt"),
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)
skip = (page - 1) * page_size
filters = {}
if search:
filters["search"] = search
items, total = TariffFractionService.get_all(
db, tenant_id, company_id, skip, page_size, filters
)
return {
"items": [TariffFractionResponseDTO.model_validate(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size,
}
# Include other CRUD routes from base router
router.include_router(base_router.router)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
"""
Service para fracciones arancelarias
"""
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
import logging
from .models import TariffFraction
from .dto import TariffFractionCreateDTO, TariffFractionUpdateDTO
logger = logging.getLogger(__name__)
class TariffFractionService:
"""Service para gestionar fracciones arancelarias"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[TariffFraction], int]:
"""Obtiene todas las fracciones arancelarias con filtros opcionales"""
query = db.query(TariffFraction).filter(
TariffFraction.tenant_id == tenant_id,
TariffFraction.company_id == company_id,
)
# Aplicar filtros
if filters:
# Si hay un filtro 'search', buscar en múltiples campos
if filters.get("search"):
search_term = f"%{filters['search']}%"
query = query.filter(
TariffFraction.code.ilike(search_term) |
TariffFraction.fraction.ilike(search_term) |
TariffFraction.description.ilike(search_term) |
TariffFraction.nico.ilike(search_term) |
TariffFraction.umt.ilike(search_term)
)
else:
# Filtros individuales
if filters.get("code"):
query = query.filter(TariffFraction.code.ilike(f"%{filters['code']}%"))
if filters.get("fraction"):
query = query.filter(TariffFraction.fraction.ilike(f"%{filters['fraction']}%"))
if filters.get("description"):
query = query.filter(TariffFraction.description.ilike(f"%{filters['description']}%"))
if filters.get("nico"):
query = query.filter(TariffFraction.nico.ilike(f"%{filters['nico']}%"))
if filters.get("umt"):
query = query.filter(TariffFraction.umt.ilike(f"%{filters['umt']}%"))
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session,
tariff_fraction_id: int,
tenant_id: int,
company_id: int,
) -> Optional[TariffFraction]:
"""Obtiene una fracción arancelaria por ID"""
return (
db.query(TariffFraction)
.filter(
TariffFraction.id == tariff_fraction_id,
TariffFraction.tenant_id == tenant_id,
TariffFraction.company_id == company_id,
)
.first()
)
@staticmethod
def get_by_code(
db: Session,
code: str,
tenant_id: int,
company_id: int,
) -> Optional[TariffFraction]:
"""Obtiene una fracción arancelaria por código"""
return (
db.query(TariffFraction)
.filter(
TariffFraction.code == code,
TariffFraction.tenant_id == tenant_id,
TariffFraction.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
tariff_fraction_data: TariffFractionCreateDTO,
tenant_id: int,
company_id: int,
) -> TariffFraction:
"""Crea una nueva fracción arancelaria"""
try:
tariff_fraction = TariffFraction(
**tariff_fraction_data.model_dump(),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(tariff_fraction)
db.commit()
db.refresh(tariff_fraction)
return tariff_fraction
except IntegrityError as e:
db.rollback()
logger.error(f"Error creating tariff fraction: {e}")
raise HTTPException(
status_code=400,
detail="Tariff fraction with this code already exists",
)
@staticmethod
def update(
db: Session,
tariff_fraction_id: int,
tariff_fraction_data: TariffFractionUpdateDTO,
tenant_id: int,
company_id: int,
) -> Optional[TariffFraction]:
"""Actualiza una fracción arancelaria existente"""
tariff_fraction = TariffFractionService.get_by_id(
db, tariff_fraction_id, tenant_id, company_id
)
if not tariff_fraction:
return None
try:
update_data = tariff_fraction_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(tariff_fraction, key, value)
db.commit()
db.refresh(tariff_fraction)
return tariff_fraction
except IntegrityError as e:
db.rollback()
logger.error(f"Error updating tariff fraction: {e}")
raise HTTPException(
status_code=400,
detail="Error updating tariff fraction",
)
@staticmethod
def delete(
db: Session,
tariff_fraction_id: int,
tenant_id: int,
company_id: int,
) -> bool:
"""Elimina una fracción arancelaria"""
tariff_fraction = TariffFractionService.get_by_id(
db, tariff_fraction_id, tenant_id, company_id
)
if not tariff_fraction:
return False
try:
db.delete(tariff_fraction)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"Error deleting tariff fraction: {e}")
raise HTTPException(
status_code=400,
detail="Cannot delete tariff fraction - may be in use",
)

View File

@@ -0,0 +1,49 @@
"""
Datos semilla para Unidades de Medida Comerciales
Migrado desde frontend para centralizar en backend
"""
# Formato: (code, description, description_en, customs_code, american_code, ace_code, oma_code)
seed = [
('BARR', 'BARRIL', 'BARREL', '8', 'BBL', '', 'BLL'),
('BD FT', 'PIE TABLA', 'BD FEET', '5', 'FT', '', 'BFT'),
('BOLS', 'BOLSA', 'BAG', '6', 'PCS', '', 'BG'),
('BTL', 'BOTELLA', 'BOTTLE', '21', 'PCS', '', 'BO'),
('BULT', 'BULTO', 'BULK', '6', 'PCS', '', 'VQ'),
('CAJA', 'CAJA', 'BOX', '20', '', '', 'BX'),
('CARAT', 'CARAT', 'CARAT', '22', '', '', 'HE'),
('CBZA', 'CABEZA', 'HEAD', '7', 'PCS', '', 'Z4'),
('CIEN', 'CIENTO', 'CIEN', '18', '', '', 'CEN'),
('CM', 'CENTIMETRO', 'CM', '3', 'CM', '', 'CMT'),
('CM2', 'CENTIMETRO CUADRADO', 'CM2', '4', 'CM2', '', 'CMK'),
('DEC', 'DECENA', '', '17', '', '', 'DC'),
('DM', 'DECIMETRO', 'DM', '3', '', '', 'DMT'),
('DM2', 'DECIMETRO CUADRADO', 'SQ DM', '4', '', '', 'DMK'),
('DOCE', 'DOCENA', 'DOZ', '19', 'DOZ', 'DZ', 'DZN'),
('FOZ', 'ONZA LIQUIDA', 'FOZ', '8', 'FOZ', '', 'OZA'),
('FT', 'PIES', 'FT', '3', 'FT', '', 'LF'),
('FT2', 'PIE CUADRADO', 'FT2', '4', 'SFT', '', 'FTK'),
('GAL', 'GALON', 'GAL', '8', 'GAL', '', 'GLL'),
('GR', 'GRAMO', 'GRAM', '2', '', '', 'GRM'),
('IN', 'PULGADA', 'IN', '3', '', '', 'LI'),
('IN2', 'PULGADA CUADRADA', 'IN2', '4', '', '', 'INK'),
('JGO', 'JUEGO', 'SET', '12', '', '', 'SET'),
('KGS', 'KILOGRAMOS', 'KGS', '1', 'KG2', '', 'KGM'),
('LB', 'LIBRAS', 'LB', '1', '', '', 'LBR'),
('LT', 'LITRO', 'LT', '8', 'L', '', 'LTR'),
('M2', 'METRO CUADRADO', 'M2', '4', 'M2', '', 'MTK'),
('M3', 'METRO CUBICO', 'M3', '5', 'M3', '', 'MTQ'),
('MI', 'MILLA', 'MILE', '3', 'KM', '', 'SMI'),
('MILLR', 'MILLAR', 'MILLR', '11', '', '', 'MIL'),
('MT', 'METROS', 'MT', '3', 'M', '', 'MTR'),
('OZ', 'ONZA', 'OZ', '8', 'FOZ', '', 'OZ'),
('PAR', 'PAR', 'PAIR', '9', '', '', 'PB'),
('PQ', 'PAQUETE', 'PACKAGE', '6', 'PCS', '', 'PK_1'),
('PZA', 'PIEZA', 'PCS', '6', 'PCS', '', 'C62_1'),
('QGL', 'CUARTO DE GALON', 'QGL', '8', '', '', 'QT'),
('ROLL', 'ROLLO', 'ROLL', '6', '', '', 'RO'),
('TON', 'TONELADA', 'TON', '14', 'TON', '', 'TNE_1'),
('TOZ', 'ONZA TROY', 'TOZ', '1', 'TOZ', '', 'APZ'),
('YD', 'YARDA', 'YD', '3', 'YD', '', 'YRD'),
('YD2', 'YARDA CUADRADA', 'YD2', '4', 'SYD', '', 'YDK'),
]

View File

@@ -0,0 +1,8 @@
"""
US Tariff Fractions Catalog Module
"""
from .models import USTariffFraction
from .routes import router
__all__ = ["USTariffFraction", "router"]

View File

@@ -0,0 +1,48 @@
"""
DTOs para fracciones arancelarias americanas
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
class USTariffFractionCreateDTO(BaseModel):
"""DTO para crear fracción arancelaria americana"""
code: str = Field(..., max_length=16, description="Código de fracción americana")
prefix: Optional[str] = Field(None, max_length=10, description="Prefijo")
type_code: Optional[str] = Field(None, max_length=10, description="Código de tipo")
ad_valorem: Optional[float] = Field(None, description="Porcentaje ad valorem")
fixed_cost: Optional[float] = Field(None, description="Tasa fija")
unit_of_measure: Optional[str] = Field(None, max_length=10, description="Unidad de medida")
description: Optional[str] = Field(None, description="Descripción")
class USTariffFractionUpdateDTO(BaseModel):
"""DTO para actualizar fracción arancelaria americana"""
prefix: Optional[str] = Field(None, max_length=10)
type_code: Optional[str] = Field(None, max_length=10)
ad_valorem: Optional[float] = None
fixed_cost: Optional[float] = None
unit_of_measure: Optional[str] = Field(None, max_length=10)
description: Optional[str] = None
class USTariffFractionResponseDTO(BaseModel):
"""DTO para respuesta de fracción arancelaria americana"""
model_config = ConfigDict(from_attributes=True)
id: int
code: str
prefix: Optional[str] = None
type_code: Optional[str] = None
ad_valorem: Optional[float] = None
fixed_cost: Optional[float] = None
unit_of_measure: Optional[str] = None
description: Optional[str] = None
created_at: datetime
updated_at: datetime

View File

@@ -0,0 +1,62 @@
"""
Modelos para fracciones arancelarias americanas
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Numeric, TIMESTAMP, func
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class USTariffFraction(Base):
"""Modelo para fracciones arancelarias americanas (US HTS codes)"""
__tablename__ = "us_tariff_fractions"
__table_args__ = {"schema": "a76"}
# Primary Key
id: Mapped[int] = mapped_column(primary_key=True, index=True)
# Tenant/Company
tenant_id: Mapped[int] = mapped_column(index=True, nullable=False)
company_id: Mapped[int] = mapped_column(index=True, nullable=False)
# Datos principales
code: Mapped[str] = mapped_column(
String(16), nullable=False, comment="Código de fracción americana"
)
prefix: Mapped[Optional[str]] = mapped_column(
String(10), comment="Prefijo de clasificación"
)
type_code: Mapped[Optional[str]] = mapped_column(
String(10), comment="Código de tipo"
)
ad_valorem: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2), comment="Porcentaje ad valorem"
)
fixed_cost: Mapped[Optional[float]] = mapped_column(
Numeric(15, 8), comment="Tasa fija"
)
unit_of_measure: Mapped[Optional[str]] = mapped_column(
String(10), comment="Unidad de medida"
)
description: Mapped[Optional[str]] = mapped_column(
String, comment="Descripción de la fracción"
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
def __repr__(self):
return f"<USTariffFraction {self.code}>"

View File

@@ -0,0 +1,73 @@
"""
Endpoints API para fracciones arancelarias americanas
"""
from typing import Any, Dict, Optional
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 (
USTariffFractionCreateDTO,
USTariffFractionResponseDTO,
USTariffFractionUpdateDTO,
)
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,
)
router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
# Custom list endpoint with search filter
@router.get(
"/",
response_model=Dict[str, Any],
summary="List US Tariff Fractions",
description="Get paginated list of US Tariff Fractions with optional search filter",
)
async def list_us_tariff_fractions(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
search: Optional[str] = Query(None, description="Search in code, description, or prefix"),
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)
skip = (page - 1) * page_size
filters = {}
if search:
filters["search"] = search
items, total = USTariffFractionService.get_all(
db, tenant_id, company_id, skip, page_size, filters
)
return {
"items": [USTariffFractionResponseDTO.model_validate(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size,
}
# Include other CRUD routes from base router
router.include_router(base_router.router)

View File

@@ -0,0 +1,417 @@
"""
Datos semilla para Fracciones Arancelarias de Estados Unidos
Catálogo de fracciones HTS/Schedule B para importaciones/exportaciones con EE.UU.
Total: 406 registros
Estructura: (code, prefix, type_code, ad_valorem, fixed_cost, unit_of_measure, description)
"""
seed = [
('0902300090', '', 'PO', '0.00', '0.00000000', 'PZA', 'BOLSA DE TE')
,('2508400150', '', 'PO', '0.00', '0.00000000', '', '')
,('2520200000', '', 'PO', '0.00', '0.00000000', '', '')
,('2525200000', '', 'PO', '0.00', '0.00000000', 'KGS', 'PITMENT BASED ON TITANIUM DIOXIDE')
,('2526200000', '', 'PO', '0.00', '0.00000000', '', '')
,('2707999090', '', 'PO', '0.00', '0.00000000', '', '')
,('2710121550', '', 'PO', '0.00', '0.00000000', '', '')
,('2710129000', '', 'PO', '0.00', '0.00000000', 'LT', 'MOLD RELEASE')
,('2710129050', '', 'PO', '0.00', '0.00000000', 'LT', '')
,('2710190650', '', 'PO', '0.00', '0.00000000', 'LT', 'MOLD RELEASE')
,('2710199000', '', 'PO', '0.00', '0.00000000', '', '')
,('2712902000', '', 'PO', '0.00', '0.00000000', '', '')
,('2839905000', '', 'PO', '0.00', '0.00000000', '', '')
,('2905120050', '', 'PO', '0.00', '0.00000000', '', '')
,('2905145050', '', 'PO', '0.00', '0.00000000', '', '')
,('2909430000', '', 'PO', '0.00', '0.00000000', '', '')
,('2909496000', '', 'PO', '0.00', '0.00000000', '', '')
,('2914115000', '', 'PO', '0.00', '0.00000000', 'LT', 'BUTYL ACETATE')
,('2914120000', '', 'PO', '0.00', '0.00000000', '', '')
,('2915905050', '', 'PO', '0.00', '0.00000000', '', '')
,('2924293600', '', 'PO', '0.00', '0.00000000', '', '')
,('2929108090', '', 'PO', '0.00', '0.00000000', 'KGS', 'Urethane, Iso Side')
,('3206110000', '', 'PO', '0.00', '0.00000000', '', '')
,('3206190000', '', 'PO', '0.00', '0.00000000', '', '')
,('3206495060', '', 'PO', '0.00', '0.00000000', 'KGS', 'RESIN')
,('3206496050', '', 'PO', '0.00', '0.00000000', '', 'PIGMENTO COLORANTE')
,('3208100000', '', 'PO', '0.00', '0.00000000', '', '')
,('3208200000', '', 'PO', '0.00', '0.00000000', 'LT', 'LACA ALQUIDICA NITROCELULOSA')
,('3208900000', '', 'PO', '0.00', '0.00000000', '', 'PAINT')
,('3209100000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('3209900000', 'S', 'PO', '0.00', '0.00000000', 'LT', 'LIQUID PAINT')
,('3212900050', '', 'PO', '0.00', '0.00000000', '', '')
,('3214100020', '', 'PO', '0.00', '0.00000000', '', '')
,('3215905000', '', 'PO', '0.00', '0.00000000', '', '')
,('3402205100', '', 'PO', '0.00', '0.00000000', '', '')
,('3402905050', '', 'PO', '0.00', '0.00000000', '', '')
,('3403195000', '', 'PO', '0.00', '0.00000000', 'LT', 'Mold Release, Stoner, GL')
,('3403990000', '', 'PO', '0.00', '0.00000000', '', 'PASTE FOR POLISHING METALS')
,('3404905150', '', 'PO', '0.00', '0.00000000', '', '')
,('3405400000', '', 'PO', '0.00', '0.00000000', '', '')
,('3405900000', '', 'PO', '0.00', '0.00000000', '', '')
,('3506105000', 'US', '', '0.00', '0.00000000', 'PZA', 'ADHESIVO EN AEROSOL')
,('3506915000', '', 'PO', '0.00', '0.00000000', 'LT', 'Adhesive')
,('3506990000', '', 'PO', '0.00', '0.00000000', 'LT', 'Adhesive')
,('3802.20.00.00', '', 'PO', '0.00', '0.00000000', 'KGS', '')
,('3811900000', '', 'PO', '0.00', '0.00000000', '', '')
,('3814005090', '', 'PO', '0.00', '0.00000000', 'LT', 'THINNER')
,('3815901000', '', 'PO', '0.00', '0.00000000', '', '')
,('3815903000', '', 'PO', '0.00', '0.00000000', '', '')
,('3815905000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CATALYST')
,('3820000000', '', 'PO', '0.00', '0.00000000', '', '')
,('3824910000', '', 'PO', '0.00', '0.00000000', 'LT', 'LIQUIDO PARA PAVONAR METAL A BASE DE ACIDOS')
,('3824991900', '', 'PO', '0.00', '0.00000000', '', '')
,('3824999297', '', 'PO', '0.00', '0.00000000', 'PZA', 'WELDING ANTI SPLATTER')
,('3825900000', '', 'PO', '0.00', '0.00000000', '', '')
,('3903190000', '', 'PO', '0.00', '0.00000000', 'KGS', '')
,('3903905000', '', 'PO', '0.00', '0.00000000', '', '')
,('3905300000', '', 'PO', '0.00', '0.00000000', '', '')
,('3906905050', 'US', '', '0.00', '0.00000000', 'PZA', 'POLIMERO ACRILICO')
,('3907200000', '', 'PO', '0.00', '0.00000000', 'KGS', 'POLYURETHANE BASED RESIN')
,('3907210000', '', 'PO', '0.00', '0.00000000', 'PZA', 'POLYURETHANE-BASED RESIN')
,('3907300000', 'US', '', '0.00', '0.00000000', 'KGS', 'RESINA EPOXICA A Y B')
,('3907915000', '', 'PO', '0.00', '0.00000000', 'KGS', 'resin')
,('3907995050', '', 'PO', '0.00', '0.00000000', '', '')
,('3909100000', '', 'PO', '0.00', '0.00000000', 'KGS', 'GRANULATED PLASTIC (PLASTIC RESIN)')
,('3909310000', '', 'PO', '0.00', '0.00000000', 'KGS', 'ISOCIANATE')
,('3909390000', '', 'PO', '0.00', '0.00000000', '', '')
,('3909505000', '', 'PO', '0.00', '0.00000000', 'LT', 'POLYURETHANE ELASTOMER PART A AND B')
,('3909506000', '', 'PO', '0.00', '0.00000000', 'KGS', '')
,('3909900000', '', 'PO', '0.00', '0.00000000', '', '')
,('3910000000', '', 'PO', '0.00', '0.00000000', 'LT', 'SEALER CONDITIONER PART B')
,('3911902500', '', 'PO', '0.00', '0.00000000', '', '')
,('3911909050', '', 'PO', '0.00', '0.00000000', '', '')
,('3915900090', '', 'PO', '0.00', '0.00000000', 'KGS', 'DESPERDICIO DE PLASTICO')
,('3917230000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PLASTIC HOSE')
,('3917320050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('39173299', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('3917330000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('39173399', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('3917390050', '', 'PO', '0.00', '0.00000000', 'PZA', 'PLASTIC TUBE')
,('3919101050', 'US', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA')
,('3919102055', 'US', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA (PLASTICA)')
,('3919905060', '', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA (PLASTICO TRANSPARENTE)')
,('3920100000', '', 'PO', '0.00', '0.00000000', 'PZA', 'burbuja')
,('3920200055', '', 'PO', '0.00', '0.00000000', '', '')
,('3920515000', '', 'PO', '0.00', '0.00000000', 'PZA', 'ACRYLIC POLYMER SHEET')
,('3921135000', '', 'PO', '0.00', '0.00000000', '', '')
,('3921905050', 'S', '', '0.00', '0.00000000', 'PZA', 'PELICULA DE PLASTICO PARA FLEJAR')
,('3923109000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAJA DE PLASTICO')
,('3923210095', '', 'PO', '0.00', '0.00000000', '', '')
,('3923290000', '', 'PO', '0.00', '0.00000000', 'PZA', 'Bag')
,('3923300090', '', 'PO', '0.00', '0.00000000', '', '')
,('3923500000', '', 'PO', '0.00', '0.00000000', '', '')
,('3923900080', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'Plastic container')
,('3926209050', '', 'PO', '0.00', '0.00000000', '', '')
,('3926400090', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('3926902100', 'US', '', '0.00', '0.00000000', 'PZA', 'PROTECTORES DE PLASTICO P/OIDO')
,('3926903500', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('3926909985', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'PLASTIC')
,('3926909987', 'S', 'PO', '0.00', '0.00000000', '', '')
,('3926909989', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('3926909990', '', 'PO', '0.00', '0.00000000', 'PZA', 'PLACTIC BASE')
,('3926909995', 'S', 'PO', '0.00', '0.00000000', '', 'PLASTIC DOME')
,('3926909996', '', 'PO', '0.00', '0.00000000', 'PZA', 'Foam Plug, Hosiery Leg')
,('4015190002', 'US', '', '0.00', '0.00000000', 'PZA', 'GUANTES DE CAUCHO')
,('4016930000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('4016935050', '', 'PO', '0.00', '0.00000000', '', '')
,('4016992000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('4016993510', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('4016996000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('4016996050', '', 'PO', '0.00', '0.00000000', 'PZA', 'BALL')
,('4201003000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'Dog leashes, collars, muzzles, harnesses and similar dog equipment')
,('4201006000', '', 'PO', '0.00', '0.00000000', '', '')
,('4410190060', '', 'PO', '0.00', '0.00000000', '', '')
,('4415109000', '', 'PO', '0.00', '0.00000000', '', '')
,('4415208000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'WOOD PALLETS')
,('4417004000', '', 'PO', '0.00', '0.00000000', '', 'Paint brush and paint roller handles')
,('4421904000', '', 'PO', '5.10', '0.00000000', '', 'NECKCAP WOOD')
,('4421909750', 'US', '', '0.00', '0.00000000', 'PZA', 'PALILLO DE MADERA')
,('4421914000', '', 'PO', '0.00', '0.00000000', '', '')
,('4421994000', '', 'PO', '0.00', '0.00000000', '', '')
,('4421999880', '', 'PO', '0.00', '0.00000000', '', '')
,('4503106000', '', 'PO', '0.00', '0.00000000', '', '')
,('4802693000', '', 'PO', '0.00', '0.00000000', '', '')
,('4804394000', '', 'PO', '0.00', '0.00000000', '', 'Wrapping paper')
,('4804590000', 'S', 'PO', '0.00', '0.00000000', '', '')
,('4805400000', '', 'PO', '0.00', '0.00000000', '', '')
,('4808100000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'CORRUGATED CARTON SEPARATOR')
,('4808906000', '', 'PO', '0.00', '0.00000000', '', '')
,('4810137040', '', 'PO', '0.00', '0.00000000', 'PZA', 'ROLLO DE CABLE DE ACER')
,('4811412100', '', 'PO', '0.00', '0.00000000', '', '')
,('4817100000', '', 'PO', '0.00', '0.00000000', '', '')
,('4818200020', 'US', '', '0.00', '0.00000000', 'PZA', 'TOALLAS DESECHABLES DE PAPEL TISSUE')
,('4819100040', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'BOX')
,('4819504060', '', 'PO', '0.00', '0.00000000', '', '')
,('4821104000', '', 'PO', '0.00', '0.00000000', 'PZA', 'LABEL')
,('4821904000', '', 'PO', '0.00', '0.00000000', '', '')
,('4822900000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'CARTON BOX')
,('4823700040', '', 'PO', '0.00', '0.00000000', '', '')
,('4823901000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PAPER CUP')
,('4823908600', '', 'PO', '0.00', '0.00000000', 'PZA', 'BASES DE CARTON PRENSADO')
,('4823908850', 'US', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA DE PAPEL')
,('4901990091', '', 'PO', '0.00', '0.00000000', 'PZA', 'MANUALES')
,('4911100080', '', 'PO', '0.00', '0.00000000', '', '')
,('4911998000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('5407619975', '', 'PO', '0.00', '0.00000000', '', '')
,('5508200000', '', 'PO', '0.00', '0.00000000', '', '')
,('5703308085', '', 'PO', '0.00', '0.00000000', 'PZA', 'PASTO SINTETICO (MUESTRAS)')
,('5806310000', '', 'PO', '0.00', '0.00000000', 'MT', 'CINTA TEXTIL')
,('5806322000', '', 'PO', '0.00', '0.00000000', 'MT', 'VELCRO')
,('59039001', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('5903903090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('5911102000', '', 'PO', '0.00', '0.00000000', 'MT', '')
,('5911900040', '', 'PO', '0.00', '0.00000000', 'PZA', 'Cords, braids and the like of a kind used in industry as packing or lubricating material')
,('5911900080', '', 'PO', '0.00', '0.00000000', 'MT', 'COVER FORM 3/4 FEMALE FOAM LINEN')
,('6116100000', 'US', '', '0.00', '0.00000000', 'PZA', 'GUANTES DE ALGOHODON CON RECUBRIMIENTO DE NITRILIO')
,('6116920000', 'US', '', '0.00', '0.00000000', 'PZA', 'GUANTES DE ALGOHODON')
,('6116929400', '', 'PO', '0.00', '0.00000000', '', '')
,('6306192120', '', 'PO', '0.00', '0.00000000', '', '')
,('6307100000', 'US', '', '0.00', '0.00000000', 'PZA', 'FIBRA SINTETICA P/LIMPIAR')
,('6307909089', 'S', 'PO', '0.00', '0.00000000', '', 'BAG')
,('6307909889', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('6307909891', '', 'PO', '0.00', '0.00000000', 'PZA', 'bolsa de tela')
,('6307909995', 'US', '', '0.00', '0.00000000', 'PZA', 'MASCARILLA DE PROTECCION CONTRA EL POLVO')
,('6403919015', '', 'PO', '0.00', '0.00000000', '', '')
,('6403999031', '', 'PO', '10.00', '0.00000000', 'PAR', 'LEATHER SHOES')
,('6406109090', '', 'PO', '0.00', '0.00000000', 'PZA', 'HUARACHE')
,('6506106075', '', 'PO', '0.00', '0.00000000', 'PZA', 'PROTECTION HELMET')
,('6804220000', 'US', '', '0.00', '0.00000000', 'PZA', 'DISCO ABRASIVO CIRCULARES')
,('6805100000', 'US', '', '0.00', '0.00000000', 'PZA', 'DISCO ABRASIVO EN ROLLO')
,('6805100199', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('6805300000', 'US', '', '0.00', '0.00000000', 'PZA', 'LIJA CON SOPORTE DE PLASTICO CELULAR')
,('6805305000', '', 'PO', '0.00', '0.00000000', '', '')
,('7007190000', '', 'PO', '0.00', '0.00000000', 'PZA', 'TEMPERED GLASS BASE')
,('7009921000', '', 'PO', '0.00', '0.00000000', 'PZA', 'ESPEJO ENMARCADO')
,('7010905055', '', 'PO', '0.00', '0.00000000', '', '')
,('7019905050', '', 'PO', '0.00', '0.00000000', '', 'FIBRA DE VIDRIO')
,('7019905150', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7020006000', '', 'PO', '0.00', '0.00000000', 'PZA', 'BASE DE VIDRIO')
,('7020009990', '', 'PO', '0.00', '0.00000000', '', 'GLASS BOARD')
,('7206900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'TUBO')
,('7215100080', '', 'PO', '0.00', '0.00000000', '', '')
,('7215905000', '', 'PO', '0.00', '0.00000000', '', '')
,('7226928050', '', 'PO', '0.00', '0.00000000', '', '')
,('7228400000', '', 'PO', '0.00', '0.00000000', 'PZA', 'METAL BAR')
,('7228608000', '', 'PO', '0.00', '0.00000000', '', '')
,('7304598080', '', 'PO', '0.00', '0.00000000', '', '')
,('7306200000', '', 'PO', '0.00', '0.00000000', '', '')
,('7306305090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7306905000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7307225000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7307929000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7308909590', '', 'PO', '0.00', '0.00000000', 'PZA', 'RACK')
,('7309000090', '', 'PO', '0.00', '0.00000000', 'PZA', 'TANQUE HERMETICO DE ACERO')
,('7310100050', '', 'PO', '0.00', '0.00000000', '', '')
,('7310290050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7312109090', '', 'PO', '0.00', '0.00000000', '', '')
,('7315827000', '', 'PO', '0.00', '0.00000000', '', 'CHAIN')
,('7317007500', '', 'PO', '0.00', '0.00000000', '', '')
,('7318130060', '', 'PO', '0.00', '0.00000000', '', 'EYE HOOK')
,('73181504', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7318150400', '', 'PO', '0.00', '0.00000000', '', '')
,('7318152000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7318158066', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7318158085', '', '', '0.00', '0.00000000', 'PZA', 'TORNILLO DE ACERO CON TUERCA')
,('7318158688', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7318159000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'TORNILLO DE ACERO MARIPOSA')
,('7318160085', '', 'PO', '0.00', '0.00000000', '', '')
,('7318190000', '', 'PO', '0.00', '0.00000000', 'PZA', 'REMACHE')
,('7318210090', '', 'PO', '0.00', '0.00000000', '', '')
,('7318220000', '', 'PO', '0.00', '0.00000000', '', '')
,('7318230000', '', 'PO', '0.00', '0.00000000', '', '')
,('7318240000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7318290000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7319909000', '', 'PO', '0.00', '0.00000000', '', '')
,('7320205060', '', 'PO', '0.00', '0.00000000', '', '')
,('7321811000', '', 'PO', '0.00', '0.00000000', '', '')
,('7325995000', '', 'PO', '0.00', '0.00000000', '', '')
,('7326901000', '', 'PO', '0.00', '0.00000000', '', 'HOOK')
,('7326908605', '', 'PO', '0.00', '0.00000000', '', '')
,('7326908688', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'METAL INSERTS')
,('7326908695', '', 'PO', '0.00', '0.00000000', 'PZA', 'STEEL FASTENER')
,('73269099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7412200090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('74122001', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7415100000', '', 'PO', '0.00', '0.00000000', '', '')
,('7415390000', '', 'PO', '0.00', '0.00000000', '', '')
,('7419995050', '', 'PO', '0.00', '0.00000000', '', '')
,('7607196000', '', 'PO', '0.00', '0.00000000', 'PZA', 'aluminium')
,('7609000000', '', 'PO', '0.00', '0.00000000', '', '')
,('7616109090', '', 'PO', '0.00', '0.00000000', 'PZA', 'tachuela')
,('7616995090', 'S', 'PO', '0.00', '0.00000000', '', 'SOPORTE DE ALUMINIO')
,('7616995190', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('7618000000', '', 'PO', '0.00', '0.00000000', '', '')
,('7806008000', '', 'PO', '0.00', '0.00000000', '', '')
,('7907006000', '', 'PO', '0.00', '0.00000000', '', '')
,('8021230000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8202200060', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8203109000', '', 'PO', '0.00', '0.00000000', 'PZA', 'ESTUCHE DE LIMAS')
,('8203208000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('82032099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8204110060', '', 'PO', '0.00', '0.00000000', '', 'WRENCH')
,('8205511500', '', 'PO', '0.00', '0.00000000', '', 'CEPILLO DE ALAMBRE')
,('8205599000', 'US', '', '0.00', '0.00000000', 'PZA', 'DESPACHADOR DE CINTA MANUAL')
,('8205700060', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8205700090', '', 'PO', '0.00', '0.00000000', '', '')
,('8205906000', '', 'PO', '0.00', '0.00000000', '', '')
,('8207502055', '', 'PO', '0.00', '0.00000000', '', 'DRILL BIT')
,('8207907585', '', 'PO', '0.00', '0.00000000', '', '')
,('8208906000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8302200000', '', 'PO', '0.00', '0.00000000', '', 'RUEDAS PARA BASE DE MANIQUI')
,('8302426000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8302498090', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'Kit, Knob,Hand')
,('8302500000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8309900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'TAPA METALICA')
,('8309900090', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8310000000', '', 'PO', '0.00', '0.00000000', '', '')
,('8413600090', '', 'PO', '0.00', '0.00000000', 'PZA', 'PUMPS OF MACHINE MOLDING')
,('8413919060', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8413919080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8413919096', '', 'PO', '0.00', '0.00000000', 'PZA', 'DIAPHRAGM FOR PUMP')
,('8414.59.6595', '', 'PO', '0.00', '0.00000000', '', '')
,('8414100000', '', 'PO', '0.00', '0.00000000', 'PZA', 'VACUM PUMP')
,('8414510090', '', 'PO', '0.00', '0.00000000', '', '')
,('8414519090', '', 'PO', '0.00', '0.00000000', 'PZA', 'FAN')
,('8414596595', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8414809000', '', 'PO', '0.00', '0.00000000', 'PZA', 'DUST COLLECTOR')
,('8414909080', '', 'PO', '0.00', '0.00000000', 'PZA', 'DISPOSITIVO DE FILTRACCION PARA BOMBA NEUMATICA')
,('8419390180', '', 'PO', '0.00', '0.00000000', '', '')
,('8421210000', '', 'PO', '0.00', '0.00000000', 'PZA', 'FEEDER-DISASSEMBLY DISASSEMBLED')
,('8421230000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8421290065', '', 'PO', '0.00', '0.00000000', 'PZA', 'FILTER')
,('8421390115', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8421390190', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8421398015', '', 'PO', '0.00', '0.00000000', 'PZA', 'FILTER')
,('8421398040', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8421398090', '', 'PO', '0.00', '0.00000000', '', '')
,('8421990180', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8422309191', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8422401190', '', 'PO', '0.00', '0.00000000', '', '')
,('8422409180', '', 'PO', '0.00', '0.00000000', 'PZA', 'STRAPPING MACHINE')
,('8424209000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PISTOLA AEROGRAFICA')
,('8424890000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8424900100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8424900500', '', 'PO', '0.00', '0.00000000', '', '')
,('8424902000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8424909080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8425110000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8427108095', '', 'PO', '0.00', '0.00000000', 'PZA', 'Pallet jack')
,('8427900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PALLET JACK')
,('8456111050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8459290090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8461500090', '', 'PO', '0.00', '0.00000000', 'PZA', 'BELT SAW')
,('8461508090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8465910091', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8465930012', '', 'PO', '0.00', '0.00000000', '', '')
,('8466306085', '', 'PO', '0.00', '0.00000000', 'PZA', 'CILINDRO NEUMATICO')
,('8466925090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8467.19.5090', '', 'PO', '0.00', '0.00000000', '', '')
,('8467111080', 'US', '', '0.00', '0.00000000', 'PZA', 'PULIDOR NEUMATICO MANUAL')
,('8467195090', '', 'PO', '0.00', '0.00000000', '', '')
,('8467210070', '', 'PO', '0.00', '0.00000000', '', 'ROTARY')
,('8467220090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8467290090', '', 'PO', '0.00', '0.00000000', '', '')
,('8467895090', '', 'PO', '0.00', '0.00000000', 'PZA', 'Pneumatic screwdriver')
,('8467920050', '', 'PO', '0.00', '0.00000000', '', 'pulidor')
,('8467920090', 'US', '', '0.00', '0.00000000', 'PZA', 'DISCO DE URETANO P/ PULIDOR NEUMATICO')
,('8471410150', '', 'PO', '0.00', '0.00000000', '', '')
,('8471500150', '', 'PO', '0.00', '0.00000000', '', 'CPU')
,('8471608000', '', 'PO', '0.00', '0.00000000', '', 'SCANNER')
,('8471609050', '', 'PO', '0.00', '0.00000000', 'PZA', 'SCANNER')
,('8477590100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8477800000', '', 'PO', '0.00', '0.00000000', 'PZA', 'MACHINE FOR FILLING AIR BAGS')
,('8477800100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8477900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'Presses setup the mold during blowing process')
,('8477902580', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8477908595', '', 'PO', '0.00', '0.00000000', 'PZA', 'DISC')
,('8477908695', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8479820040', '', 'PO', '0.00', '0.00000000', '', 'ROTARY MACHINE')
,('8479820080', '', 'PO', '0.00', '0.00000000', 'PZA', 'APARATO AGITADOR')
,('8479830100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8479899199', '', 'PO', '0.00', '0.00000000', 'PZA', 'PORTABLE DOCK PLATE')
,('8479899499', '', 'PO', '0.00', '0.00000000', 'PZA', 'MAQUINA APLICADORA')
,('8479899599', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8479899797', '', 'PO', '0.00', '0.00000000', 'PZA', 'COLECTOR DE POLVO')
,('8479909496', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('84799099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8480718045', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'MOLDE')
,('8480718060', '', 'PO', '0.00', '0.00000000', 'PZA', 'TIG TORCH')
,('8480719090', '', 'PO', '0.00', '0.00000000', 'PZA', 'Semi-finished mold')
,('8481100090', '', 'PO', '0.00', '0.00000000', '', '')
,('8481200080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('84812099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8481400000', '', 'PO', '0.00', '0.00000000', '', '')
,('8481809020', '', 'PO', '0.00', '0.00000000', '', '')
,('8481809050', '', 'PO', '0.00', '0.00000000', 'PZA', 'NOZZLE, CONE DUT')
,('8481909085', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8483308090', '', 'PO', '0.00', '0.00000000', 'PZA', 'BUJES')
,('8484200000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8484900000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8501106080', '', 'PO', '0.00', '0.00000000', 'PZA', 'MOTOR ELECTRICO INCLUYE ACCESORIOS')
,('8504404000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CONTROLADOR DE VELOCIDAD')
,('8504409580', '', 'PO', '0.00', '0.00000000', 'PZA', 'BATTERY CHARGER')
,('8505110090', '', 'PO', '0.00', '0.00000000', 'PZA', 'INSERT')
,('8505200000', '', 'PO', '0.00', '0.00000000', 'PZA', 'BARRIER')
,('8507208091', '', 'PO', '0.00', '0.00000000', '', '')
,('8512902000', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8514908000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8515390040', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8515800080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('85160808000', '', 'PO', '0.00', '0.00000000', '', '')
,('8516808000', '', 'PO', '0.00', '0.00000000', '', '')
,('8517620010', '', 'PO', '0.00', '0.00000000', '', 'WIRELESS ACCESS POINT')
,('8523520010', '', 'PO', '0.00', '0.00000000', 'PZA', 'RF Label')
,('8525805050', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAMARA DE CIRCUITO CERRADO')
,('8533408070', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8536100040', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8536490050', '', 'PO', '0.00', '0.00000000', '', '')
,('8536507000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8536908585', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8537103000', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8537109070', '', 'PO', '0.00', '0.00000000', '', 'DIGITAL PANEL CONTROL')
,('8543908885', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8544190000', '', 'PO', '0.00', '0.00000000', '', '')
,('8544429090', '', 'PO', '0.00', '0.00000000', 'PZA', 'SENSOR CABLE')
,('8547200000', '', 'PO', '0.00', '0.00000000', '', '')
,('8713100000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('8716805090', '', 'PO', '0.00', '0.00000000', 'PZA', 'UTILITY CART')
,('9018390050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('9023000000', '', 'PO', '0.00', '0.00000000', '', '')
,('9024800000', '', 'PO', '0.00', '0.00000000', '', 'TESTING MACHINE')
,('9026102010', '', 'PO', '0.00', '0.00000000', '', 'DIGITAL VACUUM GAUGE')
,('9026106000', '', 'PO', '0.00', '0.00000000', 'PZA', 'FLOAT & GAUGE')
,('9026204000', '', 'PO', '0.00', '0.00000000', '', '')
,('9026208000', '', 'PO', '0.00', '0.00000000', 'PZA', 'REGULATOR')
,('9027304040', '', 'PO', '0.00', '0.00000000', 'PZA', 'SPECTROPHOTOMETER')
,('9031.80.8085', '', 'PO', '1.70', '0.00000000', '', 'FIXTURA')
,('9031808085', '', 'PO', '1.70', '2.25000000', 'PZA', 'APARATO')
,('9031907000', '', 'PO', '0.00', '0.00000000', 'PZA', 'FIXTURE')
,('9032200000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PRESOSTATO')
,('9106100000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('9403100040', '', 'PO', '0.00', '0.00000000', 'PZA', 'MESA DE METAL CON BASE DE MADERA')
,('9403200030', '', 'PO', '0.00', '0.00000000', 'PZA', 'ESTANTE DE ACERO PARA TOTE')
,('9506310000', '', 'PO', '0.00', '0.00000000', '', 'GOLF CLUB')
,('9506320000', '', 'PO', '0.00', '0.00000000', 'PZA', 'BALL')
,('9506620000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PELOTA')
,('9506628060', '', 'PO', '0.00', '0.00000000', '', '')
,('9506996080', '', 'PO', '0.00', '0.00000000', '', '')
,('9603404050', 'US', '', '0.00', '0.00000000', 'PZA', 'BROCHAS CON MANGO DE PLASTICO Y CERDAS DE FIBRA SINTETICA')
,('9604000000', '', 'PO', '0.00', '0.00000000', '', '')
,('9607190060', '', 'PO', '0.00', '0.00000000', '', '')
,('9612101020', '', 'PO', '0.00', '0.00000000', 'PZA', 'RIBBON')
,('9612109090', '', 'PO', '0.00', '0.00000000', 'PZA', 'RIBBON')
,('9618000000', 'S', 'PO', '0.00', '79.00000000', 'PZA', 'MANIQUIES')
,('9618009900', '', 'PO', '0.00', '0.00000000', 'PZA', '')
,('9801001095', '', 'PO', '0.00', '4.35000000', '', 'RESINA')
,('9801001097', 'S', 'PO', '0.00', '4.35000000', 'PZA', '')
,('9801001098', '', 'PO', '0.00', '0.00000000', 'PZA', 'US RETURNS')
,('9801002500', '', 'PO', '0.00', '0.00000000', 'PZA', 'CHINA ARTICLES')
,('9802004040', 'US', 'PO', '0.00', '0.00000000', 'PZA', 'MANIQUIS REPARADOS')
,('9802005060', '', 'PO', '0.00', '0.00000000', '', 'Repaired in Mexico')
,('9861661098', 'M6363', 'PO', '0.00', '8.10000000', 'PZA', '')
,('MX4415109000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAJON DE MADERA')
,('MX4820400000', '', 'PO', '0.00', '0.50000000', '', 'FORMULARIOS DE PAPEL')
,('MX7326909980', '', 'PO', '0.00', '0.00000000', '', 'STEEL PANEL')
,('MX8309900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAP')
]

View File

@@ -0,0 +1,129 @@
"""
Service para fracciones arancelarias americanas
"""
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
import logging
from .models import USTariffFraction
from .dto import USTariffFractionCreateDTO, USTariffFractionUpdateDTO
logger = logging.getLogger(__name__)
class USTariffFractionService:
"""Service para gestionar fracciones arancelarias americanas"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[USTariffFraction], int]:
"""Obtiene todas las fracciones arancelarias americanas con filtros opcionales"""
query = db.query(USTariffFraction).filter(
USTariffFraction.tenant_id == tenant_id,
USTariffFraction.company_id == company_id,
)
# Aplicar filtros
if filters:
# Si hay un filtro 'search', buscar en múltiples campos
if filters.get("search"):
search_term = f"%{filters['search']}%"
query = query.filter(
USTariffFraction.code.ilike(search_term) |
USTariffFraction.description.ilike(search_term) |
USTariffFraction.prefix.ilike(search_term)
)
total = query.count()
items = query.order_by(USTariffFraction.code).offset(skip).limit(limit).all()
return items, total
@staticmethod
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"""
return (
db.query(USTariffFraction)
.filter(
USTariffFraction.id == fraction_id,
USTariffFraction.tenant_id == tenant_id,
USTariffFraction.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
tenant_id: int,
company_id: int,
fraction_data: USTariffFractionCreateDTO,
) -> USTariffFraction:
"""Crea una nueva fracción arancelaria americana"""
try:
db_fraction = USTariffFraction(
tenant_id=tenant_id,
company_id=company_id,
**fraction_data.model_dump(),
)
db.add(db_fraction)
db.commit()
db.refresh(db_fraction)
return db_fraction
except IntegrityError as e:
db.rollback()
logger.error(f"Error creando fracción americana: {e}")
raise HTTPException(
status_code=400,
detail="Ya existe una fracción americana con este código",
)
@staticmethod
def update(
db: Session,
tenant_id: int,
company_id: int,
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
)
if not db_fraction:
return None
update_data = fraction_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_fraction, field, value)
db.commit()
db.refresh(db_fraction)
return db_fraction
@staticmethod
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
)
if not db_fraction:
return False
db.delete(db_fraction)
db.commit()
return True

View File

@@ -19,6 +19,10 @@ from .general_catalogs.identifiers.routes import router as identifiers_router
from .fraction_rule_octave.routes import router as fraction_rule_octave_router
from .general_catalogs.packages.routes import router as package_router
from .general_catalogs.ports.routes import router as ports_router
from .general_catalogs.tariff_fractions.routes import router as tariff_fractions_router
from .general_catalogs.us_tariff_fractions.routes import router as us_tariff_fractions_router
from .general_catalogs.depreciation_catalog.routes import router as depreciation_catalog_router
from .general_catalogs.fda_catalog.routes import router as fda_catalog_router
from .parts import router as parts_router
from .pedmientos.router import router as pedimentos_router
from .permission_rule_oct.routes import router as permission_rule_oct_router
@@ -58,6 +62,10 @@ router.include_router(
)
router.include_router(package_router, prefix="/a76")
router.include_router(ports_router, prefix="/a76")
router.include_router(tariff_fractions_router, prefix="/a76")
router.include_router(us_tariff_fractions_router, prefix="/a76")
router.include_router(depreciation_catalog_router, prefix="/a76")
router.include_router(fda_catalog_router, prefix="/a76")
router.include_router(seal_router, prefix="/a76", tags=["a76 / seal"])
router.include_router(units_of_measure_router, prefix="/a76")
router.include_router(

View File

@@ -0,0 +1,41 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
// Interfaces
export interface DepreciationCatalog {
id: number;
tenant_id: number;
company_id: number;
fraction: string;
description: string;
depreciation_rate: number;
created_at: string;
updated_at: string;
}
export interface DepreciationCatalogListResponse {
items: DepreciationCatalog[];
total: number;
page: number;
page_size: number;
pages: number;
}
// API Functions
export async function getDepreciationCatalog(
page = 1,
pageSize = 100,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<DepreciationCatalogListResponse>> {
const params = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return api.get<DepreciationCatalogListResponse>(
`/v1/a76/depreciation-catalog/?${params.toString()}`
);
}

View File

@@ -0,0 +1,41 @@
import { api } from '$lib/api';
export interface FDACatalog {
id: number;
fda_key: string;
description: string;
fda_code?: string;
requirements?: string;
manufacturer_number?: string;
country_of_production?: string;
storage_status?: string;
warehouse_code?: string;
call_atl?: string;
created_at?: string;
updated_at?: string;
}
export async function getFDACatalog(
page: number,
pageSize: number,
companyId: number,
filters: any = {}
): Promise<any> {
try {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString()
});
if (filters.search) {
queryParams.append('search', filters.search);
}
const response = await api.get(`/v1/a76/fda-catalog/?${queryParams.toString()}`);
return response;
} catch (error) {
console.error('Error fetching FDA catalog:', error);
throw error;
}
}

View File

@@ -0,0 +1,88 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
// Interfaces
export interface TariffFraction {
id: number;
code: string;
fraction: string;
description: string | null;
nico: string | null;
umt: string | null;
adv_impo: string | null;
adv_expo: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface TariffFractionCreate {
code: string;
fraction: string;
description?: string | null;
nico?: string | null;
umt?: string | null;
adv_impo?: string | null;
adv_expo?: string | null;
}
export interface TariffFractionUpdate {
fraction?: string;
description?: string | null;
nico?: string | null;
umt?: string | null;
adv_impo?: string | null;
adv_expo?: string | null;
}
export interface TariffFractionListResponse {
items: TariffFraction[];
total: number;
page: number;
page_size: number;
pages: number;
}
// API Functions
export async function getTariffFractions(
page = 1,
pageSize = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<TariffFractionListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/v1/a76/tariff-fractions/?${queryParams.toString()}`);
}
export async function getTariffFractionById(
id: number,
companyId: number
): Promise<ApiResponse<TariffFraction>> {
return await api.get(`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}`);
}
export async function createTariffFraction(
data: TariffFractionCreate,
companyId: number
): Promise<ApiResponse<TariffFraction>> {
return await api.post(`/v1/a76/tariff-fractions/?company_id=${companyId}`, data);
}
export async function updateTariffFraction(
id: number,
data: TariffFractionUpdate,
companyId: number
): Promise<ApiResponse<TariffFraction>> {
return await api.put(`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}`, data);
}
export async function deleteTariffFraction(
id: number,
companyId: number
): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}`);
}

View File

@@ -0,0 +1,88 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
// Interfaces
export interface USTariffFraction {
id: number;
code: string;
prefix: string | null;
type_code: string | null;
ad_valorem: number | null;
fixed_cost: number | null;
unit_of_measure: string | null;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface USTariffFractionCreate {
code: string;
prefix?: string | null;
type_code?: string | null;
ad_valorem?: number | null;
fixed_cost?: number | null;
unit_of_measure?: string | null;
description?: string | null;
}
export interface USTariffFractionUpdate {
prefix?: string | null;
type_code?: string | null;
ad_valorem?: number | null;
fixed_cost?: number | null;
unit_of_measure?: string | null;
description?: string | null;
}
export interface USTariffFractionListResponse {
items: USTariffFraction[];
total: number;
page: number;
page_size: number;
pages: number;
}
// API Functions
export async function getUSTariffFractions(
page = 1,
pageSize = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<USTariffFractionListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/v1/a76/us-tariff-fractions/?${queryParams.toString()}`);
}
export async function getUSTariffFractionById(
id: number,
companyId: number
): Promise<ApiResponse<USTariffFraction>> {
return await api.get(`/v1/a76/us-tariff-fractions/${id}/?company_id=${companyId}`);
}
export async function createUSTariffFraction(
data: USTariffFractionCreate,
companyId: number
): Promise<ApiResponse<USTariffFraction>> {
return await api.post(`/v1/a76/us-tariff-fractions/?company_id=${companyId}`, data);
}
export async function updateUSTariffFraction(
id: number,
data: USTariffFractionUpdate,
companyId: number
): Promise<ApiResponse<USTariffFraction>> {
return await api.put(`/v1/a76/us-tariff-fractions/${id}/?company_id=${companyId}`, data);
}
export async function deleteUSTariffFraction(
id: number,
companyId: number
): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/us-tariff-fractions/${id}/?company_id=${companyId}`);
}

View File

@@ -58,7 +58,7 @@ export function getSidebarData(): SidebarData {
user: {
name: "", // Se llena dinámicamente desde Keycloak
email: "", // Se llena dinámicamente desde Keycloak
// avatar: "/avatars/default.jpg", // Avatar por defecto
avatar: "", // Se llena dinámicamente desde Keycloak
},
teams: [
{
@@ -366,6 +366,17 @@ export function getSidebarData(): SidebarData {
icon: BadgeCheck,
items: [],
},
{
title: "Mercancías",
url: "#",
icon: Package,
items: [
{
title: "Clase de Activo Fijo",
url: "/dashboard/merchandise/fixed_asset_classes",
},
],
},
{
title: m["sidebar.reference_data.configuracion"](),
url: "#",

View File

@@ -0,0 +1,211 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Card from '$lib/components/ui/card';
import * as Table from '$lib/components/ui/table';
import { Search, Loader2, Download } from 'lucide-svelte';
import { getTariffFractions, type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
import { companyStore } from '$lib/stores/company.svelte';
import { onMount } from 'svelte';
let tariffFractions = $state<TariffFraction[]>([]);
let filteredFractions = $state<TariffFraction[]>([]);
let searchQuery = $state('');
let isLoading = $state(false);
let currentPage = $state(1);
let totalPages = $state(1);
let totalRecords = $state(0);
const pageSize = 50;
onMount(() => {
loadTariffFractions();
});
// Filtrar fracciones cuando cambia la búsqueda
$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)
);
}
});
async function loadTariffFractions(page: number = 1) {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
console.error('No hay compañía activa');
return;
}
isLoading = true;
try {
const response = await getTariffFractions(page, pageSize, companyId);
if (response.data) {
tariffFractions = response.data.items;
filteredFractions = response.data.items;
totalPages = response.data.pages;
totalRecords = response.data.total;
currentPage = response.data.page;
}
} catch (error) {
console.error('Error cargando fracciones arancelarias:', error);
} finally {
isLoading = false;
}
}
async function goToPage(page: number) {
if (page >= 1 && page <= totalPages && page !== currentPage) {
await loadTariffFractions(page);
}
}
</script>
<div class="container mx-auto p-6">
<Card.Root>
<Card.Header class="text-center border-b">
<Card.Title class="text-2xl font-bold uppercase">
Catálogo de Fracciones SITAR - SCAII
</Card.Title>
<p class="text-muted-foreground mt-2">
Nomenclatura arancelaria mexicana completa
</p>
</Card.Header>
<Card.Content class="pt-6">
<div class="space-y-6">
<!-- Barra de búsqueda y acciones -->
<div class="flex gap-4 items-center">
<div class="flex-1 relative">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
bind:value={searchQuery}
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
class="pl-9"
/>
</div>
<Button variant="outline" size="icon" title="Exportar a CSV">
<Download class="h-4 w-4" />
</Button>
</div>
<!-- Información de registros -->
<div class="flex items-center justify-between text-sm text-muted-foreground">
<div>
{#if isLoading}
<div class="flex items-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" />
<span>Cargando...</span>
</div>
{:else}
Mostrando {filteredFractions.length} de {totalRecords} fracciones arancelarias
{#if searchQuery}
(filtrado)
{/if}
{/if}
</div>
{#if !searchQuery && totalPages > 1}
<div class="flex items-center gap-2">
<span>Página {currentPage} de {totalPages}</span>
</div>
{/if}
</div>
<!-- Tabla de fracciones -->
<div class="border rounded-md overflow-auto max-h-[600px]">
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
<Table.Row>
<Table.Head class="w-[100px]">Código</Table.Head>
<Table.Head class="w-[120px]">Fracción</Table.Head>
<Table.Head class="min-w-[350px]">Descripción</Table.Head>
<Table.Head class="w-[80px]">NICO</Table.Head>
<Table.Head class="w-[80px]">UMT</Table.Head>
<Table.Head class="w-[100px]">Adv. Impo</Table.Head>
<Table.Head class="w-[100px]">Adv. Expo</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if filteredFractions.length === 0}
<Table.Row>
<Table.Cell colspan={7} class="text-center py-8 text-muted-foreground">
{#if isLoading}
Cargando fracciones arancelarias...
{:else if searchQuery}
No se encontraron fracciones que coincidan con la búsqueda
{:else}
No hay fracciones arancelarias disponibles
{/if}
</Table.Cell>
</Table.Row>
{:else}
{#each filteredFractions as fraction (fraction.id)}
<Table.Row class="hover:bg-muted/50">
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
<Table.Cell class="font-mono text-sm font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell class="text-sm">
{fraction.description || '-'}
</Table.Cell>
<Table.Cell class="text-sm">{fraction.nico || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.umt || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.adv_impo || '-'}</Table.Cell>
<Table.Cell class="text-sm">{fraction.adv_expo || '-'}</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
<!-- Paginación -->
{#if !searchQuery && totalPages > 1}
<div class="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
disabled={currentPage === 1 || isLoading}
onclick={() => goToPage(1)}
>
Primera
</Button>
<Button
variant="outline"
size="sm"
disabled={currentPage === 1 || isLoading}
onclick={() => goToPage(currentPage - 1)}
>
Anterior
</Button>
<span class="px-4 text-sm">
Página {currentPage} de {totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={currentPage === totalPages || isLoading}
onclick={() => goToPage(currentPage + 1)}
>
Siguiente
</Button>
<Button
variant="outline"
size="sm"
disabled={currentPage === totalPages || isLoading}
onclick={() => goToPage(totalPages)}
>
Última
</Button>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>
</div>

File diff suppressed because it is too large Load Diff