diff --git a/backend/alembic/versions/c8d9e0f1a2b3_transporter_has_express_line.py b/backend/alembic/versions/c8d9e0f1a2b3_transporter_has_express_line.py new file mode 100644 index 00000000..2d424d31 --- /dev/null +++ b/backend/alembic/versions/c8d9e0f1a2b3_transporter_has_express_line.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/company/dto.py b/backend/api/v1/modules/a76/general_catalogs/company/dto.py index d9153034..97421872 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/dto.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/company/models.py b/backend/api/v1/modules/a76/general_catalogs/company/models.py index ef914264..90c41010 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/models.py @@ -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)) diff --git a/backend/api/v1/modules/a76/general_catalogs/company/service.py b/backend/api/v1/modules/a76/general_catalogs/company/service.py index 1432a7be..962292d8 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/service.py @@ -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", diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py index 21277c99..f46df469 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py @@ -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).", + ) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py index 380cf508..a54f8cf7 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py @@ -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: diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index d228d085..5bb5cb44 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/exports/validators/common.py b/backend/api/v1/modules/a76/items/exports/validators/common.py index d8e204f5..4acd302d 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/common.py +++ b/backend/api/v1/modules/a76/items/exports/validators/common.py @@ -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: diff --git a/backend/api/v1/modules/a76/items/exports/validators/create.py b/backend/api/v1/modules/a76/items/exports/validators/create.py index 4c189ee0..1aaaf83f 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/create.py +++ b/backend/api/v1/modules/a76/items/exports/validators/create.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/exports/validators/update.py b/backend/api/v1/modules/a76/items/exports/validators/update.py index 758c71de..2d3e13e9 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/update.py +++ b/backend/api/v1/modules/a76/items/exports/validators/update.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/imports/validators/common.py b/backend/api/v1/modules/a76/items/imports/validators/common.py index 7104b545..3ab71464 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/common.py +++ b/backend/api/v1/modules/a76/items/imports/validators/common.py @@ -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: diff --git a/backend/api/v1/modules/a76/items/imports/validators/create.py b/backend/api/v1/modules/a76/items/imports/validators/create.py index 4a5f3958..daa3d52a 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/create.py +++ b/backend/api/v1/modules/a76/items/imports/validators/create.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/imports/validators/update.py b/backend/api/v1/modules/a76/items/imports/validators/update.py index ad18716d..3f891e0a 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/update.py +++ b/backend/api/v1/modules/a76/items/imports/validators/update.py @@ -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 diff --git a/backend/api/v1/modules/a76/layouts_csv/classes/common/fk_loader.py b/backend/api/v1/modules/a76/layouts_csv/classes/common/fk_loader.py index 95f331d6..3d3b401c 100644 --- a/backend/api/v1/modules/a76/layouts_csv/classes/common/fk_loader.py +++ b/backend/api/v1/modules/a76/layouts_csv/classes/common/fk_loader.py @@ -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 diff --git a/backend/api/v1/modules/a76/layouts_csv/classes/validators/common.py b/backend/api/v1/modules/a76/layouts_csv/classes/validators/common.py index fad8ded8..a5ea6b7e 100644 --- a/backend/api/v1/modules/a76/layouts_csv/classes/validators/common.py +++ b/backend/api/v1/modules/a76/layouts_csv/classes/validators/common.py @@ -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)." ), } diff --git a/backend/api/v1/modules/a76/layouts_csv/facturas/line_item_enrichment/import_enrichment.py b/backend/api/v1/modules/a76/layouts_csv/facturas/line_item_enrichment/import_enrichment.py index afbf83c2..9ce52568 100644 --- a/backend/api/v1/modules/a76/layouts_csv/facturas/line_item_enrichment/import_enrichment.py +++ b/backend/api/v1/modules/a76/layouts_csv/facturas/line_item_enrichment/import_enrichment.py @@ -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: diff --git a/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py b/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py index 0c279146..2bf46101 100644 --- a/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py +++ b/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py @@ -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( diff --git a/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_expo.py b/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_expo.py index dea9652e..f4096474 100644 --- a/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_expo.py +++ b/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_expo.py @@ -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") diff --git a/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_impo_temp.py b/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_impo_temp.py index 54e2ebe4..f69350f4 100644 --- a/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_impo_temp.py +++ b/backend/api/v1/modules/a76/layouts_csv/facturas/validators/partidas_impo_temp.py @@ -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") diff --git a/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/common/fk_loader.py b/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/common/fk_loader.py index e2da98a4..10ffa10b 100644 --- a/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/common/fk_loader.py +++ b/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/common/fk_loader.py @@ -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) diff --git a/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/validators/common.py b/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/validators/common.py index 9b72342f..d9e071b3 100644 --- a/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/validators/common.py +++ b/backend/api/v1/modules/a76/layouts_csv/us_tariff_fractions/validators/common.py @@ -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: diff --git a/backend/api/v1/modules/a76/reports/exportacion/transmission/MAINX30/service.py b/backend/api/v1/modules/a76/reports/exportacion/transmission/MAINX30/service.py index ffe01710..e48363fe 100644 --- a/backend/api/v1/modules/a76/reports/exportacion/transmission/MAINX30/service.py +++ b/backend/api/v1/modules/a76/reports/exportacion/transmission/MAINX30/service.py @@ -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] diff --git a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py index c90b81b1..28e17498 100644 --- a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py @@ -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%" ) diff --git a/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py index d942d1ce..6aa530ae 100644 --- a/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py @@ -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" diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py index 634058ee..881406b3 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py @@ -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], diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py b/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py index 78dadbc6..276182bd 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py @@ -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], diff --git a/backend/api/v1/modules/a76/transportation/transporters/dto.py b/backend/api/v1/modules/a76/transportation/transporters/dto.py index c4397d00..969fed6e 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/dto.py +++ b/backend/api/v1/modules/a76/transportation/transporters/dto.py @@ -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): diff --git a/backend/api/v1/modules/a76/transportation/transporters/models.py b/backend/api/v1/modules/a76/transportation/transporters/models.py index 58d03367..ec50d643 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/models.py +++ b/backend/api/v1/modules/a76/transportation/transporters/models.py @@ -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") diff --git a/backend/api/v1/modules/a76/transportation/transporters/services.py b/backend/api/v1/modules/a76/transportation/transporters/services.py index 2654677e..8f40888d 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/services.py +++ b/backend/api/v1/modules/a76/transportation/transporters/services.py @@ -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( diff --git a/backend/api/v1/modules/sitar/common/base_service.py b/backend/api/v1/modules/sitar/common/base_service.py index e58d62b3..a8e1f077 100644 --- a/backend/api/v1/modules/sitar/common/base_service.py +++ b/backend/api/v1/modules/sitar/common/base_service.py @@ -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() diff --git a/backend/api/v1/modules/sitar/fracciones_usa/catalog_resolve.py b/backend/api/v1/modules/sitar/fracciones_usa/catalog_resolve.py new file mode 100644 index 00000000..087ba1c5 --- /dev/null +++ b/backend/api/v1/modules/sitar/fracciones_usa/catalog_resolve.py @@ -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 diff --git a/backend/api/v1/modules/sitar/fracciones_usa/service.py b/backend/api/v1/modules/sitar/fracciones_usa/service.py index b1f739df..fe4b513c 100644 --- a/backend/api/v1/modules/sitar/fracciones_usa/service.py +++ b/backend/api/v1/modules/sitar/fracciones_usa/service.py @@ -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 [] diff --git a/frontend/e2e/setup-catalogs.spec.ts b/frontend/e2e/setup-catalogs.spec.ts index cc1f14a6..c08e4a90 100644 --- a/frontend/e2e/setup-catalogs.spec.ts +++ b/frontend/e2e/setup-catalogs.spec.ts @@ -15,7 +15,8 @@ function saveCatalog(data: Record) { // 8471.30.01 es una fraccion comun para equipos de computo — existe en SITAR const CLASS_CODE = 'E2E01' const CLASS_FRACTION = '12787' -const CLASS_US_FRACTION = '12787' +// HTS-style code resolvable via SITAR fracciones-usa (8 or 10 digit patterns used in UI) +const CLASS_US_FRACTION = '8471300100' const CLASS_UM = 'KG' const CLASS_MATERIAL_KEY = 'EAGRI' const CLASS_DESC_ES = 'Clase E2E Test' diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 31e47ba2..57197776 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -805,24 +805,6 @@ export const api = { fetchBlob(`/v1/a76/exchange-rate/imports/${jobId}/errors/scan-csv`) }, - // CSV import for Fracción Americana (us_tariff_fractions/imports) - americanFractionImports: { - upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => { - const formData = new FormData(); - formData.append('file', file); - return fetchApiFormDataPost( - `/v1/a76/us-tariff-fractions/imports/upload?company_id=${companyId}`, - formData, - uploadOptions - ); - }, - status: (jobId: string) => api.get(`/v1/a76/us-tariff-fractions/imports/${jobId}/status`), - commit: (jobId: string) => - api.post(`/v1/a76/us-tariff-fractions/imports/${jobId}/commit`, {}), - downloadScanErrorsCsv: (jobId: string) => - fetchBlob(`/v1/a76/us-tariff-fractions/imports/${jobId}/errors/scan-csv`) - }, - // CSV import for Pedimentos (pedimentos/imports) pedimentosImports: { upload: ( diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts index 73228d75..7934463e 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts @@ -107,7 +107,6 @@ export interface Company { responsible_mother_last_name: string | null; responsible_rfc?: string | null; position?: string | null; - has_express_line?: boolean; fiscal_deposit?: boolean; generate_barcodes_with_fiel?: boolean; is_service_company?: boolean; @@ -273,7 +272,6 @@ export interface CompanyCreate { responsible_mother_last_name?: string | null; responsible_rfc?: string | null; position?: string | null; - has_express_line?: boolean; fiscal_deposit?: boolean; generate_barcodes_with_fiel?: boolean; is_service_company?: boolean; diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/us-tariff-fractions.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/us-tariff-fractions.ts index e2f5e1e2..d2cfac3c 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/us-tariff-fractions.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/us-tariff-fractions.ts @@ -1,37 +1,19 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; -// Interfaces +/** List item shape returned by GET /v1/a76/us-tariff-fractions/ (SITAR-backed, read-only). */ export interface USTariffFraction { id: number; code: string; - prefix: string | null; - type_code: string | null; - ad_valorem: number | null; - fixed_cost: number | null; - unit_of_measure: string | null; - description: string | null; - created_at: string | null; - updated_at: string | null; -} - -export interface USTariffFractionCreate { - code: string; - prefix?: string | null; - type_code?: string | null; - ad_valorem?: number | null; - fixed_cost?: number | null; - unit_of_measure?: string | null; - description?: string | null; -} - -export interface USTariffFractionUpdate { + fraction?: string | null; prefix?: string | null; type_code?: string | null; ad_valorem?: number | null; fixed_cost?: number | null; unit_of_measure?: string | null; description?: string | null; + created_at?: string; + updated_at?: string; } export interface USTariffFractionListResponse { @@ -42,47 +24,20 @@ export interface USTariffFractionListResponse { pages: number; } -// API Functions +/** Paginated list from SITAR (same data as tariff-fractions?catalog=american). */ export async function getUSTariffFractions( page = 1, pageSize = 50, companyId: number, - filters: Record = {} + filters: Record = {} ): Promise> { const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), company_id: companyId.toString(), - ...filters + ...Object.fromEntries( + Object.entries(filters).map(([k, v]) => [k, String(v)]) + ) }); return await api.get(`/v1/a76/us-tariff-fractions/?${queryParams.toString()}`); } - -export async function getUSTariffFractionById( - id: number, - companyId: number -): Promise> { - return await api.get(`/v1/a76/us-tariff-fractions/${id}/?company_id=${companyId}`); -} - -export async function createUSTariffFraction( - data: USTariffFractionCreate, - companyId: number -): Promise> { - return await api.post(`/v1/a76/us-tariff-fractions/?company_id=${companyId}`, data); -} - -export async function updateUSTariffFraction( - id: number, - data: USTariffFractionUpdate, - companyId: number -): Promise> { - return await api.put(`/v1/a76/us-tariff-fractions/${id}/?company_id=${companyId}`, data); -} - -export async function deleteUSTariffFraction( - id: number, - companyId: number -): Promise> { - return await api.delete(`/v1/a76/us-tariff-fractions/${id}/?company_id=${companyId}`); -} diff --git a/frontend/src/lib/api/dashboard/a76/transporters.ts b/frontend/src/lib/api/dashboard/a76/transporters.ts index da855ae0..0ff1084f 100644 --- a/frontend/src/lib/api/dashboard/a76/transporters.ts +++ b/frontend/src/lib/api/dashboard/a76/transporters.ts @@ -21,6 +21,7 @@ export interface Transporter { ftp_password?: string; ftp_directory?: string; filler_code?: string; + has_express_line?: boolean; company_id?: number | string; tenant_id?: number | string; } diff --git a/frontend/src/lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte b/frontend/src/lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte index 5ffb0f1a..c9d03b91 100644 --- a/frontend/src/lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte +++ b/frontend/src/lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte @@ -40,7 +40,6 @@ customs_brokers: 'Agentes aduanales', clients_providers: 'Clientes / proveedores', exchange_rates: 'Tipos de cambio', - american_fractions: 'Fracciones arancelarias US', pedimentos: 'Pedimentos', material_classes: 'Clases de material', vehicles: 'Vehículos', diff --git a/frontend/src/lib/components/dashboard/csv-upload/ProcessingResultModal.svelte b/frontend/src/lib/components/dashboard/csv-upload/ProcessingResultModal.svelte index dd342907..b15acaeb 100644 --- a/frontend/src/lib/components/dashboard/csv-upload/ProcessingResultModal.svelte +++ b/frontend/src/lib/components/dashboard/csv-upload/ProcessingResultModal.svelte @@ -37,7 +37,6 @@ | 'customs_brokers' | 'clients_providers' | 'exchange_rate' - | 'us_tariff_fractions' | 'pedimentos' | 'classes' | 'vehicles' @@ -139,9 +138,6 @@ case 'exchange_rate': blob = await api.exchangeRateImports.downloadScanErrorsCsv(jobId); break; - case 'us_tariff_fractions': - blob = await api.americanFractionImports.downloadScanErrorsCsv(jobId); - break; case 'pedimentos': blob = await api.pedimentosImports.downloadScanErrorsCsv(jobId); break; diff --git a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte index 2cc43f4a..933f4680 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte @@ -6,6 +6,7 @@ import * as Tabs from "$lib/components/ui/tabs"; import { Switch } from "$lib/components/ui/switch"; import { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/general_catalogs/company"; + import { companyStore } from "$lib/stores/company.svelte"; let { open = $bindable(false), @@ -43,7 +44,6 @@ // Configuración / Operativo manufacturer_id: '', - has_express_line: false, is_service_company: false, order_format_type: '', @@ -76,7 +76,6 @@ position: item.position || '', manufacturer_id: item.manufacturer_id || '', - has_express_line: item.has_express_line || false, is_service_company: item.is_service_company || false, order_format_type: item.order_format_type || '', @@ -89,7 +88,7 @@ name: '', rfc: '', curp: '', main_activity: '', program: '', program_number: '', prosec: 0, prosec_authorization: '', responsible_name: '', responsible_last_name: '', responsible_mother_last_name: '', responsible_rfc: '', position: '', - manufacturer_id: '', has_express_line: false, is_service_company: false, order_format_type: '', + manufacturer_id: '', is_service_company: false, order_format_type: '', ctpat_svi: '', trusted_exporter_number: '' }; } @@ -124,7 +123,6 @@ position: formData.position.trim() || null, manufacturer_id: formData.manufacturer_id.trim() || null, - has_express_line: formData.has_express_line, is_service_company: formData.is_service_company, order_format_type: formData.order_format_type.trim() || null, @@ -143,6 +141,7 @@ throw new Error(response.error); } + await companyStore.loadCompanies(); open = false; if (onSuccess) onSuccess(); } catch (e) { @@ -249,10 +248,6 @@
-
- - -
diff --git a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte index adbf03e1..6c16bed8 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte @@ -14,10 +14,6 @@ getTariffFractions, type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions'; - import { - getUSTariffFractions, - type USTariffFraction - } from '$lib/api/dashboard/a76/general_catalogs/us-tariff-fractions'; import { getDepreciationCatalog, type DepreciationCatalog @@ -203,7 +199,7 @@ formData.import_tariff_type = (fraction.umt ?? '') as string; showFractionDialog = false; } - let usTariffFractions = $state([]); + let usTariffFractions = $state([]); let searchUSFraction = $state(''); let currentUSPage = $state(1); let totalUSFractions = $state(0); @@ -392,10 +388,11 @@ isLoadingUSFractions = true; try { - const filters = search ? { search } : {}; + const filters: Record = { catalog: 'american' }; + if (search?.trim()) filters.search = search.trim(); const pageSize = 100; - const response = await getUSTariffFractions(page, pageSize, companyId, filters); + const response = await getTariffFractions(page, pageSize, companyId, filters); if (response.data) { if (page === 1) { @@ -415,10 +412,16 @@ } } - function selectUSFraction(fraction: USTariffFraction) { - formData.us_fraction = fraction.code; - formData.us_fraction_ad_valorem = fraction.ad_valorem?.toString() || '0.00'; - formData.us_fraction_fixed_rate = fraction.fixed_cost?.toString() || '0.00000000'; + function selectUSFraction(fraction: TariffFraction) { + formData.us_fraction = fraction.code || ''; + const advRaw = fraction.adv_impo; + let advNum = 0; + if (advRaw != null && String(advRaw).trim() !== '') { + const n = parseFloat(String(advRaw).replace('%', '').trim()); + if (!Number.isNaN(n)) advNum = n; + } + formData.us_fraction_ad_valorem = advNum ? String(advNum) : '0.00'; + formData.us_fraction_fixed_rate = '0.00000000'; // Actualizar tarifa de exportación formData.export_tariff_code = fraction.code || ''; formData.export_tariff_type = 'HTS'; // Harmonized Tariff Schedule @@ -1041,10 +1044,10 @@ class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700" onclick={() => selectUSFraction(fraction)} > - {fraction.code} - {fraction.prefix || ''} - {fraction.ad_valorem || '0.00'} - {fraction.fixed_cost || '0.00'} + {fraction.fraction || fraction.code} + {fraction.code || '—'} + {fraction.adv_impo ?? '—'} + {fraction.adv_expo ?? '—'} {fraction.description || ''} {:else} diff --git a/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte b/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte index 00ea7d37..99fd1223 100644 --- a/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte @@ -5,7 +5,10 @@ import * as Table from "$lib/components/ui/table"; import { toast } from "svelte-sonner"; import { Search, Loader2, Hash } from "lucide-svelte"; - import { getUSTariffFractions, type USTariffFraction } from "$lib/api/dashboard/a76/general_catalogs/us-tariff-fractions"; + import { + getTariffFractions, + type TariffFraction + } from "$lib/api/dashboard/a76/general_catalogs/tariff-fractions"; import { companyStore } from "$lib/stores/company.svelte"; import { m } from '$lib/i18n/messages'; @@ -15,11 +18,11 @@ onSelect }: { open: boolean, - onSelect: (item: USTariffFraction) => void + onSelect: (item: TariffFraction) => void } = $props(); // --- ESTADO --- - let items = $state([]); + let items = $state([]); let loading = $state(false); let searchTerm = $state(""); let loadedForCompanyId = $state(null); @@ -30,8 +33,8 @@ return (code || '').replace(/[.\s-]/g, ''); } - function isEligibleAmericanFraction(item: USTariffFraction) { - const normalizedCode = normalizeAmericanFractionCode(item.code || ''); + function isEligibleAmericanFraction(item: TariffFraction) { + const normalizedCode = normalizeAmericanFractionCode(item.code || item.fraction || ''); return /^\d{8}$/.test(normalizedCode) || /^\d{10}$/.test(normalizedCode); } @@ -70,7 +73,9 @@ loading = true; try { - const response = await getUSTariffFractions(1, 1000, companyId); + const response = await getTariffFractions(1, 1000, companyId, { + catalog: 'american' + }); if (response.error) { console.error("Error al cargar fracciones americanas:", response.error); @@ -97,7 +102,7 @@ } } - function handleSelect(item: USTariffFraction) { + function handleSelect(item: TariffFraction) { if (onSelect) onSelect(item); open = false; } @@ -150,7 +155,7 @@
- {item.code} + {item.fraction || item.code}
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte index 51111592..b06a6a37 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -8,6 +8,7 @@ import type { Invoice } from '$lib/api/dashboard/a76/invoices'; import PackageDialog from './package-dialog.svelte'; import USFractionSelectorDialog from '$lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte'; + import type { TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions'; import { m } from '$lib/i18n/messages'; let { @@ -118,11 +119,12 @@ quantities.package_description = pkg.description_es || pkg.description_en || pkg.key; } - function handleAmericanFractionSelect(fraction: any) { + function handleAmericanFractionSelect(fraction: TariffFraction) { customs.american_fraction = fraction.code || ''; (customs as any).american_fraction_description = fraction.description || ''; - if (fraction.ad_valorem !== null && fraction.ad_valorem !== undefined) { - customs.advalorem_american = fraction.ad_valorem; + if (fraction.adv_impo != null && String(fraction.adv_impo).trim() !== '') { + const n = parseFloat(String(fraction.adv_impo).replace('%', '').trim()); + if (!Number.isNaN(n)) customs.advalorem_american = n; } } diff --git a/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte index 81d953b1..6d05d2ba 100644 --- a/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte @@ -3,6 +3,7 @@ import * as Dialog from '$lib/components/ui/dialog'; import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; + import { Switch } from '$lib/components/ui/switch'; import * as Select from '$lib/components/ui/select'; import { transportersApi, type Transporter } from '$lib/api/dashboard/a76/transporters'; import { countriesApi, type Country } from '$lib/api/dashboard/reference_data/countries'; @@ -45,7 +46,8 @@ ftp_user: '', ftp_password: '', ftp_directory: '', - filler_code: '' + filler_code: '', + has_express_line: false }); let loading = $state(false); @@ -99,7 +101,8 @@ ftp_user: '', ftp_password: '', ftp_directory: '', - filler_code: '' + filler_code: '', + has_express_line: false }; } @@ -110,7 +113,7 @@ return; } if (item) { - formData = { ...item }; + formData = { ...item, has_express_line: item.has_express_line ?? false }; } else { formData = emptyTransporterForm(); } @@ -284,6 +287,11 @@
+ +
+ + +
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index c1b7f0bf..56663f4b 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -318,10 +318,6 @@ export function getSidebarData(): SidebarData { title: m["sidebar.fractions.sitar_us"](), url: "/dashboard/general_catalogs/tariff-fractions/us", }, - { - title: m["sidebar.fractions.american"](), - url: "/dashboard/general_catalogs/tariff-fractions/american", - }, { title: m["sidebar.fractions.canadian"](), url: "/dashboard/general_catalogs/tariff-fractions/canadian", diff --git a/frontend/src/lib/config/csv-upload.ts b/frontend/src/lib/config/csv-upload.ts index 3cc39412..6d1915b0 100644 --- a/frontend/src/lib/config/csv-upload.ts +++ b/frontend/src/lib/config/csv-upload.ts @@ -8,7 +8,6 @@ import { Plane, Package, Briefcase, - Globe, CreditCard, DollarSign, Calendar, @@ -227,14 +226,6 @@ export const catalogosConfig: CsvUploadItem[] = [ templateId: 'exchange_rates', layoutModule: 'layouts_csv/exchange_rate' }, - { - id: 'american_fractions', - title: 'Fracc. Ame.', - icon: Globe, - modelTarget: 'AmericanFraction', - templateId: 'american_fractions', - layoutModule: 'layouts_csv/us_tariff_fractions' - }, { id: 'material_classes', title: 'Clases de Materiales', diff --git a/frontend/src/lib/csv-import-pending.ts b/frontend/src/lib/csv-import-pending.ts index fee48340..eb81fd54 100644 --- a/frontend/src/lib/csv-import-pending.ts +++ b/frontend/src/lib/csv-import-pending.ts @@ -5,7 +5,6 @@ const VALID_PROFILES: CsvImportProfile[] = [ 'customs_brokers', 'clients_providers', 'exchange_rates', - 'american_fractions', 'pedimentos', 'material_classes', 'vehicles', diff --git a/frontend/src/lib/csv-import-session.ts b/frontend/src/lib/csv-import-session.ts index b895ca85..27f7a636 100644 --- a/frontend/src/lib/csv-import-session.ts +++ b/frontend/src/lib/csv-import-session.ts @@ -9,7 +9,6 @@ export type CsvImportProfile = | 'customs_brokers' | 'clients_providers' | 'exchange_rates' - | 'american_fractions' | 'pedimentos' | 'material_classes' | 'vehicles' @@ -88,7 +87,6 @@ function isCsvImportProfile(p: unknown): p is CsvImportProfile { 'customs_brokers', 'clients_providers', 'exchange_rates', - 'american_fractions', 'pedimentos', 'material_classes', 'vehicles', diff --git a/frontend/src/lib/csv-import-status-api.ts b/frontend/src/lib/csv-import-status-api.ts index 6dd46f68..ab0ef205 100644 --- a/frontend/src/lib/csv-import-status-api.ts +++ b/frontend/src/lib/csv-import-status-api.ts @@ -15,8 +15,6 @@ export function fetchCsvImportStatus( return api.clientProviderImports.status(jobId); case 'exchange_rates': return api.exchangeRateImports.status(jobId); - case 'american_fractions': - return api.americanFractionImports.status(jobId); case 'pedimentos': return api.pedimentosImports.status(jobId); case 'material_classes': diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 4294fe73..890f8870 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -1,6 +1,6 @@ - - diff --git a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/american/+page.ts b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/american/+page.ts new file mode 100644 index 00000000..b2bd1df9 --- /dev/null +++ b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/american/+page.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +/** Legacy URL; catalog is unified under SITAR US listing. */ +export const load: PageLoad = () => { + throw redirect(308, '/dashboard/general_catalogs/tariff-fractions/us'); +};