feat: Add general catalogs for historical, Canadian, US, and SITAR tariff fractions and sectors with full stack implementation.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""seed_initial_data
|
||||
|
||||
Revision ID: 7937209f9718
|
||||
Revises: 531bf8cdae06
|
||||
Revises:
|
||||
|
||||
Create Date: 2025-10-19 18:23:55.258800
|
||||
|
||||
"""
|
||||
@@ -95,6 +96,174 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
|
||||
# --- CREAR TABLA fraccion canadienses ---
|
||||
op.create_table('canadian_tariff_fractions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('fraction', sa.String(length=13), nullable=False),
|
||||
sa.Column('ad_valorem', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
|
||||
sa.Column('country_code', sa.String(length=3), nullable=False),
|
||||
sa.Column('description', sa.String(length=1000), nullable=True),
|
||||
|
||||
# Tenant Scoped Mixin
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
|
||||
# Timestamp Mixin
|
||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], source_schema='a76', referent_schema='core'),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], source_schema='a76', referent_schema='a76'),
|
||||
sa.UniqueConstraint('fraction', 'country_code', 'company_id', name='uq_canadian_fraction_country_company'),
|
||||
schema='a76'
|
||||
)
|
||||
op.create_index(op.f('ix_a76_canadian_tariff_fractions_country_code'), 'canadian_tariff_fractions', ['country_code'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_canadian_tariff_fractions_fraction'), 'canadian_tariff_fractions', ['fraction'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_canadian_tariff_fractions_id'), 'canadian_tariff_fractions', ['id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_canadian_tariff_fractions_company_id'), 'canadian_tariff_fractions', ['company_id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_canadian_tariff_fractions_tenant_id'), 'canadian_tariff_fractions', ['tenant_id'], unique=False, schema='a76')
|
||||
|
||||
# --- CREAR TABLA fraccion americana ---
|
||||
op.create_table('us_tariff_fractions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=16), nullable=False, comment='Código de fracción americana'),
|
||||
sa.Column('prefix', sa.String(length=10), nullable=True, comment='Prefijo de clasificación'),
|
||||
sa.Column('type_code', sa.String(length=10), nullable=True, comment='Código de tipo'),
|
||||
sa.Column('ad_valorem', sa.Numeric(precision=10, scale=2), nullable=True, comment='Porcentaje ad valorem'),
|
||||
sa.Column('fixed_cost', sa.Numeric(precision=15, scale=8), nullable=True, comment='Tasa fija'),
|
||||
sa.Column('unit_of_measure', sa.String(length=10), nullable=True, comment='Unidad de medida'),
|
||||
sa.Column('description', sa.String(), nullable=True, comment='Descripción de la fracción'),
|
||||
|
||||
# Tenant Scoped Mixin
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
|
||||
# Timestamp Mixin
|
||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], source_schema='a76', referent_schema='core'),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], source_schema='a76', referent_schema='a76'),
|
||||
schema='a76'
|
||||
)
|
||||
op.create_index(op.f('ix_a76_us_tariff_fractions_id'), 'us_tariff_fractions', ['id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_us_tariff_fractions_company_id'), 'us_tariff_fractions', ['company_id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_us_tariff_fractions_tenant_id'), 'us_tariff_fractions', ['tenant_id'], unique=False, schema='a76')
|
||||
|
||||
# --- CREAR TABLA fraccion historico ---
|
||||
op.create_table('historical_tariff_fractions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('historical_fraction', sa.String(length=8), nullable=True),
|
||||
sa.Column('unit_of_measure_code', sa.String(), nullable=True),
|
||||
sa.Column('country', sa.String(), nullable=True),
|
||||
sa.Column('fraction_type', sa.String(length=7), nullable=True),
|
||||
sa.Column('sector', sa.String(length=5), nullable=True),
|
||||
sa.Column('import_tax_rate', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('export_tax_rate', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('publication_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_immex', sa.Boolean(), nullable=True),
|
||||
sa.Column('normal_temporality', sa.Boolean(), nullable=True),
|
||||
sa.Column('services_temporality', sa.Boolean(), nullable=True),
|
||||
sa.Column('certified_temporality', sa.Boolean(), nullable=True),
|
||||
sa.Column('by_log', sa.Boolean(), nullable=True),
|
||||
sa.Column('end_date', sa.DateTime(), nullable=True),
|
||||
|
||||
# Tenant Scoped Mixin
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
|
||||
# Timestamp Mixin
|
||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], source_schema='a76', referent_schema='core'),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], source_schema='a76', referent_schema='a76'),
|
||||
# Foreign Keys to other tables (assuming they exist or are created before/simultaneously, referencing explicit schemas)
|
||||
sa.ForeignKeyConstraint(['unit_of_measure_code'], ['a76.unit_of_measure_customs.code'], source_schema='a76', referent_schema='a76'),
|
||||
sa.ForeignKeyConstraint(['country'], ['public.countries.m3_key'], source_schema='a76', referent_schema='public'),
|
||||
|
||||
schema='a76'
|
||||
)
|
||||
op.create_index(op.f('ix_a76_historical_tariff_fractions_id'), 'historical_tariff_fractions', ['id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_historical_tariff_fractions_company_id'), 'historical_tariff_fractions', ['company_id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_historical_tariff_fractions_tenant_id'), 'historical_tariff_fractions', ['tenant_id'], unique=False, schema='a76')
|
||||
|
||||
# --- CREAR TABLA fraccion americana ---
|
||||
op.create_table('us_tariff_fractions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=16), nullable=False, comment='Código de fracción americana'),
|
||||
sa.Column('prefix', sa.String(length=10), nullable=True, comment='Prefijo de clasificación'),
|
||||
sa.Column('type_code', sa.String(length=10), nullable=True, comment='Código de tipo'),
|
||||
sa.Column('ad_valorem', sa.Numeric(precision=10, scale=2), nullable=True, comment='Porcentaje ad valorem'),
|
||||
sa.Column('fixed_cost', sa.Numeric(precision=15, scale=8), nullable=True, comment='Tasa fija'),
|
||||
sa.Column('unit_of_measure', sa.String(length=10), nullable=True, comment='Unidad de medida'),
|
||||
sa.Column('description', sa.String(), nullable=True, comment='Descripción de la fracción'),
|
||||
|
||||
# Tenant Scoped Mixin
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
|
||||
# Timestamp Mixin
|
||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], source_schema='a76', referent_schema='core'),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], source_schema='a76', referent_schema='a76'),
|
||||
schema='a76'
|
||||
)
|
||||
op.create_index(op.f('ix_a76_us_tariff_fractions_id'), 'us_tariff_fractions', ['id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_us_tariff_fractions_company_id'), 'us_tariff_fractions', ['company_id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_us_tariff_fractions_tenant_id'), 'us_tariff_fractions', ['tenant_id'], unique=False, schema='a76')
|
||||
|
||||
# --- CREAR TABLA fraccion historico ---
|
||||
op.create_table('historical_tariff_fractions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('historical_fraction', sa.String(length=8), nullable=True),
|
||||
sa.Column('unit_of_measure_code', sa.String(), nullable=True),
|
||||
sa.Column('country', sa.String(), nullable=True),
|
||||
sa.Column('fraction_type', sa.String(length=7), nullable=True),
|
||||
sa.Column('sector', sa.String(length=5), nullable=True),
|
||||
sa.Column('import_tax_rate', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('export_tax_rate', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('publication_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_immex', sa.Boolean(), nullable=True),
|
||||
sa.Column('normal_temporality', sa.Boolean(), nullable=True),
|
||||
sa.Column('services_temporality', sa.Boolean(), nullable=True),
|
||||
sa.Column('certified_temporality', sa.Boolean(), nullable=True),
|
||||
sa.Column('by_log', sa.Boolean(), nullable=True),
|
||||
sa.Column('end_date', sa.DateTime(), nullable=True),
|
||||
|
||||
# Tenant Scoped Mixin
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
|
||||
# Timestamp Mixin
|
||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], source_schema='a76', referent_schema='core'),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], source_schema='a76', referent_schema='a76'),
|
||||
# Foreign Keys to other tables (assuming they exist or are created before/simultaneously, referencing explicit schemas)
|
||||
sa.ForeignKeyConstraint(['unit_of_measure_code'], ['a76.unit_of_measure_customs.code'], source_schema='a76', referent_schema='a76'),
|
||||
sa.ForeignKeyConstraint(['country'], ['public.countries.m3_key'], source_schema='a76', referent_schema='public'),
|
||||
|
||||
schema='a76'
|
||||
)
|
||||
op.create_index(op.f('ix_a76_historical_tariff_fractions_id'), 'historical_tariff_fractions', ['id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_historical_tariff_fractions_company_id'), 'historical_tariff_fractions', ['company_id'], unique=False, schema='a76')
|
||||
op.create_index(op.f('ix_a76_historical_tariff_fractions_tenant_id'), 'historical_tariff_fractions', ['tenant_id'], unique=False, schema='a76')
|
||||
|
||||
# --- UTILIDAD DE FORMATEO ---
|
||||
def format_value(val):
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
@@ -520,7 +689,8 @@ def upgrade() -> None:
|
||||
f"{format_value(fraction_type)}, {format_value(sector)}, {format_value(import_tax)}, "
|
||||
f"{format_value(export_tax)}, {format_timestamp(pub_date)}, {format_bool(is_immex)}, "
|
||||
f"{format_bool(normal_temp)}, {format_bool(services_temp)}, {format_bool(certified_temp)}, "
|
||||
f"{format_bool(by_log)}, {format_timestamp(end_date)})"
|
||||
f"{format_bool(by_log)}, {format_timestamp(end_date)}, "
|
||||
f"1, 1, 'NOW()', 'NOW()')" # tenant_id=1, company_id=1, created_at=NOW(), updated_at=NOW()
|
||||
for historical_fraction, unit_measure, country, fraction_type, sector, import_tax, export_tax, pub_date, is_immex, normal_temp, services_temp, certified_temp, by_log, end_date in historical_tariff_fractions_seed
|
||||
]
|
||||
)
|
||||
@@ -531,7 +701,8 @@ def upgrade() -> None:
|
||||
INSERT INTO a76.historical_tariff_fractions
|
||||
(historical_fraction, unit_of_measure_code, country, fraction_type, sector,
|
||||
import_tax_rate, export_tax_rate, publication_date, is_immex,
|
||||
normal_temporality, services_temporality, certified_temporality, by_log, end_date)
|
||||
normal_temporality, services_temporality, certified_temporality, by_log, end_date,
|
||||
tenant_id, company_id, created_at, updated_at)
|
||||
VALUES {values_historical_fractions}
|
||||
ON CONFLICT DO NOTHING;
|
||||
"""
|
||||
@@ -541,6 +712,9 @@ def upgrade() -> None:
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
|
||||
op.drop_table("us_tariff_fractions", schema="a76")
|
||||
op.drop_table("historical_tariff_fractions", schema="a76")
|
||||
op.drop_table("canadian_tariff_fractions", schema="a76")
|
||||
op.drop_table("valuation_methods", schema="public")
|
||||
op.drop_table("transport_types", schema="public")
|
||||
op.drop_table("trailer_types", schema="public")
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Numeric, TIMESTAMP, func, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from core.database import Base
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
class CanadianTariffFraction(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Model for Canadian Tariff Fractions (GFracEUACan)"""
|
||||
__tablename__ = "canadian_tariff_fractions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('fraction', 'country_code', 'company_id', name='uq_canadian_fraction_country_company'),
|
||||
{"schema": "a76"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
|
||||
# FRACCION
|
||||
fraction: Mapped[str] = mapped_column(String(13), nullable=False, index=True)
|
||||
# ADV
|
||||
ad_valorem: Mapped[Optional[float]] = mapped_column(Numeric(5, 2))
|
||||
# UNIDAD
|
||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(String(5))
|
||||
# CLAVEM3 (Part of original PK)
|
||||
country_code: Mapped[str] = mapped_column(String(3), nullable=False, index=True)
|
||||
# DESCRIPCION
|
||||
description: Mapped[Optional[str]] = mapped_column(String(1000))
|
||||
@@ -0,0 +1,98 @@
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from .service import CanadianTariffFractionService
|
||||
from .schemas import (
|
||||
CanadianTariffFractionResponse,
|
||||
CanadianTariffFractionCreate,
|
||||
CanadianTariffFractionUpdate,
|
||||
CanadianTariffFractionListResponse
|
||||
)
|
||||
|
||||
from api.v1.common.tenant_crud_routes import validate_access_to_resource
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=CanadianTariffFractionListResponse)
|
||||
def list_canadian_fractions(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=1000),
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
skip = (page - 1) * page_size
|
||||
service = CanadianTariffFractionService(db)
|
||||
items, total = service.get_multi(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
skip=skip,
|
||||
limit=page_size,
|
||||
search=search
|
||||
)
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": (total + page_size - 1) // page_size if page_size > 0 else 1
|
||||
}
|
||||
|
||||
@router.get("/{id}", response_model=CanadianTariffFractionResponse)
|
||||
def get_canadian_fraction(
|
||||
id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = CanadianTariffFractionService(db)
|
||||
item = service.get(id, tenant_id, company_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return item
|
||||
|
||||
@router.post("/", response_model=CanadianTariffFractionResponse)
|
||||
def create_canadian_fraction(
|
||||
item_in: CanadianTariffFractionCreate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = CanadianTariffFractionService(db)
|
||||
return service.create(item_in, tenant_id, company_id)
|
||||
|
||||
@router.put("/{id}", response_model=CanadianTariffFractionResponse)
|
||||
def update_canadian_fraction(
|
||||
id: int,
|
||||
item_in: CanadianTariffFractionUpdate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = CanadianTariffFractionService(db)
|
||||
item = service.get(id, tenant_id, company_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return service.update(item, item_in)
|
||||
|
||||
@router.delete("/{id}", response_model=CanadianTariffFractionResponse)
|
||||
def delete_canadian_fraction(
|
||||
id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = CanadianTariffFractionService(db)
|
||||
item = service.get(id, tenant_id, company_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return service.delete(id, tenant_id, company_id)
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
class CanadianTariffFractionBase(BaseModel):
|
||||
fraction: str = Field(..., max_length=13)
|
||||
ad_valorem: Optional[Decimal] = Field(None, max_digits=5, decimal_places=2)
|
||||
unit_of_measure: Optional[str] = Field(None, max_length=5)
|
||||
country_code: str = Field(..., max_length=3)
|
||||
description: Optional[str] = Field(None, max_length=1000)
|
||||
|
||||
class CanadianTariffFractionCreate(CanadianTariffFractionBase):
|
||||
pass
|
||||
|
||||
class CanadianTariffFractionUpdate(CanadianTariffFractionBase):
|
||||
pass
|
||||
|
||||
class CanadianTariffFractionResponse(CanadianTariffFractionBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class CanadianTariffFractionListResponse(BaseModel):
|
||||
items: List[CanadianTariffFractionResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
pages: int
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
from typing import Optional, List, Tuple
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import Session
|
||||
from .models import CanadianTariffFraction
|
||||
from .schemas import CanadianTariffFractionCreate, CanadianTariffFractionUpdate
|
||||
|
||||
class CanadianTariffFractionService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get(self, id: int, tenant_id: int, company_id: int) -> Optional[CanadianTariffFraction]:
|
||||
return self.db.query(CanadianTariffFraction).filter(
|
||||
CanadianTariffFraction.id == id,
|
||||
CanadianTariffFraction.tenant_id == tenant_id,
|
||||
CanadianTariffFraction.company_id == company_id
|
||||
).first()
|
||||
|
||||
def get_multi(
|
||||
self,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None
|
||||
) -> Tuple[List[CanadianTariffFraction], int]:
|
||||
query = select(CanadianTariffFraction).where(
|
||||
CanadianTariffFraction.tenant_id == tenant_id,
|
||||
CanadianTariffFraction.company_id == company_id
|
||||
)
|
||||
|
||||
if search:
|
||||
query = query.where(
|
||||
(CanadianTariffFraction.fraction.ilike(f"%{search}%")) |
|
||||
(CanadianTariffFraction.description.ilike(f"%{search}%"))
|
||||
)
|
||||
|
||||
total = self.db.execute(select(func.count()).select_from(query.subquery())).scalar_one()
|
||||
items = self.db.scalars(query.offset(skip).limit(limit)).all()
|
||||
return items, total
|
||||
|
||||
def create(self, obj_in: CanadianTariffFractionCreate, tenant_id: int, company_id: int) -> CanadianTariffFraction:
|
||||
db_obj = CanadianTariffFraction(
|
||||
**obj_in.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update(
|
||||
self,
|
||||
db_obj: CanadianTariffFraction,
|
||||
obj_in: CanadianTariffFractionUpdate
|
||||
) -> CanadianTariffFraction:
|
||||
update_data = obj_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_obj, field, value)
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def delete(self, id: int, tenant_id: int, company_id: int) -> Optional[CanadianTariffFraction]:
|
||||
obj = self.get(id, tenant_id, company_id)
|
||||
if obj:
|
||||
self.db.delete(obj)
|
||||
self.db.commit()
|
||||
return obj
|
||||
@@ -4,9 +4,10 @@ from decimal import Decimal
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Integer, Numeric, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from core.database import Base
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
|
||||
class HistoricalTariffFraction(Base):
|
||||
class HistoricalTariffFraction(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Historical tariff fractions catalog.
|
||||
Maps to SQL Server table: GFraccionesHistorico
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from .service import HistoricalTariffFractionService
|
||||
from .schemas import HistoricalTariffFractionResponse, HistoricalTariffFractionCreate, HistoricalTariffFractionUpdate, HistoricalTariffFractionListResponse
|
||||
|
||||
from api.v1.common.tenant_crud_routes import validate_access_to_resource
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=HistoricalTariffFractionListResponse)
|
||||
def get_historical_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=1000, description="Page size"),
|
||||
historical_fraction: Optional[str] = Query(None, description="Search by historical fraction code"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all historical tariff fractions (paginated).
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
skip = (page - 1) * page_size
|
||||
service = HistoricalTariffFractionService(db)
|
||||
items, total = service.get_multi(tenant_id, company_id, skip=skip, limit=page_size, historical_fraction=historical_fraction)
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": (total + page_size - 1) // page_size if page_size > 0 else 1
|
||||
}
|
||||
|
||||
@router.get("/{id}", response_model=HistoricalTariffFractionResponse)
|
||||
def get_historical_fraction(
|
||||
id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get a historical tariff fraction by ID.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = HistoricalTariffFractionService(db)
|
||||
fraction = service.get(id, tenant_id, company_id)
|
||||
if not fraction:
|
||||
raise HTTPException(status_code=404, detail="Historical tariff fraction not found")
|
||||
return fraction
|
||||
|
||||
@router.post("/", response_model=HistoricalTariffFractionResponse)
|
||||
def create_historical_fraction(
|
||||
fraction_in: HistoricalTariffFractionCreate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Create a new historical tariff fraction.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = HistoricalTariffFractionService(db)
|
||||
return service.create(fraction_in, tenant_id, company_id)
|
||||
|
||||
@router.put("/{id}", response_model=HistoricalTariffFractionResponse)
|
||||
def update_historical_fraction(
|
||||
id: int,
|
||||
fraction_in: HistoricalTariffFractionUpdate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Update a historical tariff fraction.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = HistoricalTariffFractionService(db)
|
||||
fraction = service.get(id, tenant_id, company_id)
|
||||
if not fraction:
|
||||
raise HTTPException(status_code=404, detail="Historical tariff fraction not found")
|
||||
return service.update(fraction, fraction_in)
|
||||
|
||||
@router.delete("/{id}", response_model=HistoricalTariffFractionResponse)
|
||||
def delete_historical_fraction(
|
||||
id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Delete a historical tariff fraction.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = HistoricalTariffFractionService(db)
|
||||
fraction = service.get(id, tenant_id, company_id)
|
||||
if not fraction:
|
||||
raise HTTPException(status_code=404, detail="Historical tariff fraction not found")
|
||||
return service.delete(id, tenant_id, company_id)
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class HistoricalTariffFractionBase(BaseModel):
|
||||
"""Base schema for Historical Tariff Fraction"""
|
||||
historical_fraction: Optional[str] = Field(None, max_length=8)
|
||||
unit_of_measure_code: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
fraction_type: Optional[str] = Field(None, max_length=7)
|
||||
sector: Optional[str] = Field(None, max_length=5)
|
||||
import_tax_rate: Optional[Decimal] = None
|
||||
export_tax_rate: Optional[Decimal] = None
|
||||
publication_date: Optional[datetime] = None
|
||||
is_immex: Optional[bool] = None
|
||||
normal_temporality: Optional[bool] = None
|
||||
services_temporality: Optional[bool] = None
|
||||
certified_temporality: Optional[bool] = None
|
||||
by_log: Optional[bool] = None
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
class HistoricalTariffFractionCreate(HistoricalTariffFractionBase):
|
||||
"""Schema for creating a Historical Tariff Fraction"""
|
||||
pass
|
||||
|
||||
class HistoricalTariffFractionUpdate(HistoricalTariffFractionBase):
|
||||
"""Schema for updating a Historical Tariff Fraction"""
|
||||
pass
|
||||
|
||||
class HistoricalTariffFractionResponse(HistoricalTariffFractionBase):
|
||||
"""Schema for reading a Historical Tariff Fraction"""
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class HistoricalTariffFractionListResponse(BaseModel):
|
||||
"""Schema for paginated list of Historical Tariff Fractions"""
|
||||
items: List[HistoricalTariffFractionResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
pages: int
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
from typing import Optional, List, Tuple
|
||||
from sqlalchemy import select, or_, func
|
||||
from sqlalchemy.orm import Session
|
||||
from .models import HistoricalTariffFraction
|
||||
from .schemas import HistoricalTariffFractionCreate, HistoricalTariffFractionUpdate
|
||||
|
||||
class HistoricalTariffFractionService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get(self, id: int, tenant_id: int, company_id: int) -> Optional[HistoricalTariffFraction]:
|
||||
return self.db.query(HistoricalTariffFraction).filter(
|
||||
HistoricalTariffFraction.id == id,
|
||||
HistoricalTariffFraction.tenant_id == tenant_id,
|
||||
HistoricalTariffFraction.company_id == company_id
|
||||
).first()
|
||||
|
||||
def get_multi(
|
||||
self,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
historical_fraction: Optional[str] = None
|
||||
) -> Tuple[List[HistoricalTariffFraction], int]:
|
||||
query = select(HistoricalTariffFraction).where(
|
||||
HistoricalTariffFraction.tenant_id == tenant_id,
|
||||
HistoricalTariffFraction.company_id == company_id
|
||||
)
|
||||
|
||||
if historical_fraction:
|
||||
query = query.where(HistoricalTariffFraction.historical_fraction.ilike(f"%{historical_fraction}%"))
|
||||
|
||||
total = self.db.execute(select(func.count()).select_from(query.subquery())).scalar_one()
|
||||
items = self.db.scalars(query.offset(skip).limit(limit)).all()
|
||||
return items, total
|
||||
|
||||
def create(self, obj_in: HistoricalTariffFractionCreate, tenant_id: int, company_id: int) -> HistoricalTariffFraction:
|
||||
db_obj = HistoricalTariffFraction(**obj_in.model_dump())
|
||||
db_obj.tenant_id = tenant_id
|
||||
db_obj.company_id = company_id
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update(
|
||||
self,
|
||||
db_obj: HistoricalTariffFraction,
|
||||
obj_in: HistoricalTariffFractionUpdate
|
||||
) -> HistoricalTariffFraction:
|
||||
# db_obj already validated for tenant/company in get()
|
||||
update_data = obj_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_obj, field, value)
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def delete(self, id: int, tenant_id: int, company_id: int) -> Optional[HistoricalTariffFraction]:
|
||||
obj = self.get(id, tenant_id, company_id)
|
||||
if obj:
|
||||
self.db.delete(obj)
|
||||
self.db.commit()
|
||||
return obj
|
||||
@@ -30,6 +30,8 @@ async def list_tariff_fractions(
|
||||
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"),
|
||||
level: Optional[int] = Query(None, description="Filter by hierarchy level (e.g. 5)"),
|
||||
catalog: Optional[str] = Query("mex", description="Catalog source: 'mex' (default) or 'usa'"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
@@ -37,13 +39,20 @@ async def list_tariff_fractions(
|
||||
filters = {}
|
||||
if search:
|
||||
filters["search"] = search
|
||||
if level is not None:
|
||||
filters["level"] = level
|
||||
|
||||
# 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.
|
||||
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id") # Assuming user is context-aware or we use a default?
|
||||
# If using headers for selected company, it might be in current_user context if middleware sets it.
|
||||
|
||||
items, total = await TariffFractionService.get_all(
|
||||
db, skip, page_size, filters
|
||||
db, skip, page_size, filters, catalog, tenant_id, company_id
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -72,3 +81,132 @@ 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 (Only supported for 'american' catalog)",
|
||||
)
|
||||
async def create_tariff_fraction(
|
||||
fraction_data: TariffFractionCreateDTO,
|
||||
catalog: str = Query("mex", description="Catalog source: 'mex', 'usa', or 'american'"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Crea una nueva fracción.
|
||||
- MEX/USA: No permitido (Read-Only)
|
||||
- AMERICAN: Permitido (Local DB)
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
|
||||
if catalog == "american":
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.dto import USTariffFractionCreateDTO
|
||||
import re
|
||||
|
||||
# Map generic DTO to US DTO
|
||||
ad_valorem = None
|
||||
if fraction_data.adv_impo:
|
||||
try:
|
||||
# remove non-numeric chars except dot
|
||||
clean = re.sub(r'[^\d.]', '', fraction_data.adv_impo)
|
||||
if clean:
|
||||
ad_valorem = float(clean)
|
||||
except:
|
||||
pass
|
||||
|
||||
us_dto = USTariffFractionCreateDTO(
|
||||
code=fraction_data.code,
|
||||
description=fraction_data.description,
|
||||
unit_of_measure=fraction_data.umt,
|
||||
ad_valorem=ad_valorem,
|
||||
# Defaults for others
|
||||
prefix=None,
|
||||
type_code=None,
|
||||
fixed_cost=None
|
||||
)
|
||||
|
||||
created = USTariffFractionService.create(db, tenant_id, company_id, us_dto)
|
||||
return TariffFractionService.to_domain_usa_local(created)
|
||||
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Creation not allowed for '{catalog}' catalog (Read-Only)")
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{tariff_fraction_id}",
|
||||
response_model=TariffFractionResponseDTO,
|
||||
summary="Update Tariff Fraction",
|
||||
description="Update a tariff fraction (Only supported for 'american' catalog)",
|
||||
)
|
||||
async def update_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
fraction_data: TariffFractionUpdateDTO,
|
||||
catalog: str = Query("mex", description="Catalog source: 'mex', 'usa', or 'american'"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
from fastapi import HTTPException
|
||||
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
|
||||
if catalog == "american":
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.dto import USTariffFractionUpdateDTO
|
||||
import re
|
||||
|
||||
ad_valorem = None
|
||||
if fraction_data.adv_impo:
|
||||
try:
|
||||
clean = re.sub(r'[^\d.]', '', fraction_data.adv_impo)
|
||||
if clean:
|
||||
ad_valorem = float(clean)
|
||||
except:
|
||||
pass
|
||||
|
||||
us_dto = USTariffFractionUpdateDTO(
|
||||
description=fraction_data.description,
|
||||
unit_of_measure=fraction_data.umt,
|
||||
ad_valorem=ad_valorem
|
||||
)
|
||||
|
||||
updated = USTariffFractionService.update(db, tenant_id, company_id, tariff_fraction_id, us_dto)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="US Tariff fraction not found")
|
||||
return TariffFractionService.to_domain_usa_local(updated)
|
||||
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Update not allowed for '{catalog}' catalog (Read-Only)")
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{tariff_fraction_id}",
|
||||
summary="Delete Tariff Fraction",
|
||||
description="Delete a tariff fraction (Only supported for 'american' catalog)",
|
||||
)
|
||||
async def delete_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
catalog: str = Query("mex", description="Catalog source: 'mex', 'usa', or 'american'"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
from fastapi import HTTPException
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
|
||||
if catalog == "american":
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
|
||||
success = USTariffFractionService.delete(db, tenant_id, company_id, tariff_fraction_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="US Tariff fraction not found")
|
||||
return {"ok": True}
|
||||
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Delete not allowed for '{catalog}' catalog (Read-Only)")
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ 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
|
||||
from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService
|
||||
from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,30 +70,117 @@ class TariffFractionMapper:
|
||||
|
||||
return tf
|
||||
|
||||
@staticmethod
|
||||
def to_domain_usa(item: FraccionesUSAResponse) -> TariffFraction:
|
||||
"""Map US Fraction to Domain"""
|
||||
return TariffFraction(
|
||||
id=item.CONSECUTIVO,
|
||||
code=item.FRACCION_SIN_PUNTO or "",
|
||||
fraction=item.FRACCION_CON_PUNTO or "",
|
||||
description=item.DESCRIPCION or "(Sin descripción)",
|
||||
nico=None, # Not applicable
|
||||
umt=item.UNIDADCANTIDAD,
|
||||
adv_impo=item.TARIFA1,
|
||||
adv_expo=item.TARIFA2
|
||||
)
|
||||
|
||||
|
||||
class TariffFractionService:
|
||||
"""Service para gestionar fracciones arancelarias (catálogo global)"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def to_domain_usa_local(item: Any) -> TariffFraction:
|
||||
"""Map Local US Fraction (ORM) to Domain"""
|
||||
# Formatter helper (simple logic: add dots every 2/4 chars? or just return as is?)
|
||||
# US format: 1234.56.78.90. For now return as is or use helper if available.
|
||||
# item is USTariffFraction (imported inside method to avoid circular import if needed, or assumed available)
|
||||
|
||||
return TariffFraction(
|
||||
id=item.id,
|
||||
code=item.code,
|
||||
fraction=item.code, # TODO: Format if needed
|
||||
description=item.description or "(Sin descripción)",
|
||||
nico=None,
|
||||
umt=item.unit_of_measure,
|
||||
adv_impo=str(item.ad_valorem) if item.ad_valorem is not None else None,
|
||||
adv_expo=None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_all(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
catalog: str = "mex",
|
||||
tenant_id: Optional[int] = None,
|
||||
company_id: Optional[int] = None,
|
||||
) -> Tuple[List[TariffFraction], int]:
|
||||
"""
|
||||
Obtiene fracciones arancelarias.
|
||||
Estrategia: Sitar API -> Fallback Local DB
|
||||
Estrategia:
|
||||
- MEX: Sitar API -> Fallback Local DB
|
||||
- USA: Local DB (Defined by user requirement)
|
||||
"""
|
||||
|
||||
# 1. Try Sitar API
|
||||
# AMERICAN CATALOG HANDLING (LOCAL - 'Fracciones Americanas')
|
||||
if catalog == "american":
|
||||
if tenant_id is None or company_id is None:
|
||||
logger.warning("Solicitud de fracciones Americanas sin tenant/company ID")
|
||||
return [], 0
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
|
||||
|
||||
# Use local service directly
|
||||
usa_items, total = USTariffFractionService._get_all_local(
|
||||
db, tenant_id, company_id, skip, limit, filters
|
||||
)
|
||||
|
||||
items = [TariffFractionMapper.to_domain_usa_local(item) for item in usa_items]
|
||||
return items, total
|
||||
|
||||
# USA CATALOG HANDLING (API - 'Fracciones US')
|
||||
if catalog == "usa":
|
||||
try:
|
||||
usa_service = FraccionesUSAService.get_instance()
|
||||
search_term = None
|
||||
if filters and filters.get("search"):
|
||||
search_term = filters["search"]
|
||||
|
||||
# USA Service search signature: fraccion, skip, limit
|
||||
usa_items = await usa_service.search(
|
||||
fraccion=search_term,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
items = [TariffFractionMapper.to_domain_usa(item) for item in usa_items]
|
||||
total = len(items) + skip
|
||||
if len(items) == limit:
|
||||
total += 1
|
||||
return items, total
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching USA fractions (API): {e}")
|
||||
# Return empty list on error as per requirement (since API is broken)
|
||||
return [], 0
|
||||
|
||||
# MEX (SITAR) CATALOG HANDLING
|
||||
try:
|
||||
sitar_service = FraccionesService.get_instance()
|
||||
|
||||
# Map filters
|
||||
sitar_fraccion = None
|
||||
sitar_nico = None
|
||||
has_filters = False
|
||||
|
||||
# Default level logic
|
||||
level_filter = 5 # Default legacy
|
||||
if filters and filters.get("level") is not None:
|
||||
level_filter = filters["level"]
|
||||
|
||||
# Allow disabling level filter explicitly
|
||||
if level_filter == -1:
|
||||
level_filter = None
|
||||
|
||||
if filters:
|
||||
if filters.get("search"):
|
||||
@@ -101,7 +190,6 @@ class TariffFractionService:
|
||||
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.")
|
||||
@@ -109,13 +197,10 @@ class TariffFractionService:
|
||||
|
||||
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
|
||||
@@ -124,7 +209,6 @@ class TariffFractionService:
|
||||
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.
|
||||
@@ -133,7 +217,7 @@ class TariffFractionService:
|
||||
fraccion=sitar_fraccion,
|
||||
nico=sitar_nico,
|
||||
description=sitar_description,
|
||||
nivel=5, # User requested filtering by level 5
|
||||
nivel=level_filter, # Dynamic level
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
@@ -9,9 +9,10 @@ from sqlalchemy import String, Numeric, TIMESTAMP, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from core.database import Base
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
|
||||
class USTariffFraction(Base):
|
||||
class USTariffFraction(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Modelo para fracciones arancelarias americanas (US HTS codes)"""
|
||||
|
||||
__tablename__ = "us_tariff_fractions"
|
||||
@@ -20,10 +21,6 @@ class USTariffFraction(Base):
|
||||
# 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"
|
||||
@@ -47,16 +44,5 @@ class USTariffFraction(Base):
|
||||
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}>"
|
||||
|
||||
@@ -6,6 +6,8 @@ from .packages.routes import router as package_router
|
||||
from .ports.routes import router as ports_router
|
||||
from .fractions.tariff_fractions.routes import router as tariff_fractions_router
|
||||
from .fractions.us_tariff_fractions.routes import router as us_tariff_fractions_router
|
||||
from .fractions.historical_tariff_fractions.routes import router as historical_tariff_fractions_router
|
||||
from .fractions.canadian_tariff_fractions.routes import router as canadian_tariff_fractions_router
|
||||
from .depreciation_catalog.routes import router as depreciation_catalog_router
|
||||
from .fda_catalog.routes import router as fda_catalog_router
|
||||
from .seal.routes import router as seal_router
|
||||
@@ -31,6 +33,8 @@ router.include_router(package_router)
|
||||
router.include_router(ports_router)
|
||||
router.include_router(tariff_fractions_router)
|
||||
router.include_router(us_tariff_fractions_router)
|
||||
router.include_router(historical_tariff_fractions_router, prefix="/fractions/historical-tariff-fractions", tags=["a76 / historical_tariff_fractions"])
|
||||
router.include_router(canadian_tariff_fractions_router, prefix="/fractions/canadian-tariff-fractions", tags=["a76 / canadian_tariff_fractions"])
|
||||
router.include_router(depreciation_catalog_router)
|
||||
router.include_router(fda_catalog_router)
|
||||
router.include_router(seal_router, tags=["a76 / seal"])
|
||||
|
||||
@@ -4,6 +4,6 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
class SectorDTO(BaseModel):
|
||||
key: str = Field(..., min_length=1, max_length=8)
|
||||
description: str
|
||||
authorized: int
|
||||
authorized: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, has_role
|
||||
from core.security import get_current_user
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import SectorDTO
|
||||
@@ -15,13 +17,23 @@ router = APIRouter(prefix="/sectors")
|
||||
def list_sectors(
|
||||
page: int = Query(1, ge=1, description="Número de página"),
|
||||
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
|
||||
search: Optional[str] = Query(None, description="Término de búsqueda"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(Sector)
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
if search:
|
||||
search_filter = or_(
|
||||
Sector.key.ilike(f"%{search}%"),
|
||||
Sector.description.ilike(f"%{search}%")
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"items": [SectorDTO.model_validate(obj) for obj in items],
|
||||
"total": total,
|
||||
@@ -40,47 +52,3 @@ def get_sector(
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return obj
|
||||
|
||||
|
||||
@router.post("/", response_model=SectorDTO, status_code=201)
|
||||
def create_sector(
|
||||
data: SectorDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = Sector(**data.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.put("/{key}", response_model=SectorDTO)
|
||||
def update_sector(
|
||||
key: str,
|
||||
data: SectorDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(Sector).filter(Sector.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
for field, value in data.dict().items():
|
||||
setattr(obj, field, value)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{key}", status_code=204)
|
||||
def delete_sector(
|
||||
key: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(Sector).filter(Sector.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
@@ -11,7 +11,7 @@ class FraccionesUSAResponse(BaseModel):
|
||||
FRACCION_CON_PUNTO: Optional[str] = None
|
||||
FRACCION_MOSTRAR: Optional[str] = None
|
||||
ESPECIFICO: Optional[str] = None
|
||||
NIVEL: Optional[str] = None
|
||||
NIVEL: Optional[int] = None
|
||||
DESCRIPCION: Optional[str] = None
|
||||
UNIDADCANTIDAD: Optional[str] = None
|
||||
TARIFA1: Optional[str] = None
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Fracciones USA Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict, Any
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import FraccionesUSAResponse
|
||||
|
||||
|
||||
|
||||
class FraccionesUSAService(SitarAPIBaseService):
|
||||
"""Service for USA Fracciones operations"""
|
||||
|
||||
@@ -28,10 +29,10 @@ class FraccionesUSAService(SitarAPIBaseService):
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
|
||||
data = await self._make_request("GET", "/api/v1/fracciones-usa/", params=params)
|
||||
data = await self._make_request("GET", "api/v1/fracciones-usa/", params=params)
|
||||
return [FraccionesUSAResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, consecutivo: int) -> FraccionesUSAResponse:
|
||||
"""Get single USA Fraccion record by CONSECUTIVO"""
|
||||
data = await self._make_request("GET", f"/api/v1/fracciones-usa/{consecutivo}")
|
||||
data = await self._make_request("GET", f"api/v1/fracciones-usa/{consecutivo}")
|
||||
return FraccionesUSAResponse(**data)
|
||||
|
||||
@@ -21,7 +21,7 @@ router.include_router(core_router)
|
||||
router.include_router(a76_router)
|
||||
router.include_router(a24_router)
|
||||
router.include_router(public_router)
|
||||
router.include_router(sitar_router)
|
||||
router.include_router(sitar_router, prefix="/sitar")
|
||||
|
||||
|
||||
# Health check
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface HistoricalFraction {
|
||||
id: number;
|
||||
historical_fraction: string | null;
|
||||
unit_of_measure_code: string | null;
|
||||
country: string | null;
|
||||
fraction_type: string | null;
|
||||
sector: string | null;
|
||||
import_tax_rate: number | null;
|
||||
export_tax_rate: number | null;
|
||||
publication_date: string | null;
|
||||
is_immex: boolean | null;
|
||||
normal_temporality: boolean | null;
|
||||
services_temporality: boolean | null;
|
||||
certified_temporality: boolean | null;
|
||||
by_log: boolean | null;
|
||||
end_date: string | null;
|
||||
}
|
||||
|
||||
export interface HistoricalFractionList {
|
||||
items: HistoricalFraction[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getHistoricalFractions(
|
||||
companyId: number,
|
||||
historicalFraction?: string,
|
||||
page = 1,
|
||||
pageSize = 50
|
||||
): Promise<HistoricalFractionList> {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
if (historicalFraction) params.append('historical_fraction', historicalFraction);
|
||||
|
||||
const response = await api.get<HistoricalFractionList>(`/v1/a76/general_catalogs/fractions/historical-tariff-fractions/?${params.toString()}`);
|
||||
if (!response.data) throw new Error('Error fetching historical fractions');
|
||||
return response.data;
|
||||
}
|
||||
@@ -69,22 +69,33 @@ export async function getTariffFractionById(
|
||||
|
||||
export async function createTariffFraction(
|
||||
data: TariffFractionCreate,
|
||||
companyId: number
|
||||
companyId: number,
|
||||
catalog = 'mex'
|
||||
): Promise<ApiResponse<TariffFraction>> {
|
||||
return await api.post(`/v1/a76/tariff-fractions/?company_id=${companyId}`, data);
|
||||
return await api.post(
|
||||
`/v1/a76/tariff-fractions/?company_id=${companyId}&catalog=${catalog}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateTariffFraction(
|
||||
id: number,
|
||||
data: TariffFractionUpdate,
|
||||
companyId: number
|
||||
companyId: number,
|
||||
catalog = 'mex'
|
||||
): Promise<ApiResponse<TariffFraction>> {
|
||||
return await api.put(`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}`, data);
|
||||
return await api.put(
|
||||
`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}&catalog=${catalog}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteTariffFraction(
|
||||
id: number,
|
||||
companyId: number
|
||||
companyId: number,
|
||||
catalog = 'mex'
|
||||
): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}`);
|
||||
return await api.delete(
|
||||
`/v1/a76/tariff-fractions/${id}/?company_id=${companyId}&catalog=${catalog}`
|
||||
);
|
||||
}
|
||||
|
||||
77
frontend/src/lib/api/dashboard/general_catalogs/canadian.ts
Normal file
77
frontend/src/lib/api/dashboard/general_catalogs/canadian.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export interface CanadianFraction {
|
||||
id: number;
|
||||
fraction: string;
|
||||
ad_valorem: number | null;
|
||||
unit_of_measure: string | null;
|
||||
country_code: string;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CanadianFractionList {
|
||||
items: CanadianFraction[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface CanadianFractionCreate {
|
||||
fraction: string;
|
||||
ad_valorem?: number;
|
||||
unit_of_measure?: string;
|
||||
country_code: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CanadianFractionUpdate {
|
||||
fraction?: string;
|
||||
ad_valorem?: number;
|
||||
unit_of_measure?: string;
|
||||
country_code?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const BASE_URL = '/v1/a76/fractions/canadian-tariff-fractions/';
|
||||
|
||||
export async function getCanadianFractions(
|
||||
companyId: number,
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
search?: string
|
||||
): Promise<CanadianFractionList> {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
if (search) params.append('search', search);
|
||||
|
||||
const response = await api.get<CanadianFractionList>(`${BASE_URL}?${params.toString()}`);
|
||||
if (!response.data) throw new Error('Error fetching Canadian fractions');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createCanadianFraction(companyId: number, data: CanadianFractionCreate): Promise<CanadianFraction> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post<CanadianFraction>(`${BASE_URL}?${params.toString()}`, data);
|
||||
if (!response.data) throw new Error('Error creating Canadian fraction');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateCanadianFraction(companyId: number, id: number, data: CanadianFractionUpdate): Promise<CanadianFraction> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put<CanadianFraction>(`${BASE_URL}${id}?${params.toString()}`, data);
|
||||
if (!response.data) throw new Error('Error updating Canadian fraction');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteCanadianFraction(companyId: number, id: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`${BASE_URL}${id}?${params.toString()}`);
|
||||
}
|
||||
35
frontend/src/lib/api/dashboard/general_catalogs/sectors.ts
Normal file
35
frontend/src/lib/api/dashboard/general_catalogs/sectors.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Sector {
|
||||
key: string;
|
||||
description: string;
|
||||
authorized: boolean;
|
||||
}
|
||||
|
||||
export interface SectorListResponse {
|
||||
items: Sector[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export async function getSectors(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
search?: string
|
||||
): Promise<SectorListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
if (search) {
|
||||
params.append('search', search);
|
||||
}
|
||||
|
||||
const response = await api.get<SectorListResponse>(`/v1/public/reference_data/sectors/?${params.toString()}`);
|
||||
if (!response.data) throw new Error('Error fetching sectors');
|
||||
return response.data;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getSectors, type Sector } from '$lib/api/dashboard/general_catalogs/sectors';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
|
||||
export let title = 'Sectores';
|
||||
|
||||
let sectors: Sector[] = [];
|
||||
let loading = false;
|
||||
let searchTerm = '';
|
||||
let page = 1;
|
||||
let pageSize = 50;
|
||||
let hasMore = true;
|
||||
let total = 0;
|
||||
|
||||
async function loadSectors(reset = false) {
|
||||
if (loading || (!hasMore && !reset)) return;
|
||||
loading = true;
|
||||
|
||||
if (reset) {
|
||||
page = 1;
|
||||
sectors = [];
|
||||
hasMore = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getSectors(page, pageSize, searchTerm || undefined);
|
||||
|
||||
if (response.items.length === 0) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
sectors = reset ? response.items : [...sectors, ...response.items];
|
||||
total = response.total;
|
||||
hasMore = sectors.length < total;
|
||||
if (sectors.length >= total) hasMore = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading sectors:', error);
|
||||
toast.error('Error al cargar sectores');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadSectors(true);
|
||||
}
|
||||
|
||||
function handleLoadMore() {
|
||||
if (!loading && hasMore) {
|
||||
page++;
|
||||
loadSectors();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSectors(true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-3xl font-bold tracking-tight">{title}</h2>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar por clave o descripción..."
|
||||
class="pl-8"
|
||||
bind:value={searchTerm}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button onclick={handleSearch} disabled={loading}>
|
||||
{#if loading && page === 1}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
Buscar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[100px]">Clave</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
<Table.Head class="text-right">Estatus</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if loading && page === 1}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={3} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else if sectors.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={3} class="h-24 text-center">
|
||||
No se encontraron sectores.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each sectors as sector}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{sector.key}</Table.Cell>
|
||||
<Table.Cell>{sector.description}</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
{#if sector.authorized}
|
||||
<Badge variant="default">Autorizado</Badge>
|
||||
{:else}
|
||||
<Badge variant="secondary">No Autorizado</Badge>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col items-center gap-2">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
Mostrando {sectors.length} de {total} registros
|
||||
</div>
|
||||
{#if hasMore}
|
||||
<Button variant="outline" onclick={handleLoadMore} disabled={loading}>
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Cargando...
|
||||
{:else}
|
||||
Cargar más
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
getCanadianFractions,
|
||||
type CanadianFraction
|
||||
} from '$lib/api/dashboard/general_catalogs/canadian';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Search, Loader2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let fractions: CanadianFraction[] = [];
|
||||
let loading = false;
|
||||
let searchQuery = '';
|
||||
let page = 1;
|
||||
let totalItems = 0;
|
||||
let totalPages = 0;
|
||||
let pageSize = 50;
|
||||
|
||||
async function loadFractions(targetPage = 1) {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
loading = true;
|
||||
page = targetPage;
|
||||
|
||||
try {
|
||||
const response = await getCanadianFractions(
|
||||
companyId,
|
||||
page,
|
||||
pageSize,
|
||||
searchQuery || undefined
|
||||
);
|
||||
fractions = response.items;
|
||||
totalItems = response.total;
|
||||
totalPages = response.pages;
|
||||
} catch (error) {
|
||||
console.error('Error loading Canadian fractions:', error);
|
||||
toast.error('Error al cargar fracciones canadienses');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadFractions(1);
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (companyStore.activeCompany) {
|
||||
loadFractions(1);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (companyStore.activeCompany?.id) {
|
||||
loadFractions(1);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-4 md:flex-row">
|
||||
<div class="flex-1">
|
||||
<label for="search-fraction" class="mb-2 block text-sm font-medium">Buscar</label>
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-fraction"
|
||||
type="text"
|
||||
placeholder="Buscar por fracción o descripción..."
|
||||
class="pl-9"
|
||||
bind:value={searchQuery}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button onclick={handleSearch} disabled={loading}>Buscar</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Fracción</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
<Table.Head>País</Table.Head>
|
||||
<Table.Head>Unidad</Table.Head>
|
||||
<Table.Head class="text-right">ADV</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={5} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else if fractions.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={5} class="h-24 text-center"
|
||||
>No se encontraron resultados</Table.Cell
|
||||
>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
|
||||
<Table.Cell>{fraction.description || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.country_code}</Table.Cell>
|
||||
<Table.Cell>{fraction.unit_of_measure || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">{fraction.ad_valorem ?? '-'}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => loadFractions(page - 1)}
|
||||
disabled={page === 1 || loading}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages || 1}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => loadFractions(page + 1)}
|
||||
disabled={page >= totalPages || loading}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
getHistoricalFractions,
|
||||
type HistoricalFraction
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/historical-tariff-fractions';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Search, Loader2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let fractions = $state<HistoricalFraction[]>([]);
|
||||
let loading = $state(false);
|
||||
let historicalFraction = $state('');
|
||||
let page = $state(1);
|
||||
let totalItems = $state(0);
|
||||
let totalPages = $state(0);
|
||||
let pageSize = 50;
|
||||
|
||||
async function loadFractions(targetPage = 1) {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
loading = true;
|
||||
page = targetPage;
|
||||
|
||||
try {
|
||||
const response = await getHistoricalFractions(
|
||||
companyId,
|
||||
historicalFraction || undefined,
|
||||
page,
|
||||
pageSize
|
||||
);
|
||||
fractions = response.items;
|
||||
totalItems = response.total;
|
||||
totalPages = response.pages;
|
||||
} catch (error) {
|
||||
console.error('Error loading historical fractions:', error);
|
||||
toast.error('Error al cargar fracciones históricas');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadFractions(1);
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (companyStore.activeCompany?.id) {
|
||||
loadFractions(1);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (companyStore.activeCompany?.id) {
|
||||
loadFractions(1);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-4 md:flex-row">
|
||||
<div class="flex-1">
|
||||
<label for="search-fraction" class="mb-2 block text-sm font-medium">Fracción Histórica</label>
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search-fraction"
|
||||
type="text"
|
||||
placeholder="Buscar fracción..."
|
||||
class="pl-9"
|
||||
bind:value={historicalFraction}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button onclick={handleSearch} disabled={loading}>Buscar</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Fracción</Table.Head>
|
||||
<Table.Head>Tipo</Table.Head>
|
||||
<Table.Head>UM</Table.Head>
|
||||
<Table.Head>País</Table.Head>
|
||||
<Table.Head>Fecha Pub.</Table.Head>
|
||||
<Table.Head>Fecha Fin</Table.Head>
|
||||
<Table.Head class="text-right">IGI</Table.Head>
|
||||
<Table.Head class="text-right">IGE</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={8} class="h-24 text-center">
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else if fractions.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={8} class="h-24 text-center"
|
||||
>No se encontraron resultados</Table.Cell
|
||||
>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
|
||||
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
|
||||
<Table.Cell>{fraction.country || '-'}</Table.Cell>
|
||||
<Table.Cell
|
||||
>{fraction.publication_date
|
||||
? new Date(fraction.publication_date).toLocaleDateString()
|
||||
: '-'}</Table.Cell
|
||||
>
|
||||
<Table.Cell
|
||||
>{fraction.end_date
|
||||
? new Date(fraction.end_date).toLocaleDateString()
|
||||
: '-'}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => loadFractions(page - 1)}
|
||||
disabled={page === 1 || loading}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages || 1}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => loadFractions(page + 1)}
|
||||
disabled={page >= totalPages || loading}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import {
|
||||
createTariffFraction,
|
||||
updateTariffFraction,
|
||||
type TariffFraction,
|
||||
type TariffFractionCreate,
|
||||
type TariffFractionUpdate
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
fraction = null, // If null, create mode. If set, edit mode.
|
||||
catalog = 'mex',
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
fraction?: TariffFraction | null;
|
||||
catalog?: string;
|
||||
onSuccess: () => void;
|
||||
} = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
|
||||
// Form fields
|
||||
let code = $state('');
|
||||
let fractionFormatted = $state('');
|
||||
let description = $state('');
|
||||
let nico = $state('');
|
||||
let umt = $state('');
|
||||
let adv_impo = $state('');
|
||||
let adv_expo = $state('');
|
||||
let um_code = $state(''); // New field for unit code
|
||||
|
||||
// Load data on open/fraction change
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (fraction) {
|
||||
// Edit mode
|
||||
code = fraction.code;
|
||||
fractionFormatted = fraction.fraction;
|
||||
description = fraction.description || '';
|
||||
nico = fraction.nico || '';
|
||||
umt = fraction.umt || '';
|
||||
adv_impo = fraction.adv_impo || '';
|
||||
adv_expo = fraction.adv_expo || '';
|
||||
um_code = fraction.um_code || '';
|
||||
} else {
|
||||
// Create mode - reset
|
||||
code = '';
|
||||
fractionFormatted = '';
|
||||
description = '';
|
||||
nico = '';
|
||||
umt = '';
|
||||
adv_impo = '';
|
||||
adv_expo = '';
|
||||
um_code = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
if (fraction) {
|
||||
// Update
|
||||
const updateData: TariffFractionUpdate = {
|
||||
fraction: fractionFormatted,
|
||||
description,
|
||||
nico: catalog === 'mex' ? nico : null,
|
||||
umt,
|
||||
adv_impo,
|
||||
adv_expo
|
||||
};
|
||||
await updateTariffFraction(fraction.id, updateData, companyId, catalog);
|
||||
toast.success('Fracción actualizada correctamente');
|
||||
} else {
|
||||
// Create
|
||||
const createData: TariffFractionCreate = {
|
||||
code,
|
||||
fraction: fractionFormatted,
|
||||
description,
|
||||
nico: catalog === 'mex' ? nico : null,
|
||||
umt,
|
||||
adv_impo,
|
||||
adv_expo
|
||||
};
|
||||
await createTariffFraction(createData, companyId, catalog);
|
||||
toast.success('Fracción creada correctamente');
|
||||
}
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error('Error saving fraction:', error);
|
||||
toast.error('Error al guardar la fracción');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[600px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title
|
||||
>{fraction ? 'Editar' : 'Crear'} Fracción Arancelaria {catalog === 'usa'
|
||||
? '(USA)'
|
||||
: ''}</Dialog.Title
|
||||
>
|
||||
<Dialog.Description>
|
||||
{fraction
|
||||
? 'Modifica los detalles de la fracción seleccionada.'
|
||||
: 'Ingresa los datos para la nueva fracción.'}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="code">Clave / Código {catalog === 'mex' ? '(Sin puntos)' : ''}</Label>
|
||||
<Input id="code" bind:value={code} disabled={!!fraction} placeholder="Ej. 01012101" />
|
||||
{#if fraction}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
El código no se puede modificar una vez creado.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="fraction">Fracción {catalog === 'mex' ? '(Con puntos)' : ''}</Label>
|
||||
<Input id="fraction" bind:value={fractionFormatted} placeholder="Ej. 0101.21.01" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="description">Descripción</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
placeholder="Descripción de la mercancía..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if catalog === 'mex'}
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="nico">NICO</Label>
|
||||
<Input id="nico" bind:value={nico} placeholder="Ej. 00" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="um_code">Clave U.M.</Label>
|
||||
<Input id="um_code" bind:value={um_code} placeholder="Ej. 06" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="umt">U.M.T</Label>
|
||||
<Input id="umt" bind:value={umt} placeholder="Ej. Kg" />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="umt">Unidad de Medida</Label>
|
||||
<Input id="umt" bind:value={umt} placeholder="Unit" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="adv_impo">Adv. Impo</Label>
|
||||
<Input id="adv_impo" bind:value={adv_impo} placeholder="Ej. Ex." />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="adv_expo">Adv. Expo</Label>
|
||||
<Input id="adv_expo" bind:value={adv_expo} placeholder="Ej. Ex." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
<Button onclick={handleSubmit} disabled={isLoading}>
|
||||
{#if isLoading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
Guardar
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,293 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/components/ui/table';
|
||||
import { Loader2, Plus, Search, Trash2, Edit } from 'lucide-svelte';
|
||||
import {
|
||||
getTariffFractions,
|
||||
deleteTariffFraction,
|
||||
type TariffFraction
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import TariffFractionFormDialog from './TariffFractionFormDialog.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
title = 'Fracciones Arancelarias',
|
||||
catalog = 'mex', // 'mex' or 'usa'
|
||||
levelFilter = null, // null or number
|
||||
readOnly = false
|
||||
}: {
|
||||
title?: string;
|
||||
catalog?: string;
|
||||
levelFilter?: number | null;
|
||||
readOnly?: boolean;
|
||||
} = $props();
|
||||
|
||||
let fractions = $state<TariffFraction[]>([]);
|
||||
let totalFractions = $state(0);
|
||||
let currentPage = $state(1);
|
||||
let pageSize = 50;
|
||||
let isLoading = $state(false);
|
||||
let search = $state('');
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
let isFormDialogOpen = $state(false);
|
||||
let selectedFraction = $state<TariffFraction | null>(null);
|
||||
let isManageMode = $state(false); // If true, opens form in edit mode
|
||||
|
||||
// Delete confirmation
|
||||
let showDeleteConfirm = $state(false);
|
||||
let fractionToDelete = $state<TariffFraction | null>(null);
|
||||
|
||||
async function loadFractions() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
const filters: Record<string, any> = {};
|
||||
if (search) filters.search = search;
|
||||
if (levelFilter !== null) filters.level = levelFilter;
|
||||
filters.catalog = catalog;
|
||||
|
||||
const response = await getTariffFractions(currentPage, pageSize, companyId, filters);
|
||||
|
||||
if (response.data) {
|
||||
fractions = response.data.items;
|
||||
totalFractions = response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading fractions:', error);
|
||||
toast.error('Error al cargar las fracciones');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearchInput() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
currentPage = 1;
|
||||
loadFractions();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
currentPage = newPage;
|
||||
loadFractions();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
selectedFraction = null;
|
||||
isManageMode = false; // Create mode
|
||||
isFormDialogOpen = true;
|
||||
}
|
||||
|
||||
function openEditDialog(fraction: TariffFraction) {
|
||||
selectedFraction = fraction;
|
||||
isManageMode = true; // Edit mode
|
||||
isFormDialogOpen = true;
|
||||
}
|
||||
|
||||
function confirmDelete(fraction: TariffFraction) {
|
||||
fractionToDelete = fraction;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!fractionToDelete || !companyStore.activeCompany?.id) return;
|
||||
|
||||
try {
|
||||
// Note: Delete might allow deleting items from source API if allowed,
|
||||
// or just local overrides. Assuming Service handles logic.
|
||||
await deleteTariffFraction(fractionToDelete.id, companyStore.activeCompany.id, catalog);
|
||||
toast.success('Fracción eliminada correctamente');
|
||||
loadFractions();
|
||||
} catch (error) {
|
||||
console.error('Error deleting fraction:', error);
|
||||
toast.error('Error al eliminar la fracción. Puede que esté en uso.');
|
||||
} finally {
|
||||
showDeleteConfirm = false;
|
||||
fractionToDelete = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (companyStore.activeCompany) {
|
||||
loadFractions();
|
||||
}
|
||||
});
|
||||
|
||||
// Reload when company changes
|
||||
$effect(() => {
|
||||
if (companyStore.activeCompany?.id) {
|
||||
loadFractions();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{title}</h2>
|
||||
{#if !readOnly}
|
||||
<Button onclick={openCreateDialog}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Fracción
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative max-w-sm flex-1">
|
||||
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Buscar..." class="pl-8" bind:value={search} oninput={handleSearchInput} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Clave</TableHead>
|
||||
<TableHead>Fracción</TableHead>
|
||||
<TableHead>Descripción</TableHead>
|
||||
{#if catalog === 'mex'}
|
||||
<TableHead>NICO</TableHead>
|
||||
<TableHead>U.M.T</TableHead>
|
||||
{:else}
|
||||
<TableHead>Unidad</TableHead>
|
||||
{/if}
|
||||
<TableHead>Adv. Impo</TableHead>
|
||||
<TableHead>Adv. Expo</TableHead>
|
||||
{#if !readOnly}
|
||||
<TableHead class="w-[100px]">Acciones</TableHead>
|
||||
{/if}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if isLoading}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
|
||||
class="h-24 text-center"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{:else if fractions.length === 0}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
|
||||
class="h-24 text-center"
|
||||
>
|
||||
No se encontraron resultados
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{:else}
|
||||
{#each fractions as fraction}
|
||||
<TableRow>
|
||||
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
|
||||
<TableCell class="font-medium">{fraction.fraction}</TableCell>
|
||||
<TableCell class="max-w-md truncate" title={fraction.description}>
|
||||
{fraction.description}
|
||||
</TableCell>
|
||||
{#if catalog === 'mex'}
|
||||
<TableCell>{fraction.nico || '-'}</TableCell>
|
||||
<TableCell>{fraction.umt || '-'}</TableCell>
|
||||
{:else}
|
||||
<TableCell>{fraction.umt || '-'}</TableCell>
|
||||
{/if}
|
||||
<TableCell>{fraction.adv_impo || '-'}</TableCell>
|
||||
<TableCell>{fraction.adv_expo || '-'}</TableCell>
|
||||
{#if !readOnly}
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
|
||||
<Edit class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-destructive hover:text-destructive"
|
||||
onclick={() => confirmDelete(fraction)}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{/each}
|
||||
{/if}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {currentPage} de {Math.ceil(totalFractions / pageSize) || 1}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={!fractions.length || fractions.length < pageSize || isLoading}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog.Root bind:open={showDeleteConfirm}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Esta acción no se puede deshacer. Se eliminará la fracción arancelaria permanentemente.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Cancelar</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
class="text-destructive-foreground bg-destructive hover:bg-destructive/90"
|
||||
onclick={handleDelete}
|
||||
>
|
||||
Eliminar
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
{#if isFormDialogOpen}
|
||||
<TariffFractionFormDialog
|
||||
bind:open={isFormDialogOpen}
|
||||
fraction={selectedFraction}
|
||||
{catalog}
|
||||
onSuccess={() => {
|
||||
loadFractions();
|
||||
isFormDialogOpen = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -304,31 +304,31 @@ export function getSidebarData(): SidebarData {
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.fractions.sitar"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/tariff-fractions/sitar",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.fractions.sitar_seventh_amendment"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/tariff-fractions/seventh-amendment",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.fractions.sitar_us"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/tariff-fractions/us",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.fractions.american"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/tariff-fractions/american",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.fractions.canadian"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/tariff-fractions/canadian",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.fractions.historical"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/tariff-fractions/historical",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.fractions.sectors"](),
|
||||
url: "#",
|
||||
url: "/dashboard/general_catalogs/sectors",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import SectorsList from '$lib/components/dashboard/general_catalogs/sectors/SectorsList.svelte';
|
||||
</script>
|
||||
|
||||
<SectorsList />
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import TariffFractionList from '$lib/components/dashboard/goods/fractions/TariffFractionList.svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<TariffFractionList title={m['sidebar.fractions.american']()} catalog="american" readOnly={false} />
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import CanadianFractionList from '$lib/components/dashboard/goods/fractions/CanadianFractionList.svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.fractions.canadian']()}</h1>
|
||||
</div>
|
||||
<CanadianFractionList />
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import HistoricalFractionList from '$lib/components/dashboard/goods/fractions/HistoricalFractionList.svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<HistoricalFractionList title={m['sidebar.fractions.historical']()} />
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import TariffFractionList from '$lib/components/dashboard/goods/fractions/TariffFractionList.svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<TariffFractionList
|
||||
title={m['sidebar.fractions.sitar_seventh_amendment']()}
|
||||
catalog="mex"
|
||||
levelFilter={5}
|
||||
readOnly={true}
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import TariffFractionList from '$lib/components/dashboard/goods/fractions/TariffFractionList.svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<TariffFractionList
|
||||
title={m['sidebar.fractions.sitar']()}
|
||||
catalog="mex"
|
||||
levelFilter={-1}
|
||||
readOnly={true}
|
||||
/>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import TariffFractionList from '$lib/components/dashboard/goods/fractions/TariffFractionList.svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
</script>
|
||||
|
||||
<TariffFractionList title={m['sidebar.fractions.sitar_us']()} catalog="usa" readOnly={true} />
|
||||
Reference in New Issue
Block a user