Merge branch 'feature/fraccion-arancelaria-nico' into fix/traduccion-bitacora
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
"""Add has_express_line to transporter; remove from company.
|
||||
|
||||
Revision ID: c8d9e0f1a2b3
|
||||
Revises: 6a7b8c9d0e1f
|
||||
Create Date: 2026-04-24
|
||||
|
||||
Upgrade: add transporter column first (NOT NULL + default), then drop company column.
|
||||
Downgrade: restore company column, drop transporter column (schema only; data not restored).
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "c8d9e0f1a2b3"
|
||||
down_revision: Union[str, Sequence[str], None] = "6a7b8c9d0e1f"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
SCHEMA = "a76"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"transporter",
|
||||
sa.Column(
|
||||
"has_express_line",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
schema=SCHEMA,
|
||||
)
|
||||
op.drop_column("company", "has_express_line", schema=SCHEMA)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column(
|
||||
"company",
|
||||
sa.Column(
|
||||
"has_express_line",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=True,
|
||||
),
|
||||
schema=SCHEMA,
|
||||
)
|
||||
op.drop_column("transporter", "has_express_line", schema=SCHEMA)
|
||||
@@ -58,7 +58,6 @@ class CompanyCreateDTO(BaseModel):
|
||||
|
||||
# Configuration
|
||||
logo: Optional[str] = Field(None, max_length=512, description="Company logo (path local o clave S3)")
|
||||
has_express_line: Optional[bool] = Field(None, description="Has express line")
|
||||
order_format_type: Optional[str] = Field(
|
||||
None, max_length=19, description="Order format type"
|
||||
)
|
||||
@@ -293,7 +292,6 @@ class CompanyResponseDTO(BaseModel):
|
||||
|
||||
# Configuration
|
||||
logo: Optional[str] = None
|
||||
has_express_line: Optional[bool] = None
|
||||
fiscal_deposit: Optional[bool] = None
|
||||
generate_barcodes_with_fiel: Optional[bool] = None
|
||||
order_format_type: Optional[str] = None
|
||||
|
||||
@@ -61,7 +61,6 @@ class Company(Base, TimestampMixin):
|
||||
|
||||
# Configuración básica
|
||||
logo: Mapped[Optional[str]] = mapped_column(String(512))
|
||||
has_express_line: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false")
|
||||
fiscal_deposit: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false")
|
||||
generate_barcodes_with_fiel: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false")
|
||||
order_format_type: Mapped[Optional[str]] = mapped_column(String(19))
|
||||
|
||||
@@ -119,7 +119,7 @@ class CompanyService:
|
||||
"prosec", "prosec_authorization", "sector1", "sector2", "sector3",
|
||||
"manufacturer_id", "broker_company", "responsible", "responsible_name",
|
||||
"responsible_last_name", "responsible_mother_last_name", "responsible_rfc",
|
||||
"position", "logo", "has_express_line", "fiscal_deposit",
|
||||
"position", "logo", "fiscal_deposit",
|
||||
"generate_barcodes_with_fiel", "order_format_type",
|
||||
"is_service_company", "client_name", "subassembly_mode", "previous_code",
|
||||
"active_labels", "active_fractions", "activate_caat", "trans_interface",
|
||||
|
||||
@@ -4,7 +4,7 @@ Catálogo de referencia global (no tenant-scoped)
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
@@ -32,7 +32,10 @@ async def list_tariff_fractions(
|
||||
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'"),
|
||||
catalog: Optional[str] = Query(
|
||||
"mex",
|
||||
description="Catalog: 'mex' (default), 'usa', or 'american' (both US read-only via SITAR)",
|
||||
),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
@@ -88,7 +91,7 @@ async def get_tariff_fraction(
|
||||
"/",
|
||||
response_model=TariffFractionResponseDTO,
|
||||
summary="Create Tariff Fraction",
|
||||
description="Create a new tariff fraction (Only supported for 'american' catalog)",
|
||||
description="Not supported: all catalogs are read-only (SITAR-backed for US).",
|
||||
)
|
||||
async def create_tariff_fraction(
|
||||
fraction_data: TariffFractionCreateDTO,
|
||||
@@ -97,101 +100,36 @@ async def create_tariff_fraction(
|
||||
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.fraction, # Store the punctuated fraction in the DB
|
||||
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, us_dto, tenant_id, company_id)
|
||||
return TariffFractionService.to_domain_usa_local(created)
|
||||
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Creation not allowed for '{catalog}' catalog (Read-Only)")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail=f"Creation not allowed for '{catalog}' catalog (read-only; US data from SITAR).",
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{tariff_fraction_id}",
|
||||
response_model=TariffFractionResponseDTO,
|
||||
summary="Update Tariff Fraction",
|
||||
description="Update a tariff fraction (Only supported for 'american' catalog)",
|
||||
description="Not supported: all catalogs are read-only (SITAR-backed for US).",
|
||||
)
|
||||
async def update_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
fraction_data: TariffFractionUpdateDTO,
|
||||
catalog: str = Query("mex", description="Catalog source: 'mex', 'usa', or 'american'"),
|
||||
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(
|
||||
code=fraction_data.fraction,
|
||||
description=fraction_data.description,
|
||||
unit_of_measure=fraction_data.umt,
|
||||
ad_valorem=ad_valorem
|
||||
)
|
||||
|
||||
updated = USTariffFractionService.update(db, tariff_fraction_id, tenant_id, us_dto, company_id)
|
||||
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)")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail=f"Update not allowed for '{catalog}' catalog (read-only; US data from SITAR).",
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{tariff_fraction_id}",
|
||||
summary="Delete Tariff Fraction",
|
||||
description="Delete a tariff fraction (Only supported for 'american' catalog)",
|
||||
description="Not supported: all catalogs are read-only (SITAR-backed for US).",
|
||||
)
|
||||
async def delete_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
@@ -200,16 +138,8 @@ async def delete_tariff_fraction(
|
||||
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, tariff_fraction_id, tenant_id, company_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)")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail=f"Delete not allowed for '{catalog}' catalog (read-only; US data from SITAR).",
|
||||
)
|
||||
|
||||
|
||||
@@ -111,6 +111,49 @@ class TariffFractionService:
|
||||
adv_expo=None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _fetch_usa_catalog_from_sitar(
|
||||
skip: int,
|
||||
limit: int,
|
||||
filters: Optional[Dict[str, Any]],
|
||||
) -> Tuple[List[TariffFraction], int]:
|
||||
"""SITAR fracciones-usa for catalog 'usa' and 'american' (read-only, same source)."""
|
||||
try:
|
||||
usa_service = FraccionesUSAService.get_instance()
|
||||
except ValueError:
|
||||
logger.warning("SITAR USA not configured (missing env)")
|
||||
return [], 0
|
||||
|
||||
search_term = None
|
||||
search_description = None
|
||||
|
||||
if filters and filters.get("search"):
|
||||
term = filters["search"]
|
||||
clean_term = term.replace(".", "")
|
||||
if clean_term.isdigit() and len(clean_term) >= 4:
|
||||
search_term = term
|
||||
else:
|
||||
search_description = term
|
||||
|
||||
try:
|
||||
usa_items = await usa_service.search(
|
||||
fraccion=search_term,
|
||||
descripcion=search_description,
|
||||
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:
|
||||
import traceback
|
||||
|
||||
logger.error("Error fetching USA fractions from SITAR: %s", e)
|
||||
logger.error(traceback.format_exc())
|
||||
return [], 0
|
||||
|
||||
@staticmethod
|
||||
async def get_all(
|
||||
db: Session,
|
||||
@@ -123,64 +166,15 @@ class TariffFractionService:
|
||||
) -> Tuple[List[TariffFraction], int]:
|
||||
"""
|
||||
Obtiene fracciones arancelarias.
|
||||
Estrategia:
|
||||
- MEX: Sitar API -> Fallback Local DB
|
||||
- USA: Local DB (Defined by user requirement)
|
||||
Estrategia:
|
||||
- MEX: Sitar API (fracciones)
|
||||
- USA / AMERICAN: Sitar API (fracciones-usa), solo lectura
|
||||
"""
|
||||
|
||||
# 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 = await USTariffFractionService.get_all(
|
||||
db, tenant_id, company_id, skip, limit, filters
|
||||
|
||||
if catalog in ("american", "usa"):
|
||||
return await TariffFractionService._fetch_usa_catalog_from_sitar(
|
||||
skip, limit, filters
|
||||
)
|
||||
|
||||
items = [TariffFractionService.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
|
||||
search_description = None
|
||||
|
||||
if filters and filters.get("search"):
|
||||
term = filters["search"]
|
||||
# Simple heuristic: if it looks like a code, use code search, else description
|
||||
# FIX: Short numeric codes (e.g. "01") often fail strict 'fraccion' search.
|
||||
# Treat them as description search for partial matching.
|
||||
clean_term = term.replace(".", "")
|
||||
if clean_term.isdigit() and len(clean_term) >= 4:
|
||||
search_term = term
|
||||
else:
|
||||
search_description = term
|
||||
|
||||
# USA Service search signature: fraccion, descripcion, skip, limit
|
||||
usa_items = await usa_service.search(
|
||||
fraccion=search_term,
|
||||
descripcion=search_description,
|
||||
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:
|
||||
import traceback
|
||||
logger.error(f"Error fetching USA fractions (API): {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
# Return empty list on error as per requirement (since API is broken)
|
||||
return [], 0
|
||||
|
||||
# MEX (SITAR) CATALOG HANDLING
|
||||
try:
|
||||
|
||||
@@ -1,70 +1,131 @@
|
||||
"""
|
||||
Endpoints API para fracciones arancelarias americanas
|
||||
Endpoints API para fracciones arancelarias americanas (solo lectura; fuente SITAR).
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
|
||||
from .dto import (
|
||||
USTariffFractionCreateDTO,
|
||||
USTariffFractionResponseDTO,
|
||||
USTariffFractionUpdateDTO,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
canonical_code_from_sitar_row,
|
||||
)
|
||||
from .service import USTariffFractionService
|
||||
from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse
|
||||
from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService
|
||||
|
||||
# Create router using TenantCRUDRoutes factory for basic CRUD operations (prefix="" so we mount under main_router)
|
||||
crud_router = TenantCRUDRoutes(
|
||||
service=USTariffFractionService,
|
||||
create_schema=USTariffFractionCreateDTO,
|
||||
update_schema=USTariffFractionUpdateDTO,
|
||||
response_schema=USTariffFractionResponseDTO,
|
||||
prefix="",
|
||||
from .dto import USTariffFractionResponseDTO
|
||||
|
||||
|
||||
def _sitar_row_to_us_response_payload(item: FraccionesUSAResponse) -> dict:
|
||||
"""Build payload for USTariffFractionResponseDTO.model_validate (SITAR row)."""
|
||||
canon = canonical_code_from_sitar_row(item)
|
||||
now = datetime.now(timezone.utc)
|
||||
return {
|
||||
"id": item.CONSECUTIVO,
|
||||
"code": canon,
|
||||
"prefix": item.FRACCION_SIN_PUNTO,
|
||||
"type_code": str(item.NIVEL) if item.NIVEL is not None else None,
|
||||
"ad_valorem": american_fraction_ad_valorem_from_row(item),
|
||||
"fixed_cost": None,
|
||||
"unit_of_measure": item.UNIDADCANTIDAD,
|
||||
"description": item.DESCRIPCION,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
|
||||
main_router = APIRouter(
|
||||
prefix="/us-tariff-fractions",
|
||||
tags=["a76 / general catalogs / us tariff fractions"],
|
||||
resource_name="US Tariff Fraction",
|
||||
id_name="id",
|
||||
enable_list=False, # We implement our custom list endpoint
|
||||
)
|
||||
|
||||
# Master router with prefix so all routes live under /us-tariff-fractions
|
||||
from api.v1.modules.a76.layouts_csv.us_tariff_fractions.routes import router as imports_router
|
||||
main_router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
|
||||
main_router.include_router(imports_router, prefix="/imports", tags=["us_tariff_fractions / csv_import"])
|
||||
main_router.include_router(crud_router.router)
|
||||
|
||||
@main_router.api_route(
|
||||
"/imports",
|
||||
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
||||
include_in_schema=False,
|
||||
)
|
||||
@main_router.api_route(
|
||||
"/imports/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def us_tariff_imports_disabled(path: str = ""):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE,
|
||||
detail="CSV import for US tariff fractions is disabled; catalog is read-only from SITAR.",
|
||||
)
|
||||
|
||||
|
||||
# Custom list endpoint with search filter (under /us-tariff-fractions/)
|
||||
@main_router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary="List US Tariff Fractions",
|
||||
description="Get paginated list of US Tariff Fractions with optional search filter",
|
||||
description="Paginated list from SITAR fracciones-usa (read-only).",
|
||||
)
|
||||
async def list_us_tariff_fractions(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
|
||||
search: Optional[str] = Query(None, description="Search in code, description, or prefix"),
|
||||
search: Optional[str] = Query(None, description="Search in code or description"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
filters = {}
|
||||
try:
|
||||
svc = FraccionesUSAService.get_instance()
|
||||
except ValueError:
|
||||
return {
|
||||
"items": [],
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": 0,
|
||||
}
|
||||
|
||||
search_term = None
|
||||
search_description = None
|
||||
if search:
|
||||
filters["search"] = search
|
||||
|
||||
items, total = await USTariffFractionService.get_all(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
)
|
||||
|
||||
clean = search.replace(".", "")
|
||||
if clean.isdigit() and len(clean) >= 4:
|
||||
search_term = search
|
||||
else:
|
||||
search_description = search
|
||||
|
||||
try:
|
||||
sitar_items = await svc.search(
|
||||
fraccion=search_term,
|
||||
descripcion=search_description,
|
||||
skip=skip,
|
||||
limit=page_size,
|
||||
)
|
||||
except Exception:
|
||||
return {
|
||||
"items": [],
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": 0,
|
||||
}
|
||||
|
||||
total = len(sitar_items) + skip
|
||||
if len(sitar_items) == page_size:
|
||||
total += 1
|
||||
|
||||
items = [
|
||||
USTariffFractionResponseDTO.model_validate(_sitar_row_to_us_response_payload(row))
|
||||
for row in sitar_items
|
||||
]
|
||||
|
||||
return {
|
||||
"items": [USTariffFractionResponseDTO.model_validate(item) for item in items],
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
@@ -72,4 +133,28 @@ async def list_us_tariff_fractions(
|
||||
}
|
||||
|
||||
|
||||
@main_router.post("/")
|
||||
async def create_us_tariff_fraction_disabled():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail="US tariff fractions are read-only (SITAR).",
|
||||
)
|
||||
|
||||
|
||||
@main_router.put("/{fraction_id}")
|
||||
async def update_us_tariff_fraction_disabled(fraction_id: int):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail="US tariff fractions are read-only (SITAR).",
|
||||
)
|
||||
|
||||
|
||||
@main_router.delete("/{fraction_id}")
|
||||
async def delete_us_tariff_fraction_disabled(fraction_id: int):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail="US tariff fractions are read-only (SITAR).",
|
||||
)
|
||||
|
||||
|
||||
router = main_router
|
||||
|
||||
@@ -14,8 +14,9 @@ from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from api.v1.modules.public.reference_data.countries.models import Country
|
||||
@@ -363,48 +364,8 @@ def validate_common(
|
||||
)
|
||||
|
||||
if line.customs.american_fraction:
|
||||
def _normalize_american_fraction_code(raw_code: str) -> list[str]:
|
||||
normalized_raw = (raw_code or "").strip()
|
||||
if not normalized_raw:
|
||||
return []
|
||||
|
||||
digits_only = normalized_raw.replace(".", "").replace(" ", "").replace("-", "")
|
||||
candidates = [normalized_raw]
|
||||
|
||||
if len(digits_only) == 10:
|
||||
candidates.append(
|
||||
f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}.{digits_only[8:10]}"
|
||||
)
|
||||
elif len(digits_only) == 8:
|
||||
candidates.append(f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}")
|
||||
|
||||
candidates.append(digits_only)
|
||||
|
||||
seen: set[str] = set()
|
||||
deduped: list[str] = []
|
||||
for candidate in candidates:
|
||||
if not candidate or candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
deduped.append(candidate)
|
||||
return deduped
|
||||
|
||||
candidates = _normalize_american_fraction_code(line.customs.american_fraction)
|
||||
us_fraction: USTariffFraction | None = None
|
||||
for candidate in candidates:
|
||||
us_fraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == candidate,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if us_fraction:
|
||||
break
|
||||
|
||||
if not us_fraction:
|
||||
resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
|
||||
if not resolved:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.american_fraction",
|
||||
message="La fracción americana especificada no existe.",
|
||||
@@ -412,7 +373,8 @@ def validate_common(
|
||||
code="AMERICAN_FRACTION_NOT_FOUND",
|
||||
)
|
||||
else:
|
||||
line.customs.american_fraction = us_fraction.code
|
||||
_, canon = resolved
|
||||
line.customs.american_fraction = store_canonical_american_code(canon)
|
||||
|
||||
if line.order:
|
||||
if len(line.order) > 20:
|
||||
|
||||
@@ -10,8 +10,10 @@ from ...models import LineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
|
||||
from api.v1.modules.a76.app_settings.service import AppSettingsService
|
||||
@@ -488,25 +490,15 @@ def validate_create(
|
||||
if not line.customs.american_fraction and class_info and class_info.us_fraction:
|
||||
line.customs.american_fraction = class_info.us_fraction
|
||||
|
||||
# Buscar el advalorem de la fracción americana
|
||||
# Ad valorem desde catálogo SITAR (fracciones-usa)
|
||||
if line.customs.american_fraction:
|
||||
us_fraction: USTariffFraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == line.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
|
||||
if resolved:
|
||||
sitar_row, canon = resolved
|
||||
line.customs.american_fraction = store_canonical_american_code(canon)
|
||||
line.customs.advalorem_american = american_fraction_ad_valorem_from_row(
|
||||
sitar_row
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if us_fraction:
|
||||
# Si el tipo es 'ME' (Moneda Extranjera), usar costo fijo
|
||||
# De lo contrario, usar ad valorem
|
||||
if us_fraction.type_code == "foreign":
|
||||
line.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line.customs.advalorem_american = us_fraction.ad_valorem
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR DESCRIPCIONES POR DEFECTO
|
||||
|
||||
@@ -6,8 +6,10 @@ from core.exceptions import ErrorCollector
|
||||
from ...models import LineItem
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from .common import validate_common
|
||||
@@ -158,24 +160,15 @@ def validate_update(
|
||||
if not line.customs.sector:
|
||||
line.customs.sector = existing_line.customs.sector
|
||||
|
||||
# Fracción americana y su advalorem
|
||||
# Fracción americana y su advalorem (SITAR)
|
||||
if line.customs.american_fraction:
|
||||
# Se proporcionó nueva fracción americana, buscar su advalorem
|
||||
us_fraction: USTariffFraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == line.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
|
||||
if resolved:
|
||||
sitar_row, canon = resolved
|
||||
line.customs.american_fraction = store_canonical_american_code(canon)
|
||||
line.customs.advalorem_american = american_fraction_ad_valorem_from_row(
|
||||
sitar_row
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if us_fraction:
|
||||
if us_fraction.type_code == "ME":
|
||||
line.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line.customs.advalorem_american = us_fraction.ad_valorem
|
||||
else:
|
||||
# Mantener fracción americana existente
|
||||
line.customs.american_fraction = existing_line.customs.american_fraction
|
||||
|
||||
@@ -9,8 +9,9 @@ from ...common.fractions import search_fraction_preference
|
||||
from ...common.common_validators import item_exists
|
||||
from ...models import LineItem
|
||||
from ...line_customs.models import FractionType, LineCustom
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from api.v1.modules.a76.items.schemas import LineItemCreate
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
@@ -26,9 +27,6 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.app_settings.service import AppSettingsService
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def validate_common(
|
||||
db: Session,
|
||||
line: LineItemCreate,
|
||||
@@ -291,66 +289,9 @@ def validate_common(
|
||||
)
|
||||
|
||||
if line.customs.american_fraction:
|
||||
def _normalize_american_fraction_code(raw_code: str) -> list[str]:
|
||||
"""
|
||||
Attempts to map user input to the canonical USTariffFraction.code.
|
||||
|
||||
The catalog commonly stores dotted HTS codes (e.g. 3802.20.00.00),
|
||||
but users may paste/enter digits-only or use different separators.
|
||||
"""
|
||||
|
||||
normalized_raw = (raw_code or "").strip()
|
||||
if not normalized_raw:
|
||||
return []
|
||||
|
||||
digits_only = re.sub(r"[.\s\-]", "", normalized_raw)
|
||||
|
||||
candidates: list[str] = []
|
||||
|
||||
# 1) Exact input
|
||||
candidates.append(normalized_raw)
|
||||
|
||||
# 2) Canonical with dots if length matches common patterns
|
||||
if len(digits_only) == 10:
|
||||
candidates.append(
|
||||
f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}.{digits_only[8:10]}"
|
||||
)
|
||||
elif len(digits_only) == 8:
|
||||
candidates.append(
|
||||
f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}"
|
||||
)
|
||||
|
||||
# 3) Digits-only (if catalog stores without dots)
|
||||
candidates.append(digits_only)
|
||||
|
||||
# De-duplicate while preserving order
|
||||
seen: set[str] = set()
|
||||
deduped: list[str] = []
|
||||
for c in candidates:
|
||||
if not c or c in seen:
|
||||
continue
|
||||
seen.add(c)
|
||||
deduped.append(c)
|
||||
return deduped
|
||||
|
||||
raw_american_fraction = str(line.customs.american_fraction)
|
||||
candidates = _normalize_american_fraction_code(raw_american_fraction)
|
||||
|
||||
us_fraction: USTariffFraction | None = None
|
||||
for candidate in candidates:
|
||||
us_fraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == candidate,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if us_fraction:
|
||||
break
|
||||
|
||||
if not us_fraction:
|
||||
resolved = resolve_american_fraction_from_sitar(raw_american_fraction)
|
||||
if not resolved:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.american_fraction",
|
||||
message="La fracción americana especificada no existe.",
|
||||
@@ -358,8 +299,8 @@ def validate_common(
|
||||
code="AMERICAN_FRACTION_NOT_FOUND",
|
||||
)
|
||||
else:
|
||||
# Keep canonical value so downstream validators can use it safely.
|
||||
line.customs.american_fraction = us_fraction.code
|
||||
_, canon = resolved
|
||||
line.customs.american_fraction = store_canonical_american_code(canon)
|
||||
|
||||
if line.order:
|
||||
if len(line.order) > 20:
|
||||
|
||||
@@ -10,8 +10,10 @@ from ...models import LineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from api.v1.modules.a76.app_settings.service import AppSettingsService
|
||||
from .common import validate_common
|
||||
@@ -381,25 +383,15 @@ def validate_create(
|
||||
if not line.customs.american_fraction and class_info and class_info.us_fraction:
|
||||
line.customs.american_fraction = class_info.us_fraction
|
||||
|
||||
# Buscar el advalorem de la fracción americana
|
||||
# Ad valorem desde catálogo SITAR (fracciones-usa)
|
||||
if line.customs.american_fraction:
|
||||
us_fraction: USTariffFraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == line.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
|
||||
if resolved:
|
||||
sitar_row, canon = resolved
|
||||
line.customs.american_fraction = store_canonical_american_code(canon)
|
||||
line.customs.advalorem_american = american_fraction_ad_valorem_from_row(
|
||||
sitar_row
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if us_fraction:
|
||||
# Si el tipo es 'ME' (Moneda Extranjera), usar costo fijo
|
||||
# De lo contrario, usar ad valorem
|
||||
if us_fraction.type_code == "foreign":
|
||||
line.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line.customs.advalorem_american = us_fraction.ad_valorem
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR DESCRIPCIONES POR DEFECTO
|
||||
|
||||
@@ -4,8 +4,10 @@ from core.exceptions import ErrorCollector
|
||||
|
||||
from ...models import LineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from .common import validate_common
|
||||
|
||||
@@ -155,24 +157,15 @@ def validate_update(
|
||||
if not line.customs.sector:
|
||||
line.customs.sector = existing_line.customs.sector
|
||||
|
||||
# Fracción americana y su advalorem
|
||||
# Fracción americana y su advalorem (SITAR)
|
||||
if line.customs.american_fraction:
|
||||
# Se proporcionó nueva fracción americana, buscar su advalorem
|
||||
us_fraction: USTariffFraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == line.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
|
||||
if resolved:
|
||||
sitar_row, canon = resolved
|
||||
line.customs.american_fraction = store_canonical_american_code(canon)
|
||||
line.customs.advalorem_american = american_fraction_ad_valorem_from_row(
|
||||
sitar_row
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if us_fraction:
|
||||
if us_fraction.type_code == "ME":
|
||||
line.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line.customs.advalorem_american = us_fraction.ad_valorem
|
||||
else:
|
||||
# Mantener fracción americana existente
|
||||
line.customs.american_fraction = existing_line.customs.american_fraction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Carga de conjuntos FK para validación/mapeo de import CSV de clases de materiales.
|
||||
Clarion: Tipo Activo Fijo, U.M., Fracción Mex (GFracGenSifra + histórico), Fracción Ame (GFracAme), Código Producto CP (si existe).
|
||||
Carga de conjuntos FK para validaci?n/mapeo de import CSV de clases de materiales.
|
||||
Clarion: Tipo Activo Fijo, U.M., Fracci?n Mex (GFracGenSifra + hist?rico), Fracci?n Ame (GFracAme), C?digo Producto CP (si existe).
|
||||
"""
|
||||
from typing import Set, Tuple
|
||||
|
||||
@@ -12,11 +12,11 @@ def load_classes_fk_sets(
|
||||
company_id: int,
|
||||
) -> Tuple[Set[str], Set[str], Set[str], Set[str], Set[str]]:
|
||||
"""
|
||||
Carga todos los conjuntos necesarios para validación CSV de clases (paridad Clarion).
|
||||
Carga todos los conjuntos necesarios para validaci?n CSV de clases (paridad Clarion).
|
||||
Devuelve (valid_material_keys, valid_uom_codes, valid_fraction_mex_8, valid_fraction_ame, valid_product_codes_cp).
|
||||
- valid_fraction_mex_8: códigos de 8 caracteres válidos (TariffFraction + HistoricalTariffFraction).
|
||||
- valid_fraction_ame: códigos de fracción americana (USTariffFraction por tenant/company).
|
||||
- valid_product_codes_cp: códigos de producto/servicio CP (vacío si no existe catálogo).
|
||||
- valid_fraction_mex_8: c?digos de 8 caracteres v?lidos (TariffFraction + HistoricalTariffFraction).
|
||||
- valid_fraction_ame: c?digos de fracci?n americana (USTariffFraction por tenant/company).
|
||||
- valid_product_codes_cp: c?digos de producto/servicio CP (vac?o si no existe cat?logo).
|
||||
"""
|
||||
valid_material_keys: Set[str] = set()
|
||||
valid_uom_codes: Set[str] = set()
|
||||
@@ -29,8 +29,6 @@ def load_classes_fk_sets(
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.models import HistoricalTariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
|
||||
for m in session.query(MaterialType.key).all():
|
||||
if m[0]:
|
||||
valid_material_keys.add(m[0])
|
||||
@@ -63,16 +61,7 @@ def load_classes_fk_sets(
|
||||
if row[0] and row[0].strip():
|
||||
valid_fraction_mex_8.add(row[0].strip()[:8])
|
||||
|
||||
for row in (
|
||||
session.query(USTariffFraction.code)
|
||||
.filter(
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.all()
|
||||
):
|
||||
if row[0]:
|
||||
valid_fraction_ame.add(row[0].strip())
|
||||
# Fracci?n americana: validaci?n por fila contra SITAR (no se precarga el cat?logo completo).
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
@@ -144,20 +144,23 @@ def validate_row_fraction_mex_catalog(
|
||||
def validate_row_fraction_ame_catalog(
|
||||
row: Dict[str, Any],
|
||||
line_num: int,
|
||||
valid_fraction_ame: Optional[Set[str]],
|
||||
valid_fraction_ame: Optional[Set[str]] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Col G: si no vacía, debe existir en catálogo Fracciones Americanas (Clarion GFracAme)."""
|
||||
"""Col G: si no vacía, debe existir en SITAR fracciones-usa (valid_fraction_ame ignorado; compat)."""
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
resolve_american_fraction_from_sitar,
|
||||
)
|
||||
|
||||
val = (row.get("FRACCIONAME") or "").strip()
|
||||
if not val or valid_fraction_ame is None:
|
||||
if not val:
|
||||
return None
|
||||
if val in valid_fraction_ame:
|
||||
if resolve_american_fraction_from_sitar(val):
|
||||
return None
|
||||
return {
|
||||
"line": line_num,
|
||||
"col": "FRACCIONAME",
|
||||
"msg": (
|
||||
f"Error: (Col. G) La Fraccion Americana: {val} no existe en el Catálogo de Fracciones Americanas. "
|
||||
"Dar de alta la Fracción Americana en el Catálogo de Fracciones Americanas."
|
||||
f"Error: (Col. G) La Fraccion Americana: {val} no existe en el catálogo SITAR (fracciones USA)."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
store_canonical_american_code,
|
||||
)
|
||||
from api.v1.modules.a24.fa.fa_item_lines.dto import FaLineItemCreateDTO
|
||||
|
||||
@@ -215,20 +217,15 @@ def apply_import_defaults_and_calculations_for_csv(
|
||||
line_data.customs.american_fraction = class_info.us_fraction
|
||||
|
||||
if line_data.customs.american_fraction:
|
||||
us_fraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == line_data.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
resolved = resolve_american_fraction_from_sitar(
|
||||
str(line_data.customs.american_fraction)
|
||||
)
|
||||
if us_fraction:
|
||||
if getattr(us_fraction, "type_code", None) == "foreign":
|
||||
line_data.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line_data.customs.advalorem_american = us_fraction.ad_valorem
|
||||
if resolved:
|
||||
sitar_row, canon = resolved
|
||||
line_data.customs.american_fraction = store_canonical_american_code(canon)
|
||||
line_data.customs.advalorem_american = american_fraction_ad_valorem_from_row(
|
||||
sitar_row
|
||||
)
|
||||
|
||||
# Fallback explícito: primero descripción de parte (si existe), luego clase.
|
||||
if not line_data.description.description_spanish and part and part.description_spanish:
|
||||
|
||||
@@ -1248,7 +1248,6 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
from api.v1.modules.a76.general_catalogs.sectors.models import Sector
|
||||
from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod
|
||||
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from .validators.partidas_impo_temp import validate_row_partidas_impo_temp
|
||||
@@ -1369,10 +1368,7 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
if row[1]:
|
||||
valid_country_keys.add((row[1] or "").strip().upper())
|
||||
|
||||
valid_fraction_ame: Set[str] = set()
|
||||
for row in session.query(USTariffFraction.code).filter(USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id).all():
|
||||
if row[0]:
|
||||
valid_fraction_ame.add((row[0] or "").strip())
|
||||
valid_fraction_ame: Set[str] = set() # unused; SITAR resolve per row in validators
|
||||
|
||||
authorized_sectors: Set[str] = set()
|
||||
for row in session.query(Sector.key).filter(
|
||||
@@ -1630,7 +1626,6 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
from api.v1.modules.a76.general_catalogs.sectors.models import Sector
|
||||
from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod
|
||||
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from .validators.partidas_impo_def import validate_row_partidas_impo_def
|
||||
@@ -1753,10 +1748,7 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
if row[1]:
|
||||
valid_country_keys.add((row[1] or "").strip().upper())
|
||||
|
||||
valid_fraction_ame: Set[str] = set()
|
||||
for row in session.query(USTariffFraction.code).filter(USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id).all():
|
||||
if row[0]:
|
||||
valid_fraction_ame.add((row[0] or "").strip())
|
||||
valid_fraction_ame: Set[str] = set() # unused; SITAR resolve per row in validators
|
||||
|
||||
authorized_sectors: Set[str] = set()
|
||||
for row in session.query(Sector.key).filter(
|
||||
@@ -1915,7 +1907,6 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from .validators.partidas_expo import validate_row_partidas_expo
|
||||
@@ -2075,10 +2066,7 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
if row[0] is not None:
|
||||
valid_payment_methods.add(str(row[0]).strip())
|
||||
|
||||
valid_fraction_ame: Set[str] = set()
|
||||
for row in session.query(USTariffFraction.code).filter(USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id).all():
|
||||
if row[0]:
|
||||
valid_fraction_ame.add((row[0] or "").strip())
|
||||
valid_fraction_ame: Set[str] = set() # unused; SITAR resolve per row in validators
|
||||
|
||||
valid_part_numbers: Set[str] = set()
|
||||
for row in session.query(Part.part_number).filter(Part.tenant_id == tenant_id, Part.company_id == company_id).all():
|
||||
@@ -2209,7 +2197,6 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
from api.v1.modules.a76.general_catalogs.sectors.models import Sector
|
||||
from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod
|
||||
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from .validators.partidas_impo_def import validate_row_partidas_impo_def
|
||||
@@ -2331,10 +2318,7 @@ def _do_scan_file(self, job_id: str, model_target: str, config: Optional[str] =
|
||||
if row[1]:
|
||||
valid_country_keys.add((row[1] or "").strip().upper())
|
||||
|
||||
valid_fraction_ame: Set[str] = set()
|
||||
for row in session.query(USTariffFraction.code).filter(USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id).all():
|
||||
if row[0]:
|
||||
valid_fraction_ame.add((row[0] or "").strip())
|
||||
valid_fraction_ame: Set[str] = set() # unused; SITAR resolve per row in validators
|
||||
|
||||
authorized_sectors: Set[str] = set()
|
||||
for row in session.query(Sector.key).filter(
|
||||
|
||||
@@ -400,14 +400,18 @@ def _validaciones_par_expo(
|
||||
f"Error: (Celda M{line_num}) La Forma de Pago Capturado no es Válido. "
|
||||
"Capturar en la Celda M una Forma de Pago dentro del Catálogo General de Formas de Pago.",
|
||||
)
|
||||
# Fracción americana (Clarion Col R)
|
||||
# Fracción americana (Clarion Col R) — SITAR fracciones-usa
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
resolve_american_fraction_from_sitar,
|
||||
)
|
||||
|
||||
frac_ame = _get(row, "FRACCION AMERICANA", "FRACCIONAMERICANA")
|
||||
if frac_ame and valid_fraction_ame and frac_ame not in valid_fraction_ame:
|
||||
if frac_ame and not resolve_american_fraction_from_sitar(frac_ame):
|
||||
return _err(
|
||||
line_num,
|
||||
"FRACCION AMERICANA",
|
||||
f"Error: (Celda R{line_num}) La Fracción Americana: {frac_ame} no está en el Catálogo de Fracciones Americanas. "
|
||||
"Capturar en la Celda R una fracción que se encuentre en el catálogo o dar la de alta.",
|
||||
f"Error: (Celda R{line_num}) La Fracción Americana: {frac_ame} no está en el catálogo SITAR (fracciones USA). "
|
||||
"Capturar en la Celda R una fracción válida según SITAR.",
|
||||
)
|
||||
# Orden de compra / orden venta (Clarion Col S) máx 20
|
||||
orden = _get(row, "ORDEN DE COMPRA", "ORDENCOMPRA", "ORDEN DE VENTA")
|
||||
|
||||
@@ -366,10 +366,17 @@ def _validaciones_parimpo_tem(
|
||||
f"Error: (Celda M{line_num} y N{line_num}) La Preferencia Arancelaria es: {pref_ctx} y en la columna N tiene sector.",
|
||||
)
|
||||
|
||||
# O: Fracción americana
|
||||
# O: Fracción americana (SITAR fracciones-usa; valid_fraction_ame ignorado)
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
resolve_american_fraction_from_sitar,
|
||||
)
|
||||
|
||||
frac_ame = _get(row, "FRACCION AMERICANA", "FRACCIONAMERICANA")
|
||||
if frac_ame and frac_ame not in valid_fraction_ame:
|
||||
return err("FRACCION AMERICANA", f"Advertencia: (Celda O{line_num}) La Fracción Americana: {frac_ame} no existe en el Catálogo de Fracciones Americanas.")
|
||||
if frac_ame and not resolve_american_fraction_from_sitar(frac_ame):
|
||||
return err(
|
||||
"FRACCION AMERICANA",
|
||||
f"Advertencia: (Celda O{line_num}) La Fracción Americana: {frac_ame} no existe en el catálogo SITAR (fracciones USA).",
|
||||
)
|
||||
|
||||
# P: Orden de compra máx 20
|
||||
orden = _get(row, "ORDEN DE COMPRA", "ORDENCOMPRA")
|
||||
|
||||
@@ -15,15 +15,13 @@ def load_fa_fk_sets(
|
||||
Carga conjuntos para validación CSV Fracciones Americanas (paridad Clarion).
|
||||
Devuelve (valid_uom_codes, existing_fraction_codes).
|
||||
- valid_uom_codes: códigos de Unidad de Medida (a76.units_of_measure, code UPPER, max 5 chars).
|
||||
- existing_fraction_codes: códigos de USTariffFraction ya existentes por tenant/company.
|
||||
- existing_fraction_codes: reservado (vacío); existencia se valida contra SITAR por fila.
|
||||
"""
|
||||
valid_uom_codes: Set[str] = set()
|
||||
existing_fraction_codes: Set[str] = set()
|
||||
try:
|
||||
with CoreSessionLocal() as session:
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
|
||||
for row in (
|
||||
session.query(UnitOfMeasure.code)
|
||||
.filter(
|
||||
@@ -35,17 +33,6 @@ def load_fa_fk_sets(
|
||||
if row[0]:
|
||||
valid_uom_codes.add((row[0].strip() or "").upper()[:5])
|
||||
|
||||
for row in (
|
||||
session.query(USTariffFraction.code)
|
||||
.filter(
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.all()
|
||||
):
|
||||
if row[0]:
|
||||
existing_fraction_codes.add(row[0].strip())
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("FA import: could not load FK sets: %s", e)
|
||||
|
||||
@@ -16,6 +16,9 @@ from ..common.common_validators import (
|
||||
CODE_MAX,
|
||||
)
|
||||
from ..common.common_validators import TIPO_ADVALOREM_VALIDOS # noqa: F401 re-export
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
resolve_american_fraction_from_sitar,
|
||||
)
|
||||
|
||||
# Mensajes Clarion
|
||||
MSG_COL_A_VACIO = (
|
||||
@@ -156,7 +159,8 @@ def validate_row_us_tariff_fraction(
|
||||
|
||||
code_norm = normalize_code((row.get("FRACCION_ARANCELARIA") or "").strip())
|
||||
fraction_exists = (
|
||||
existing_fraction_codes is not None and code_norm in existing_fraction_codes
|
||||
bool(code_norm)
|
||||
and resolve_american_fraction_from_sitar(code_norm) is not None
|
||||
)
|
||||
|
||||
if actualizar and not fraction_exists:
|
||||
|
||||
@@ -138,6 +138,7 @@ class PartService:
|
||||
def create(cls, db: Session, part_data: PartCreateDTO, tenant_id: int, company_id: int) -> Part:
|
||||
# 1. Preparar datos
|
||||
data = part_data.model_dump()
|
||||
print(f"DEBUG: PartService.create - full data: {data}")
|
||||
|
||||
# Separar datos anidados
|
||||
fa_dict = data.pop('fa_data', None)
|
||||
@@ -316,6 +317,7 @@ class PartService:
|
||||
|
||||
@classmethod
|
||||
def update(cls, db: Session, part_id: int, tenant_id: int, part_data: PartUpdateDTO, company_id: int) -> Optional[Part]:
|
||||
print(f"DEBUG: PartService.update - part_id={part_id}, data={part_data.model_dump(exclude_unset=True)}")
|
||||
db_part = PartService.get_by_id(db, part_id, tenant_id, company_id)
|
||||
if not db_part:
|
||||
return None
|
||||
|
||||
@@ -277,7 +277,7 @@ class Mainx30Service:
|
||||
broker=(empresa.broker_company or "")[:5],
|
||||
responsable=(empresa.responsible or "")[:30],
|
||||
rfc=(empresa.rfc or "")[:13],
|
||||
tiene_linea_express=empresa.has_express_line or "N",
|
||||
tiene_linea_express="N",
|
||||
nombre_empresa=(empresa.name or "")[:40],
|
||||
manufacturer_id=(empresa.manufacturer_id or "")[:10],
|
||||
ftp_key=(empresa.ftp_key or "")[:10]
|
||||
|
||||
@@ -50,8 +50,9 @@ from .schemas import (
|
||||
FacturaImportacionCompleta,
|
||||
)
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
)
|
||||
|
||||
|
||||
@@ -700,21 +701,17 @@ class ConsolidadoImportacionMexService:
|
||||
current_agg = aggregated_data[agg_key]
|
||||
|
||||
if not current_agg["description"]:
|
||||
us_frac_db = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(USTariffFraction.code == us_frac_clean)
|
||||
.first()
|
||||
resolved = (
|
||||
resolve_american_fraction_from_sitar(us_frac_clean)
|
||||
if us_frac_clean
|
||||
else None
|
||||
)
|
||||
if us_frac_db:
|
||||
if resolved:
|
||||
sitar_row, _canon = resolved
|
||||
current_agg["description"] = (
|
||||
us_frac_db.description or "Sin Descripción"
|
||||
(sitar_row.DESCRIPCION or "").strip() or "Sin Descripción"
|
||||
)
|
||||
# Parse AdValorem from DB if available, else 0 ??
|
||||
# Creating logical placeholder. The provided Clarion code used `FraAme.Adv`
|
||||
adv_val = (
|
||||
us_frac_db.ad_valorem
|
||||
) # Assuming field exists based on viewing file later?
|
||||
# Wait, in us-tariff-fractions.ts I saw `ad_valorem: number | null`.
|
||||
adv_val = american_fraction_ad_valorem_from_row(sitar_row)
|
||||
current_agg["advalorem_txt"] = (
|
||||
f"{adv_val}%" if adv_val is not None else "0%"
|
||||
)
|
||||
|
||||
@@ -370,7 +370,10 @@ class ConsolidadoImportacionMexService:
|
||||
invoices_list = db.query(InvoiceHeader).filter(InvoiceHeader.id.in_(target_invoice_ids)).all()
|
||||
invoice_map = {inv.id: inv for inv in invoices_list}
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
|
||||
american_fraction_ad_valorem_from_row,
|
||||
resolve_american_fraction_from_sitar,
|
||||
)
|
||||
|
||||
for line in lines:
|
||||
qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first()
|
||||
@@ -510,14 +513,20 @@ class ConsolidadoImportacionMexService:
|
||||
current_agg = aggregated_data[agg_key]
|
||||
|
||||
if not current_agg["description"]:
|
||||
us_frac_db = db.query(USTariffFraction).filter(USTariffFraction.code == us_frac_clean).first()
|
||||
if us_frac_db:
|
||||
current_agg["description"] = us_frac_db.description or "Sin Descripción"
|
||||
# Parse AdValorem from DB if available, else 0 ??
|
||||
# Creating logical placeholder. The provided Clarion code used `FraAme.Adv`
|
||||
adv_val = us_frac_db.ad_valorem # Assuming field exists based on viewing file later?
|
||||
# Wait, in us-tariff-fractions.ts I saw `ad_valorem: number | null`.
|
||||
current_agg["advalorem_txt"] = f"{adv_val}%" if adv_val is not None else "0%"
|
||||
resolved = (
|
||||
resolve_american_fraction_from_sitar(us_frac_clean)
|
||||
if us_frac_clean
|
||||
else None
|
||||
)
|
||||
if resolved:
|
||||
sitar_row, _canon = resolved
|
||||
current_agg["description"] = (
|
||||
(sitar_row.DESCRIPCION or "").strip() or "Sin Descripción"
|
||||
)
|
||||
adv_val = american_fraction_ad_valorem_from_row(sitar_row)
|
||||
current_agg["advalorem_txt"] = (
|
||||
f"{adv_val}%" if adv_val is not None else "0%"
|
||||
)
|
||||
else:
|
||||
current_agg["description"] = part_master.description_spanish if part_master else "S/D"
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ class Mainx30DefinitiveService:
|
||||
broker=(empresa.broker_company or "")[:5],
|
||||
responsable=(empresa.responsible or "")[:30],
|
||||
rfc=(empresa.rfc or "")[:13],
|
||||
tiene_linea_express=empresa.has_express_line or "N",
|
||||
tiene_linea_express="N",
|
||||
nombre_empresa=(empresa.name or "")[:40],
|
||||
manufacturer_id=(empresa.manufacturer_id or "")[:10],
|
||||
ftp_key=(empresa.ftp_key or "")[:10],
|
||||
|
||||
@@ -168,7 +168,7 @@ class Mainx30Service:
|
||||
broker=(empresa.broker_company or "")[:5],
|
||||
responsable=(empresa.responsible or "")[:30],
|
||||
rfc=(empresa.rfc or "")[:13],
|
||||
tiene_linea_express=empresa.has_express_line or "N",
|
||||
tiene_linea_express="N",
|
||||
nombre_empresa=(empresa.name or "")[:40],
|
||||
manufacturer_id=(empresa.manufacturer_id or "")[:10],
|
||||
ftp_key=(empresa.ftp_key or "")[:10],
|
||||
|
||||
@@ -24,6 +24,7 @@ class TransporterBaseDTO(BaseModel):
|
||||
ftp_password: Optional[str] = None
|
||||
ftp_directory: Optional[str] = None
|
||||
filler_code: Optional[str] = None
|
||||
has_express_line: Optional[bool] = False
|
||||
|
||||
|
||||
class TransporterCreateDTO(TransporterBaseDTO):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import BigInteger, Column, ForeignKeyConstraint, String
|
||||
from sqlalchemy import BigInteger, Boolean, Column, ForeignKeyConstraint, String
|
||||
|
||||
|
||||
class Transporter(Base, TenantScopedMixin, TimestampMixin):
|
||||
@@ -30,3 +30,4 @@ class Transporter(Base, TenantScopedMixin, TimestampMixin):
|
||||
ftp_password = Column(String(100), nullable=True)
|
||||
ftp_directory = Column(String(1000), nullable=True)
|
||||
filler_code = Column(String(20), nullable=True)
|
||||
has_express_line = Column(Boolean, nullable=False, server_default="false")
|
||||
|
||||
@@ -159,6 +159,7 @@ class TransporterService:
|
||||
"ftp_password": transporter.ftp_password,
|
||||
"ftp_directory": transporter.ftp_directory,
|
||||
"filler_code": transporter.filler_code,
|
||||
"has_express_line": transporter.has_express_line,
|
||||
}
|
||||
merged.update(update_data)
|
||||
validate_transporter_row_for_api(
|
||||
|
||||
@@ -116,3 +116,47 @@ class SitarAPIBaseService:
|
||||
pass
|
||||
|
||||
return response.json()
|
||||
|
||||
def _get_token_sync(self) -> str:
|
||||
"""Same token cache as async path; safe for sync validators (no running asyncio loop)."""
|
||||
if self._token and self._token_expires and datetime.now() < self._token_expires:
|
||||
return self._token
|
||||
|
||||
login_url = f"{self.base_url}/fractions/api/v1/auth/login"
|
||||
payload = {"username": self.username, "password": self.password}
|
||||
|
||||
with httpx.Client(timeout=self.timeout) as client:
|
||||
response = client.post(login_url, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
token = data.get("access_token") or data.get("token")
|
||||
if not token:
|
||||
raise ValueError("No token received from SITAR API")
|
||||
self._token = token
|
||||
self._token_expires = datetime.now() + timedelta(hours=1)
|
||||
return token
|
||||
|
||||
def _make_request_sync(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
) -> Any:
|
||||
token = self._get_token_sync()
|
||||
endpoint = endpoint.lstrip("/")
|
||||
url = f"{self.base_url}/fractions/{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
with httpx.Client(timeout=self.timeout) as client:
|
||||
response = client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
json=json_data,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
124
backend/api/v1/modules/sitar/fracciones_usa/catalog_resolve.py
Normal file
124
backend/api/v1/modules/sitar/fracciones_usa/catalog_resolve.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Resolve American (US) tariff fraction codes against SITAR fracciones-usa.
|
||||
Used by sync validators and CSV/layout enrichment (no asyncio.run).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from .schemas import FraccionesUSAResponse
|
||||
from .service import FraccionesUSAService
|
||||
|
||||
|
||||
def normalize_american_fraction_code_candidates(raw_code: str) -> List[str]:
|
||||
"""
|
||||
Map user input to candidate strings to query SITAR (dotted HTS vs digits-only).
|
||||
Mirrors logic in items imports/validators/common.py.
|
||||
"""
|
||||
normalized_raw = (raw_code or "").strip()
|
||||
if not normalized_raw:
|
||||
return []
|
||||
|
||||
digits_only = re.sub(r"[.\s\-]", "", normalized_raw)
|
||||
|
||||
candidates: List[str] = []
|
||||
candidates.append(normalized_raw)
|
||||
|
||||
if len(digits_only) == 10:
|
||||
candidates.append(
|
||||
f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}.{digits_only[8:10]}"
|
||||
)
|
||||
elif len(digits_only) == 8:
|
||||
candidates.append(
|
||||
f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}"
|
||||
)
|
||||
|
||||
candidates.append(digits_only)
|
||||
|
||||
seen: set[str] = set()
|
||||
deduped: List[str] = []
|
||||
for c in candidates:
|
||||
if not c or c in seen:
|
||||
continue
|
||||
seen.add(c)
|
||||
deduped.append(c)
|
||||
return deduped
|
||||
|
||||
|
||||
def _digits(code: str) -> str:
|
||||
return re.sub(r"[.\s\-]", "", code or "")
|
||||
|
||||
|
||||
def canonical_code_from_sitar_row(item: FraccionesUSAResponse) -> str:
|
||||
"""Same precedence as USTariffFractionMapper.to_domain for stored line value."""
|
||||
return (
|
||||
(item.FRACCION_CON_PUNTO or item.FRACCION_MOSTRAR or item.FRACCION_SIN_PUNTO or "")
|
||||
.strip()
|
||||
)
|
||||
|
||||
|
||||
def resolve_american_fraction_from_sitar(
|
||||
raw_code: str,
|
||||
) -> Optional[Tuple[FraccionesUSAResponse, str]]:
|
||||
"""
|
||||
Returns (SITAR row, canonical_code) if found, else None.
|
||||
canonical_code is suitable for persisting on line customs / exports.
|
||||
"""
|
||||
if not (raw_code or "").strip():
|
||||
return None
|
||||
|
||||
target_digits = _digits(raw_code)
|
||||
if not target_digits:
|
||||
return None
|
||||
|
||||
candidates = normalize_american_fraction_code_candidates(raw_code)
|
||||
tried_searches: set[tuple[Optional[str], Optional[str]]] = set()
|
||||
|
||||
for cand in candidates:
|
||||
clean = cand.replace(".", "").replace("-", "").replace(" ", "")
|
||||
if clean.isdigit() and len(clean) >= 4:
|
||||
search_frac, search_desc = cand, None
|
||||
else:
|
||||
search_frac, search_desc = None, cand
|
||||
|
||||
key = (search_frac, search_desc)
|
||||
if key in tried_searches:
|
||||
continue
|
||||
tried_searches.add(key)
|
||||
|
||||
rows = FraccionesUSAService.search_sync(
|
||||
fraccion=search_frac,
|
||||
descripcion=search_desc,
|
||||
skip=0,
|
||||
limit=100,
|
||||
)
|
||||
for row in rows:
|
||||
canon = canonical_code_from_sitar_row(row)
|
||||
if _digits(canon) == target_digits:
|
||||
return (row, canon)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
MAX_AMERICAN_FRACTION_DB_LEN = 16
|
||||
|
||||
|
||||
def store_canonical_american_code(canon: str) -> str:
|
||||
"""Fit SITAR canonical HTS string into DB column (VARCHAR 16)."""
|
||||
s = (canon or "").strip()
|
||||
if len(s) <= MAX_AMERICAN_FRACTION_DB_LEN:
|
||||
return s
|
||||
digits = re.sub(r"[.\s\-]", "", s)
|
||||
return digits[:MAX_AMERICAN_FRACTION_DB_LEN]
|
||||
|
||||
|
||||
def american_fraction_ad_valorem_from_row(item: FraccionesUSAResponse) -> Optional[float]:
|
||||
"""Parse TARIFA1 like USTariffFractionMapper."""
|
||||
if not item.TARIFA1:
|
||||
return None
|
||||
try:
|
||||
return float(str(item.TARIFA1).replace("%", "").strip())
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
@@ -1,9 +1,13 @@
|
||||
"""Fracciones USA Service"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import FraccionesUSAResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class FraccionesUSAService(SitarAPIBaseService):
|
||||
@@ -39,3 +43,35 @@ class FraccionesUSAService(SitarAPIBaseService):
|
||||
"""Get single USA Fraccion record by CONSECUTIVO"""
|
||||
data = await self._make_request("GET", f"api/v1/fracciones-usa/{consecutivo}")
|
||||
return FraccionesUSAResponse(**data)
|
||||
|
||||
@classmethod
|
||||
def search_sync(
|
||||
cls,
|
||||
fraccion: Optional[str] = None,
|
||||
descripcion: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[FraccionesUSAResponse]:
|
||||
"""
|
||||
Synchronous SITAR search for use from sync validators (FastAPI async routes
|
||||
run sync code on the event loop; asyncio.run must not be used there).
|
||||
"""
|
||||
try:
|
||||
service = cls.get_instance()
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
params: dict = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if descripcion:
|
||||
params["descripcion"] = descripcion
|
||||
|
||||
try:
|
||||
data = service._make_request_sync("GET", "api/v1/fracciones-usa/", params=params)
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return [FraccionesUSAResponse(**item) for item in data]
|
||||
except Exception as exc:
|
||||
logger.warning("SITAR fracciones-usa search_sync failed: %s", exc)
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user