fix/carga-csv-ergonomia
This commit is contained in:
@@ -4,11 +4,10 @@ Construido a partir de los TEMPLATE_COLUMNS de cada módulo de imports.
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
# Importar configs de cada módulo
|
||||
from api.v1.modules.a76.layouts_csv.facturas.template_config import (
|
||||
TEMPLATE_COLUMNS as IMPORTS_TEMPLATE_COLUMNS,
|
||||
_resolve_template_columns as resolve_imports_template,
|
||||
)
|
||||
from api.v1.modules.a76.layouts_csv.parts.template_config import (
|
||||
@@ -140,6 +139,7 @@ ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE: Dict[str, set[str]] = {
|
||||
"imp_temp_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||
"imp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||
"cmex_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||
"exp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||
# Catálogos y transportes
|
||||
"customs_brokers": {"TIPO", "CLAVE", "NOMBRE"},
|
||||
"clients_providers": {"PROCEDENCIA", "SHORT_NAME", "NOMBRE", "RFC"},
|
||||
@@ -191,23 +191,35 @@ ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE: Dict[str, set[str]] = {
|
||||
}
|
||||
|
||||
|
||||
def _apply_required_prefix(template_id: str, headers: List[str]) -> List[str]:
|
||||
"""Prefija '* ' a cabeceras siempre obligatorias para la plantilla."""
|
||||
required_headers = ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE.get(template_id)
|
||||
if not required_headers:
|
||||
return headers
|
||||
def _compute_required_indices_map(base_rows: Dict[str, List[str]]) -> Dict[str, Set[int]]:
|
||||
"""Índices de columnas obligatorias (según plantilla ES/base), para marcar * en cualquier idioma."""
|
||||
out: Dict[str, Set[int]] = {}
|
||||
for tid, headers in base_rows.items():
|
||||
req = ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE.get(tid)
|
||||
if not req:
|
||||
continue
|
||||
req_norm = {_normalize_header_for_match(h) for h in req}
|
||||
idx_set: Set[int] = set()
|
||||
for i, h in enumerate(headers):
|
||||
if not h:
|
||||
continue
|
||||
if _normalize_header_for_match(h) in req_norm:
|
||||
idx_set.add(i)
|
||||
out[tid] = idx_set
|
||||
return out
|
||||
|
||||
required_norm = {_normalize_header_for_match(h) for h in required_headers}
|
||||
|
||||
def _apply_required_prefix_indices(template_id: str, headers: List[str]) -> List[str]:
|
||||
"""Prefija '* ' usando índices fijos de la plantilla base (independiente del idioma de cabecera)."""
|
||||
idx_set = _REQUIRED_INDICES.get(template_id)
|
||||
if not idx_set:
|
||||
return headers
|
||||
out: List[str] = []
|
||||
for header in headers:
|
||||
norm_header = _normalize_header_for_match(header)
|
||||
if norm_header and norm_header in required_norm:
|
||||
if header.startswith("* "):
|
||||
out.append(header)
|
||||
else:
|
||||
out.append(f"* {header}")
|
||||
for i, h in enumerate(headers):
|
||||
if i in idx_set and h and not str(h).startswith("* "):
|
||||
out.append(f"* {h}")
|
||||
else:
|
||||
out.append(header)
|
||||
out.append(h)
|
||||
return out
|
||||
|
||||
|
||||
@@ -231,6 +243,7 @@ def _build_registry() -> Dict[str, List[str]]:
|
||||
"imp_def_series",
|
||||
"exp_def_header",
|
||||
"exp_def_details",
|
||||
"exp_def_series",
|
||||
"cmex_header",
|
||||
"cmex_details",
|
||||
"cmex_series",
|
||||
@@ -292,7 +305,9 @@ def _build_registry() -> Dict[str, List[str]]:
|
||||
return registry
|
||||
|
||||
|
||||
_TEMPLATE_HEADERS: Dict[str, List[str]] = _build_registry()
|
||||
_REGISTRY_BASE_ES: Dict[str, List[str]] = _build_registry()
|
||||
_REQUIRED_INDICES: Dict[str, Set[int]] = _compute_required_indices_map(_REGISTRY_BASE_ES)
|
||||
_TEMPLATE_HEADERS: Dict[str, List[str]] = _REGISTRY_BASE_ES
|
||||
|
||||
# Nombre de archivo sugerido para descarga (sin path)
|
||||
TEMPLATE_FILENAMES: Dict[str, str] = {
|
||||
@@ -320,15 +335,65 @@ TEMPLATE_FILENAMES: Dict[str, str] = {
|
||||
"cmex_series": "EstructuraSeriesFacComprasMex.csv",
|
||||
"exp_def_header": "EstructuraEncFacExpoCamReg.csv",
|
||||
"exp_def_details": "EstructuraParExpoCamReg.csv",
|
||||
"exp_def_series": "EstructuraSeriesFacExpoCamReg.csv",
|
||||
}
|
||||
|
||||
|
||||
def get_download_headers(template_id: str, locale: Optional[str] = "es") -> Optional[List[str]]:
|
||||
"""
|
||||
Cabeceras de descarga para la plantilla (sin prefijo *).
|
||||
locale: es | en (default es).
|
||||
"""
|
||||
from api.v1.modules.a76.layouts_csv.common.template_locale import (
|
||||
download_header_cell,
|
||||
normalize_locale,
|
||||
)
|
||||
|
||||
loc = normalize_locale(locale)
|
||||
|
||||
cols = resolve_imports_template(template_id)
|
||||
if cols is not None:
|
||||
return [download_header_cell(c, loc) for c in cols]
|
||||
|
||||
if template_id in ("part_numbers", "items"):
|
||||
from api.v1.modules.a76.layouts_csv.parts import template_config as parts_tc
|
||||
|
||||
return parts_tc.download_headers_for_locale(loc)
|
||||
|
||||
if template_id == "material_classes":
|
||||
from api.v1.modules.a76.layouts_csv.classes import template_config as classes_tc
|
||||
|
||||
return classes_tc.download_headers_for_locale(loc)
|
||||
|
||||
# template_id del registry → (module_columns, clave interna del dict de columnas)
|
||||
catalog_resolvers: list[tuple[str, dict, str]] = [
|
||||
("boms", BOMS_TEMPLATE_COLUMNS, "boms"),
|
||||
("customs_brokers", CUSTOMS_BROKERS_TEMPLATE_COLUMNS, "customs_brokers"),
|
||||
("clients_providers", CLIENTS_PROVIDERS_TEMPLATE_COLUMNS, "client_providers"),
|
||||
("exchange_rates", EXCHANGE_RATE_TEMPLATE_COLUMNS, "exchange_rates"),
|
||||
("american_fractions", US_TARIFF_FRACTIONS_TEMPLATE_COLUMNS, "us_tariff_fractions"),
|
||||
("pedimentos", PEDIMENTOS_TEMPLATE_COLUMNS, "pedimentos"),
|
||||
("transports", VEHICLES_TEMPLATE_COLUMNS, "vehicles"),
|
||||
("drivers", DRIVERS_TEMPLATE_COLUMNS, "drivers"),
|
||||
("trailers", TRAILERS_TEMPLATE_COLUMNS, "trailers"),
|
||||
("transporters", TRANSPORTERS_TEMPLATE_COLUMNS, "transporters"),
|
||||
]
|
||||
for tid, mapping, inner_key in catalog_resolvers:
|
||||
if tid != template_id:
|
||||
continue
|
||||
cols = mapping.get(inner_key)
|
||||
if cols:
|
||||
return [download_header_cell(col, loc) for col in cols]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_template_headers(template_id: str) -> Optional[List[str]]:
|
||||
"""Devuelve la lista de cabeceras canónicas para el template_id, o None si no existe."""
|
||||
headers = _TEMPLATE_HEADERS.get(template_id)
|
||||
if headers is None:
|
||||
"""Cabeceras como en descarga ES histórica, con prefijo * en obligatorias."""
|
||||
raw = get_download_headers(template_id, "es")
|
||||
if raw is None:
|
||||
return None
|
||||
return _apply_required_prefix(template_id, headers)
|
||||
return _apply_required_prefix_indices(template_id, raw)
|
||||
|
||||
|
||||
def get_template_filename(template_id: str) -> str:
|
||||
@@ -336,14 +401,19 @@ def get_template_filename(template_id: str) -> str:
|
||||
return TEMPLATE_FILENAMES.get(template_id, f"plantilla_{template_id}.csv")
|
||||
|
||||
|
||||
def generate_csv_content(template_id: str, include_bom: bool = True) -> Optional[bytes]:
|
||||
def generate_csv_content(
|
||||
template_id: str,
|
||||
locale: str = "es",
|
||||
include_bom: bool = True,
|
||||
) -> Optional[bytes]:
|
||||
"""
|
||||
Genera el contenido CSV (solo fila de cabeceras) para el template_id.
|
||||
UTF-8, opcionalmente con BOM para Excel.
|
||||
"""
|
||||
headers = get_template_headers(template_id)
|
||||
headers = get_download_headers(template_id, locale)
|
||||
if not headers:
|
||||
return None
|
||||
headers = _apply_required_prefix_indices(template_id, headers)
|
||||
buf = io.StringIO()
|
||||
writer = csv.writer(buf, lineterminator="\n")
|
||||
writer.writerow(headers)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Rutas para descargar plantillas CSV generadas desde código (sin archivos XLS/XLSX).
|
||||
"""
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
|
||||
from core.security import get_current_user
|
||||
@@ -12,17 +12,23 @@ from .registry import generate_csv_content, get_template_filename
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_LOCALE_ALLOWED = frozenset({"es", "en"})
|
||||
|
||||
|
||||
@router.get("/{template_id}", response_class=Response)
|
||||
async def download_csv_template(
|
||||
template_id: str,
|
||||
locale: Optional[str] = Query("es", description="es | en: idioma de las cabeceras del CSV"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Devuelve un CSV con solo la fila de cabeceras para la plantilla indicada.
|
||||
Las cabeceras son los nombres canónicos definidos en cada template_config.
|
||||
Query `locale`: es (defecto) o en — cabeceras localizadas cuando estén definidas.
|
||||
"""
|
||||
content = generate_csv_content(template_id, include_bom=True)
|
||||
loc = (locale or "es").lower().strip()
|
||||
if loc not in _LOCALE_ALLOWED:
|
||||
raise HTTPException(status_code=400, detail="locale must be 'es' or 'en'")
|
||||
content = generate_csv_content(template_id, locale=loc, include_bom=True)
|
||||
if content is None:
|
||||
raise HTTPException(status_code=404, detail=f"Plantilla desconocida: {template_id}")
|
||||
filename = get_template_filename(template_id)
|
||||
@@ -31,5 +37,7 @@ async def download_csv_template(
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
"Cache-Control": "private, no-store, max-age=0, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Límites alineados a boms/validators/common.py y common/mappers."""
|
||||
from typing import Dict
|
||||
|
||||
BOMS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"NUMPARTE_PADRE": 70,
|
||||
"NUMPARTE_COMPONENTE": 70,
|
||||
"CANTIDAD": 30,
|
||||
"UNIMED": 10,
|
||||
"VERSION_BOM": 20,
|
||||
"VERSION_BILL": 20,
|
||||
}
|
||||
@@ -6,6 +6,10 @@ Placeholders hasta tener el XLS definitivo; ajustar canónicos y aliases según
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import BOMS_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"boms": [
|
||||
@@ -18,6 +22,17 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
# UNIMED también en parts/catálogo ("commercial UOM"); en BOM es unidad de cantidad genérica.
|
||||
_BOM_LABEL_EN_OVERRIDES = {"UNIMED": "Unit of measure"}
|
||||
for col in TEMPLATE_COLUMNS.get("boms") or []:
|
||||
canon = col.get("canonical")
|
||||
if canon in _BOM_LABEL_EN_OVERRIDES:
|
||||
col.setdefault("labels", {})["en"] = _BOM_LABEL_EN_OVERRIDES[canon]
|
||||
|
||||
inject_max_char_hints_into_columns(TEMPLATE_COLUMNS.get("boms"), BOMS_CSV_MAX_CHARS)
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("boms")
|
||||
@@ -29,6 +44,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ Por ahora misma estructura que encabezado/partidas de exportación; luego se aju
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
|
||||
# Cambio de régimen: cam_reg_header, cam_reg_details
|
||||
# Regularización: regulariz_header, regulariz_details
|
||||
@@ -108,6 +110,8 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
|
||||
def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
return TEMPLATE_COLUMNS.get(template_id)
|
||||
@@ -124,6 +128,8 @@ def build_normalized_lookup(template_id: str, normalize_header_fn) -> Dict[str,
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Máximos alineados a classes/validators/common.py."""
|
||||
from typing import Dict
|
||||
|
||||
MATERIAL_CLASSES_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"CLASE": 30,
|
||||
"DESCRIPCIONE": 500,
|
||||
"DESCRIPCIONI": 500,
|
||||
"CLAVEMAT": 10,
|
||||
"UNIMED": 5,
|
||||
"FRACCION": 20,
|
||||
"FRACCIONAME": 16,
|
||||
"TASADEPRECIA": 24,
|
||||
"CLAVESUB": 5,
|
||||
"REVFISICA": 10,
|
||||
"FRACCIONEXENTAIVA": 4,
|
||||
}
|
||||
@@ -8,10 +8,19 @@ from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common import csv_reader as common_csv_reader
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns, merge_enum_hint_before_max
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import (
|
||||
download_header_cell,
|
||||
merge_download_header_into_lookup,
|
||||
merge_labels_into_lookup,
|
||||
normalize_locale,
|
||||
)
|
||||
from .csv_max_chars_for_headers import MATERIAL_CLASSES_CSV_MAX_CHARS
|
||||
|
||||
|
||||
# Valores que indican que la primera fila es cabecera (primera columna normalizada)
|
||||
FIRST_COLUMN_HEADER_VALUES = ("CLAVE CLASE", "CLASE")
|
||||
FIRST_COLUMN_HEADER_VALUES = ("CLAVE CLASE", "CLASE", "CLASS CODE")
|
||||
|
||||
def detect_headers_or_data(
|
||||
file_path: str,
|
||||
@@ -71,6 +80,19 @@ TEMPLATE_DOWNLOAD_HEADERS: List[str] = [
|
||||
"CODIGO DE PRODUCTO/SERVICIO CP",
|
||||
]
|
||||
|
||||
TEMPLATE_DOWNLOAD_HEADERS_EN: List[str] = [
|
||||
"CLASS CODE",
|
||||
"DESCRIPTION (SPANISH)",
|
||||
"DESCRIPTION (ENGLISH)",
|
||||
"MATERIAL TYPE",
|
||||
"COMMERCIAL UOM",
|
||||
"MX FRACTION",
|
||||
"US FRACTION",
|
||||
"DEPRECIATION RATE",
|
||||
"PHYSICAL REVIEW (1/0)",
|
||||
"PRODUCT/SERVICE CP CODE",
|
||||
]
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"material_classes": [
|
||||
{"canonical": "CLASE", "aliases": ["CLAVE CLASE", "CLASS", "CODIGO", "CLASE CODIGO"]},
|
||||
@@ -87,6 +109,48 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
# Mismos canónicos que parts (CLASE, FRACCION, …); restaurar etiquetas de catálogo de clases.
|
||||
# Mismo texto que TEMPLATE_DOWNLOAD_HEADERS_EN (plantilla Clarion EN).
|
||||
_MATERIAL_CLASS_LABEL_EN_OVERRIDES = {
|
||||
"CLASE": "CLASS CODE",
|
||||
"UNIMED": "COMMERCIAL UOM",
|
||||
"FRACCION": "MX FRACTION",
|
||||
"FRACCIONAME": "US FRACTION",
|
||||
}
|
||||
for col in TEMPLATE_COLUMNS.get("material_classes") or []:
|
||||
canon = col.get("canonical")
|
||||
if canon in _MATERIAL_CLASS_LABEL_EN_OVERRIDES:
|
||||
col.setdefault("labels", {})["en"] = _MATERIAL_CLASS_LABEL_EN_OVERRIDES[canon]
|
||||
|
||||
inject_max_char_hints_into_columns(
|
||||
TEMPLATE_COLUMNS.get("material_classes"), MATERIAL_CLASSES_CSV_MAX_CHARS
|
||||
)
|
||||
for _mc in TEMPLATE_COLUMNS.get("material_classes") or []:
|
||||
if _mc.get("canonical") == "REVFISICA":
|
||||
merge_enum_hint_before_max(_mc, ["1", "0"])
|
||||
|
||||
_MC_DL_ORDER = [
|
||||
"CLASE",
|
||||
"DESCRIPCIONE",
|
||||
"DESCRIPCIONI",
|
||||
"CLAVEMAT",
|
||||
"UNIMED",
|
||||
"FRACCION",
|
||||
"FRACCIONAME",
|
||||
"TASADEPRECIA",
|
||||
"REVFISICA",
|
||||
"FRACCIONEXENTAIVA",
|
||||
]
|
||||
_MC_BY_CANON = {c["canonical"]: c for c in (TEMPLATE_COLUMNS.get("material_classes") or [])}
|
||||
_MATERIAL_CLASSES_DOWNLOAD_DEFS = [dict(_MC_BY_CANON[k]) for k in _MC_DL_ORDER]
|
||||
|
||||
|
||||
def download_headers_for_locale(locale: str) -> List[str]:
|
||||
loc = normalize_locale(locale)
|
||||
return [download_header_cell(d, loc) for d in _MATERIAL_CLASSES_DOWNLOAD_DEFS]
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("material_classes")
|
||||
@@ -98,6 +162,15 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
for es_h, en_h in zip(TEMPLATE_DOWNLOAD_HEADERS, TEMPLATE_DOWNLOAD_HEADERS_EN):
|
||||
if not es_h or not en_h:
|
||||
continue
|
||||
es_key = normalize_header_fn(es_h)
|
||||
canon = lookup.get(es_key)
|
||||
if canon:
|
||||
lookup[normalize_header_fn(en_h)] = canon
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Máximos de caracteres por columna canonical (plantilla client_providers) para sufijos en cabecera CSV.
|
||||
|
||||
Debe mantenerse alineado con `common/mappers.py` → MAX_LEN y columnas String(n) en
|
||||
`clients_and_providers.models`. Sin imports de SQLAlchemy ni Pydantic para poder cargar la plantilla sin arrancar la app.
|
||||
"""
|
||||
from typing import Dict
|
||||
|
||||
CLIENT_PROVIDER_CSV_COLUMN_MAX_CHARS: Dict[str, int] = {
|
||||
"SHORT_NAME": 10,
|
||||
"NOMBRE": 256,
|
||||
"RFC": 30,
|
||||
"DIRECCION": 100,
|
||||
"NUM_EXT": 20,
|
||||
"CODIGO POSTAL": 15,
|
||||
"COLONIA": 40,
|
||||
"CIUDAD": 30,
|
||||
"ESTADO": 30,
|
||||
"PAIS": 3,
|
||||
"TELEFONO": 30,
|
||||
"FAX": 30,
|
||||
"EMAIL": 100,
|
||||
"CURP": 19,
|
||||
"TIPO_PROGRAMA_SECON": 7,
|
||||
"NUM_PROGRAMA_SECON": 40,
|
||||
"NUM_AUT_PROSEC": 20,
|
||||
"REGISTRO_EMPRESA_CERT": 40,
|
||||
"INFORMACION_EXTRA": 399,
|
||||
"CONTACTO": 50,
|
||||
"MANUFACTURER_ID": 25,
|
||||
"TAX_ID_PROGRAMS": 30,
|
||||
"BROKER_EXPO": 6,
|
||||
"BROKER_IMPO": 6,
|
||||
"CLAVE_TRANSFER": 8,
|
||||
"CLAVE_WEB": 40,
|
||||
"RESPONSABLE": 80,
|
||||
"POSICION": 30,
|
||||
"INCOTERM": 19,
|
||||
"VINCULACION": 1,
|
||||
"TRANSFORMA_SUBMAQ": 1,
|
||||
}
|
||||
@@ -13,13 +13,27 @@ AH=COL_EXTRA (desfase).
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import (
|
||||
merge_download_header_into_lookup,
|
||||
merge_labels_into_lookup,
|
||||
)
|
||||
from .common.csv_max_chars_for_headers import CLIENT_PROVIDER_CSV_COLUMN_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"client_providers": [
|
||||
# Col A - Procedencia (E=Extranjero, N=Nacional)
|
||||
{"canonical": "PROCEDENCIA", "aliases": ["TIPO PROCEDENCIA", "EXTranjero/Nacional", "E/N"]},
|
||||
# Col B - Tipo Cliente (C/P/A)
|
||||
{"canonical": "TIPO", "aliases": ["CLIENT_OR_PROVIDER", "TIPO ENTIDAD", "CLIENTE O PROVEEDOR"]},
|
||||
# Col A - Procedencia (E=Extranjero, N=Nacional). Manual: select N/E (≤3). Ver CSV_HEADER_HINTS.md
|
||||
{
|
||||
"canonical": "PROCEDENCIA",
|
||||
"aliases": ["TIPO PROCEDENCIA", "EXTranjero/Nacional", "E/N"],
|
||||
"csv_hint": {"kind": "enum_codes", "codes": ["N", "E"]},
|
||||
},
|
||||
# Col B - Tipo Cliente (C/P/A). Manual: client/provider/both (≤3). Ver CSV_HEADER_HINTS.md
|
||||
{
|
||||
"canonical": "TIPO",
|
||||
"aliases": ["CLIENT_OR_PROVIDER", "TIPO ENTIDAD", "CLIENTE O PROVEEDOR"],
|
||||
"csv_hint": {"kind": "enum_codes", "codes": ["C", "P", "A"]},
|
||||
},
|
||||
# Col C - Clave cliente/proveedor (máx 8 Clarion)
|
||||
{"canonical": "SHORT_NAME", "aliases": ["CLAVE", "CLAVE CORTA", "NOMBRE CORTO", "SIGLAS"]},
|
||||
# Col D - Nombre
|
||||
@@ -92,6 +106,34 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
|
||||
def _inject_max_char_hints_client_providers(columns: Optional[List[Dict[str, Any]]]) -> None:
|
||||
"""Añade csv_hint max_chars desde CLIENT_PROVIDER_CSV_COLUMN_MAX_CHARS (paridad modelos/MAX_LEN)."""
|
||||
if not columns:
|
||||
return
|
||||
for item in columns:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
canon = item.get("canonical")
|
||||
if not canon:
|
||||
continue
|
||||
n = CLIENT_PROVIDER_CSV_COLUMN_MAX_CHARS.get(str(canon).strip())
|
||||
if n is None:
|
||||
continue
|
||||
mc: Dict[str, Any] = {"kind": "max_chars", "n": n}
|
||||
ch = item.get("csv_hint")
|
||||
if ch is None:
|
||||
item["csv_hint"] = mc
|
||||
elif isinstance(ch, dict):
|
||||
item["csv_hint"] = [ch, mc]
|
||||
elif isinstance(ch, list):
|
||||
item["csv_hint"] = [*ch, mc]
|
||||
|
||||
|
||||
_inject_max_char_hints_client_providers(TEMPLATE_COLUMNS.get("client_providers"))
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
"""normalized_header -> canonical_name para plantilla client_providers."""
|
||||
@@ -104,6 +146,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
408
backend/api/v1/modules/a76/layouts_csv/common/csv_headers_en.py
Normal file
408
backend/api/v1/modules/a76/layouts_csv/common/csv_headers_en.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
Traducciones EN de cabeceras CSV por nombre canónico (clave interna).
|
||||
Usado para plantillas descargadas locale=en y para aliases de carga.
|
||||
"""
|
||||
|
||||
|
||||
def _facturas_headers() -> dict[str, str]:
|
||||
return {
|
||||
# Encabezados / series / partidas — alineado con messages/en.json (pedimento, exchange rate, incrementables, discharge).
|
||||
"PEDIMENTO": "Pedimento",
|
||||
"REMESA": "Consignment",
|
||||
"NUMERO FACTURA": "Invoice number",
|
||||
"FECHA FACTURA": "Invoice date",
|
||||
"TIPO DE CAMBIO": "Exchange rate",
|
||||
"REGIMEN": "Regime",
|
||||
"CLAVE PROVEEDOR": "Supplier code",
|
||||
"CLAVE VENDIDO A": "Sold-to code",
|
||||
"CLAVE ENVIADO A": "Ship-to code",
|
||||
"AGENTE ADUANAL": "Customs broker",
|
||||
"CLAVE TRANSPORTISTA": "Carrier code",
|
||||
"NOMBRE CONDUCTOR": "Driver name",
|
||||
"TIPO TRANSPORTE": "Transport type",
|
||||
"CLAVE TRANSPORTE": "Transport code",
|
||||
"NUMERO CAJA": "Trailer/box number",
|
||||
"NUMERO TRANSPORTE": "Transport number",
|
||||
"TIPO MONEDA": "Currency type",
|
||||
"CLAVE MONEDA": "Currency code",
|
||||
"FLETES": "Freight",
|
||||
"VALOR SEGUROS": "Insurance value",
|
||||
"SEGUROS": "Insurance",
|
||||
"EMBALAJES": "Packaging",
|
||||
"OTROS INCREMENTABLES": "Incrementables (other)",
|
||||
"CLAVE INCOTERM": "Incoterm code",
|
||||
"PRECINTO": "Seal",
|
||||
"FECHA EMISION": "Issue date",
|
||||
"TIPO PESO": "Weight type",
|
||||
"E DOCUMENT": "E-document",
|
||||
"NUM OPERACION": "Operation number",
|
||||
"ADUANA DE CRUCE": "Customs office (crossing)",
|
||||
"OBSERVACIONES E": "Export remarks",
|
||||
"OBSERVACIONES I": "Import remarks",
|
||||
"FACTURA ALTERNA": "Alternate invoice",
|
||||
"NUM PROYECTO": "Project number",
|
||||
"ORDEN COMPRA": "Purchase order",
|
||||
"FACTURA EXPO REF": "Export invoice reference",
|
||||
"MANIFIESTO": "Manifest",
|
||||
"ENVIADO POR": "Shipped by",
|
||||
# Partidas impo temp / compartidas
|
||||
"NUMERO FACTURA EXPO": "Export invoice number",
|
||||
"LINEA": "Line",
|
||||
"LINEA EXPO": "Export line",
|
||||
"LINEA IMPO": "Import line",
|
||||
"LINEA FACTURA": "Invoice line",
|
||||
"LINEA SERIE": "Serial line",
|
||||
"TIPO DE IMPO": "Import type",
|
||||
"FACTURA IMPO": "Import invoice",
|
||||
"GENERA DESCARGA": "Generate discharge",
|
||||
"CANTIDAD EXPORTADA/DESCARGAR": "Exported qty / qty to discharge",
|
||||
"CLASE": "Class",
|
||||
"CANTIDAD IMPORTADA": "Imported quantity",
|
||||
"UNIDAD DE MEDIDA": "Unit of measure",
|
||||
"COSTO UNITARIO": "Unit cost",
|
||||
"PRECIO UNITARIO": "Unit price",
|
||||
"VALOR COMERCIAL": "Commercial value",
|
||||
"PESO NETO": "Net weight",
|
||||
"PESO BRUTO": "Gross weight",
|
||||
"CANTIDAD BULTOS": "Package count",
|
||||
"CLAVE BULTOS": "Package code",
|
||||
"PAIS ORIGEN": "Country of origin",
|
||||
"FRACCION ARANCELARIA": "Tariff fraction",
|
||||
"FRACCION": "Fraction",
|
||||
"PREFERENCIA ARANCELARIA": "Tariff preference",
|
||||
"SECTOR": "Sector",
|
||||
"FRACCION AMERICANA": "US tariff fraction",
|
||||
"ORDEN DE COMPRA": "Purchase order",
|
||||
"DESCRIPCION ESPAÑOL": "Description (Spanish)",
|
||||
"DESCRIPCION INGLES": "Description (English)",
|
||||
"DESCRIPCION": "Description",
|
||||
"MARCA": "Brand",
|
||||
"MODELO": "Model",
|
||||
"ES PARTIDA O SUBPARTIDA": "Line or sub-line",
|
||||
"ES PARTIDA/SUBPARTIDA": "Line or sub-line",
|
||||
"LINEA PRINCIPAL": "Main line",
|
||||
"NUM. PARTE": "Part number",
|
||||
"NUMPARTE": "Part number",
|
||||
"SE PAGO IMPUESTO": "Tax paid (Y/N)",
|
||||
"FORMA DE PAGO": "Payment method",
|
||||
"METODO DE VALORACION": "Valuation method",
|
||||
"DESCRIPCION EXTRA": "Extra description",
|
||||
"INFORMACION ADICIONAL": "Additional information",
|
||||
"AGREGAR/SUSTITUIR": "Add/replace",
|
||||
"AGREGAR(A)/SUSTITUIR(S)": "Add/replace",
|
||||
"TOTAL": "Total",
|
||||
"NUMERO ENTRADA": "Entry number",
|
||||
"LOTE": "Lot",
|
||||
"ID TYPE": "ID type",
|
||||
"SERIE": "Serial",
|
||||
"SUB MODELO": "Sub-model",
|
||||
"NUM PARTE": "Part number",
|
||||
"NUMERO ID": "ID number",
|
||||
"COL_EXTRA": "Extra column",
|
||||
}
|
||||
|
||||
|
||||
def _series_merge() -> dict[str, str]:
|
||||
"""Series share columns; ensure overlap with facturas_headers."""
|
||||
return {
|
||||
"NUMERO FACTURA": "Invoice number",
|
||||
"LINEA FACTURA": "Invoice line",
|
||||
"LINEA SERIE": "Serial line",
|
||||
"SERIE": "Serial",
|
||||
"MODELO": "Model",
|
||||
"NUM PARTE": "Part number",
|
||||
"SUB MODELO": "Sub-model",
|
||||
"NUMERO ID": "ID number",
|
||||
"COL_EXTRA": "Extra column",
|
||||
}
|
||||
|
||||
|
||||
def _customs_exchange_us_fractions_boms() -> dict[str, str]:
|
||||
return {
|
||||
# customs_brokers
|
||||
"TIPO": "Type",
|
||||
"CLAVE": "Code",
|
||||
"LICENCIA": "License",
|
||||
"NOMBRE": "Name",
|
||||
"RFC": "Tax ID",
|
||||
"DIRECCION": "Address",
|
||||
"CODIGO POSTAL": "Postal code",
|
||||
"CIUDAD": "City",
|
||||
"ESTADO": "State",
|
||||
"PAIS": "Country",
|
||||
"TELEFONO": "Phone",
|
||||
"FAX": "Fax",
|
||||
"EMAIL": "Email",
|
||||
"PERSONAL_ID": "Personal ID / CURP",
|
||||
"COL_EXTRA": "Extra column",
|
||||
"POSICION": "Position",
|
||||
"EMPRESA": "Company",
|
||||
"CONTACTO": "Contact",
|
||||
# exchange_rates
|
||||
"FECHA": "Date",
|
||||
"VALOR": "Exchange rate",
|
||||
"MONEDA_LOCAL": "Local currency",
|
||||
"MONEDA_EXTRANJERA": "Foreign currency",
|
||||
# us_tariff_fractions
|
||||
"FRACCION_ARANCELARIA": "Tariff fraction",
|
||||
"PREFIJO": "Prefix",
|
||||
"UNIDAD_DE_MEDIDA": "Unit of measure",
|
||||
"DESCRIPCION": "Description",
|
||||
"TIPO_DE_ADVALOREM": "Ad valorem type",
|
||||
"ADVALOREM_PCT": "Ad valorem %",
|
||||
"ADVALOREM_DLLS": "Ad valorem (USD)",
|
||||
# boms
|
||||
"NUMPARTE_PADRE": "Parent part number",
|
||||
"NUMPARTE_COMPONENTE": "Component part number",
|
||||
"CANTIDAD": "Quantity",
|
||||
"UNIMED": "Unit of measure",
|
||||
"VERSION_BOM": "BOM version",
|
||||
"VERSION_BILL": "Bill version",
|
||||
}
|
||||
|
||||
|
||||
def _clients_providers_headers() -> dict[str, str]:
|
||||
return {
|
||||
"PROCEDENCIA": "Origin (foreign/domestic)",
|
||||
"TIPO": "Entity type",
|
||||
"SHORT_NAME": "Short code",
|
||||
"NOMBRE": "Name",
|
||||
"RFC": "Tax ID",
|
||||
"DIRECCION": "Address",
|
||||
"NUM_EXT": "Exterior number",
|
||||
"CODIGO POSTAL": "Postal code",
|
||||
"COLONIA": "District",
|
||||
"CIUDAD": "City",
|
||||
"ESTADO": "State",
|
||||
"PAIS": "Country",
|
||||
"TELEFONO": "Phone",
|
||||
"FAX": "Fax",
|
||||
"EMAIL": "Email",
|
||||
"CURP": "CURP",
|
||||
"TIPO_PROGRAMA_SECON": "SECON program type",
|
||||
"NUM_PROGRAMA_SECON": "SECON program number",
|
||||
"FECHA_AUT_SECON": "SECON authorization date",
|
||||
"ES_PROSEC": "Is PROSEC",
|
||||
"NUM_AUT_PROSEC": "PROSEC authorization number",
|
||||
"VINCULACION": "Linkage",
|
||||
"ES_EMPRESA_CERTIFICADA": "Certified company",
|
||||
"REGISTRO_EMPRESA_CERT": "Certified company registry",
|
||||
"INFORMACION_EXTRA": "Extra information",
|
||||
"CONTACTO": "Contact",
|
||||
"MANUFACTURER_ID": "Manufacturer ID",
|
||||
"TAX_ID_PROGRAMS": "Tax ID programs",
|
||||
"BROKER_EXPO": "Export broker",
|
||||
"BROKER_IMPO": "Import broker",
|
||||
"CLAVE_TRANSFER": "Transfer code",
|
||||
"TRANSFORMA_SUBMAQ": "Transformer/sub-maquila",
|
||||
"CLAVE_WEB": "Web code",
|
||||
"COL_EXTRA": "Extra column",
|
||||
"RESPONSABLE": "Responsible",
|
||||
"POSICION": "Position",
|
||||
"INCOTERM": "Incoterm",
|
||||
"ACTIVO": "Active",
|
||||
}
|
||||
|
||||
|
||||
def _pedimentos_headers() -> dict[str, str]:
|
||||
return {
|
||||
"AÑO": "Year",
|
||||
"PATENTE": "Patent",
|
||||
"NUMERO": "Number",
|
||||
"PEDIMENTO": "Pedimento",
|
||||
"TIPO_OPERACION": "Operation type",
|
||||
"TIPO_PEDIMENTO": "Pediment type",
|
||||
"CLAVE_PEDIMENTO": "Pediment code",
|
||||
"REGIMEN": "Regime",
|
||||
"FECHA_INICIO": "Start date",
|
||||
"FECHA_FINAL": "End date",
|
||||
"FECHA_PAGO": "Payment date",
|
||||
"FECHA_ENTRADA_RECINTO": "Compound entry date",
|
||||
"FECHA_EXTRACCION_RECINTO": "Compound exit date",
|
||||
"FECHA_RECIBIDO": "Received date",
|
||||
"FECHA_AUTORIZACION": "Authorization date",
|
||||
"FECHA_CIERRE": "Closing date",
|
||||
"FECHA_REVISION": "Review date",
|
||||
"ADUANA_SECCION_CRUCE": "Customs office / crossing section",
|
||||
"ACUSE_ELECTRONICO": "Electronic acknowledgment",
|
||||
"INDIVIDUAL_CONSOLIDADO": "Individual/consolidated",
|
||||
"MET_TRANSP_ENTRADA": "Inbound transport method",
|
||||
"MET_TRANSP_ARRIVO": "Arrival transport method",
|
||||
"MET_TRANSP_SALIDA": "Outbound transport method",
|
||||
"IEPS": "IEPS",
|
||||
"IEPS_2": "IEPS (2)",
|
||||
"DTA": "DTA",
|
||||
"DTA_2": "DTA (2)",
|
||||
"CNT": "CNT",
|
||||
"CNT_2": "CNT (2)",
|
||||
"PREVALIDACION": "Pre-validation",
|
||||
"MONTO_TIGIE": "TIGIE amount",
|
||||
"PAGO_IMPUESTO": "Tax paid (Y/N)",
|
||||
"ES_MIXTO": "Mixed (yes/no)",
|
||||
"OBS_RECTIFICA": "Rectification remarks",
|
||||
"OPCION_DESTINO": "Destination option",
|
||||
"VALOR_IVA": "VAT value",
|
||||
"VALOR_ME": "Foreign currency value",
|
||||
"VALOR_ADUANAS": "Customs value",
|
||||
"VALOR_USD": "USD value",
|
||||
"VALOR_SEGUROS": "Insurance value",
|
||||
"FLETE": "Freight",
|
||||
"SEGUROS": "Insurance",
|
||||
"EMBALAJES": "Packaging",
|
||||
"OTROS_INCREMENTABLES": "Incrementables (other)",
|
||||
"ESTATUS": "Status",
|
||||
"PERSONA_REV": "Reviewer",
|
||||
"OBSERVACIONES": "Remarks",
|
||||
"TIPO_CAMBIO": "Exchange rate",
|
||||
"REPRESENTANTE_AA": "Customs broker representative",
|
||||
"CLIENTE_SHORT_NAME": "Client short code",
|
||||
"CLAVE_DEST_ORIGEN": "Destination origin code",
|
||||
"IDENTIFICADORES": "Identifiers",
|
||||
"CUOTAS_COMPENSATORIAS": "Compensatory quotas",
|
||||
"ERRORES": "Errors",
|
||||
"MULTAS": "Fines",
|
||||
"RECARGOS": "Surcharges",
|
||||
"PRECIO_PAGADO": "Price paid",
|
||||
"PESO_BRUTO": "Gross weight",
|
||||
"IVA_DE_PREV": "VAT from pre-validation",
|
||||
"IVA_2": "VAT (2)",
|
||||
"IGI_2": "IGI (2)",
|
||||
"FORMA_PAGO_IVA": "VAT payment method",
|
||||
"FORMA_PAGO_IVA_2": "VAT payment method (2)",
|
||||
"FORMA_PAGO_IGI": "IGI payment method",
|
||||
"FORMA_PAGO_IGI_2": "IGI payment method (2)",
|
||||
"FORMA_PAGO_IEPS_2": "IEPS payment method (2)",
|
||||
"FORMA_PAGO_DTA": "DTA payment method",
|
||||
"FORMA_PAGO_DTA_2": "DTA payment method (2)",
|
||||
"FORMA_PAGO_CNT_2": "CNT payment method (2)",
|
||||
"FORMA_PAGO_PREVAL": "Pre-validation payment method",
|
||||
"FORMA_PAGO_PREVALIDACION_2": "Pre-validation payment method (2)",
|
||||
}
|
||||
|
||||
|
||||
def _vehicles_drivers_trailers_transporters() -> dict[str, str]:
|
||||
return {
|
||||
# vehicles / transports template "vehicles"
|
||||
"CLAVE": "Code",
|
||||
"CODIGO DE ENTIDAD": "Entity code",
|
||||
"TIPO TRANSPORTE": "Transport type",
|
||||
"CLAVE TRANSPORTE": "Transport code",
|
||||
"CLAVE ACE": "ACE code",
|
||||
"PLACAS": "Plates",
|
||||
"PRECINTO": "Seal",
|
||||
"NUMERO DOT": "DOT number",
|
||||
"TRANSPONDEDOR": "Transponder",
|
||||
"VIN": "VIN",
|
||||
"EMPRESA ASEGURADORA": "Insurance company",
|
||||
"NUM. ASEGURADORA": "Insurance policy number",
|
||||
"FECHA DE ASEGURADORA": "Insurance date",
|
||||
"MONTO ASEGURADO": "Insured amount",
|
||||
"COL_EXTRA": "Extra column",
|
||||
# drivers
|
||||
"TRANSPORTISTA": "Carrier",
|
||||
"LINEA": "Line",
|
||||
"CLAVE CONDUCTOR": "Driver code",
|
||||
"NOMBRE(S)": "Given name(s)",
|
||||
"APELLIDO PATERNO": "Last name (paternal)",
|
||||
"SEXO": "Gender",
|
||||
"FECHA NACIMIENTO": "Birth date",
|
||||
"PAIS NACIMIENTO": "Birth country",
|
||||
"LICENCIA": "License",
|
||||
"FORMA IDENTIFICACION 1": "ID type 1",
|
||||
"NUM. IDENTIFICACION 1": "ID number 1",
|
||||
"FORMA IDENTIFICACION 2": "ID type 2",
|
||||
"NUM. IDENTIFICACION 2": "ID number 2",
|
||||
"IDENTIFICACION ACE": "ACE identification",
|
||||
"PAIS": "Country",
|
||||
"PAIS 2": "Country 2",
|
||||
"ESTADO": "State",
|
||||
"ESTADO 2": "State 2",
|
||||
"PERMISO LINEA EXPRESS": "Express line permit",
|
||||
"PERMISO MAT. PELIGROSO": "Hazmat permit",
|
||||
"TRANSPORTA MAT. PELIGROSO?": "Transports hazardous material?",
|
||||
# trailers
|
||||
"NUMERO TRAILER": "Trailer number",
|
||||
"TIPO TRAILER": "Trailer type",
|
||||
"CODIGO ENTIDAD": "Entity code",
|
||||
"CLAVE CONTENEDOR": "Container code",
|
||||
# transporters
|
||||
"CLAVE TRANSPORTISTA": "Carrier code",
|
||||
"NOMBRE CORTO": "Short name",
|
||||
"RESPONSABLE": "Responsible",
|
||||
"RFC": "Tax ID",
|
||||
"CALLES": "Street address",
|
||||
"CODIGO CAAT": "CAAT code",
|
||||
"CODIGO CARGADOR": "Loader code",
|
||||
"CODIGO TRANS": "Transport code",
|
||||
"TIPO INTERFASE TRANS": "Carrier interface type",
|
||||
"SERVIDOR FTP": "FTP server",
|
||||
"USUARIO FTP": "FTP user",
|
||||
"CLAVE ACCESO FTP": "FTP password",
|
||||
"DIRECTORIO FTP": "FTP directory",
|
||||
}
|
||||
|
||||
|
||||
def _material_classes_headers() -> dict[str, str]:
|
||||
# Coincide con classes/template_config.TEMPLATE_DOWNLOAD_HEADERS_EN (salvo columnas solo en template extendido).
|
||||
return {
|
||||
"CLASE": "CLASS CODE",
|
||||
"DESCRIPCIONE": "DESCRIPTION (SPANISH)",
|
||||
"DESCRIPCIONI": "DESCRIPTION (ENGLISH)",
|
||||
"CLAVEMAT": "MATERIAL TYPE",
|
||||
"UNIMED": "COMMERCIAL UOM",
|
||||
"FRACCION": "MX FRACTION",
|
||||
"FRACCIONAME": "US FRACTION",
|
||||
"TASADEPRECIA": "DEPRECIATION RATE",
|
||||
"CLAVESUB": "SUB KEY",
|
||||
"REVFISICA": "PHYSICAL REVIEW (1/0)",
|
||||
"FRACCIONEXENTAIVA": "PRODUCT/SERVICE CP CODE",
|
||||
}
|
||||
|
||||
|
||||
def _parts_headers() -> dict[str, str]:
|
||||
# Alineado con parts/template_config.TEMPLATE_DOWNLOAD_HEADERS_EN.
|
||||
return {
|
||||
"NUMPARTE": "PART NUMBER",
|
||||
"NUMPARTECOM": "COMMERCIAL PART NUMBER",
|
||||
"DESCRIPCIONE": "DESCRIPTION (SPANISH)",
|
||||
"DESCRIPCIONI": "DESCRIPTION (ENGLISH)",
|
||||
"CLASE": "CLASS",
|
||||
"UNIMED": "COMMERCIAL UNIT OF MEASURE",
|
||||
"COSTOUNIT": "UNIT COST",
|
||||
"TIPOMONEDA": "COST CURRENCY TYPE",
|
||||
"CLAVEMONEDA": "CURRENCY CODE",
|
||||
"PESOUNIT": "UNIT WEIGHT",
|
||||
"TIPOPESO": "WEIGHT TYPE",
|
||||
"FRACCION": "FRACTION",
|
||||
"FRACCIONAME": "US FRACTION",
|
||||
"PAIS": "COUNTRY",
|
||||
"PREFERENCIA": "PREFERENCE",
|
||||
"SECTOR": "SECTOR",
|
||||
"RUTAIMAGEN": "IMAGE PATH",
|
||||
"FDAKEY": "FDA key",
|
||||
"FCCKEY": "FCC key",
|
||||
"LICENCIA": "License code",
|
||||
"ECCN": "ECCN",
|
||||
"EXPORTCODE": "Export code",
|
||||
"EXCLUSION": "Exclusion",
|
||||
"ACTIVO": "Active",
|
||||
}
|
||||
|
||||
|
||||
def _merge_all() -> dict[str, str]:
|
||||
merged: dict[str, str] = {}
|
||||
for part in (
|
||||
_facturas_headers(),
|
||||
_series_merge(),
|
||||
_customs_exchange_us_fractions_boms(),
|
||||
_clients_providers_headers(),
|
||||
_pedimentos_headers(),
|
||||
_vehicles_drivers_trailers_transporters(),
|
||||
_material_classes_headers(),
|
||||
_parts_headers(),
|
||||
):
|
||||
merged.update(part)
|
||||
return merged
|
||||
|
||||
|
||||
HEADER_LABEL_EN: dict[str, str] = _merge_all()
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Helpers to merge csv_hint max_chars into column definitions after inject_en_labels."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def inject_max_char_hints_into_columns(
|
||||
columns: Optional[List[Dict[str, Any]]],
|
||||
max_by_canonical: Dict[str, int],
|
||||
) -> None:
|
||||
"""Attach or append ``{\"kind\": \"max_chars\", \"n\": N}`` per canonical (aligned with validators/models)."""
|
||||
if not columns or not max_by_canonical:
|
||||
return
|
||||
for item in columns:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
canon = item.get("canonical")
|
||||
if not canon:
|
||||
continue
|
||||
n = max_by_canonical.get(str(canon).strip())
|
||||
if n is None:
|
||||
continue
|
||||
mc: Dict[str, Any] = {"kind": "max_chars", "n": n}
|
||||
ch = item.get("csv_hint")
|
||||
if ch is None:
|
||||
item["csv_hint"] = mc
|
||||
elif isinstance(ch, dict):
|
||||
item["csv_hint"] = [ch, mc]
|
||||
elif isinstance(ch, list):
|
||||
item["csv_hint"] = [*ch, mc]
|
||||
|
||||
|
||||
def append_csv_hint(item: Dict[str, Any], extra: Dict[str, Any]) -> None:
|
||||
"""Añade un hint (digits, enum adicional, etc.) tras los existentes."""
|
||||
ch = item.get("csv_hint")
|
||||
if ch is None:
|
||||
item["csv_hint"] = extra
|
||||
return
|
||||
if isinstance(ch, dict):
|
||||
item["csv_hint"] = [ch, extra]
|
||||
return
|
||||
if isinstance(ch, list):
|
||||
item["csv_hint"] = [*ch, extra]
|
||||
|
||||
|
||||
def merge_enum_hint_before_max(
|
||||
item: Dict[str, Any],
|
||||
enum_codes: List[str],
|
||||
) -> None:
|
||||
"""Prepend enum_codes hint if not already present (for columns with fixed ≤3 values)."""
|
||||
if len(enum_codes) == 0 or len(enum_codes) > 3:
|
||||
return
|
||||
enum_hint: Dict[str, Any] = {"kind": "enum_codes", "codes": list(enum_codes)}
|
||||
ch = item.get("csv_hint")
|
||||
if ch is None:
|
||||
item["csv_hint"] = enum_hint
|
||||
return
|
||||
if isinstance(ch, dict):
|
||||
if ch.get("kind") == "enum_codes":
|
||||
return
|
||||
item["csv_hint"] = [enum_hint, ch]
|
||||
return
|
||||
if isinstance(ch, list):
|
||||
if any(isinstance(x, dict) and x.get("kind") == "enum_codes" for x in ch):
|
||||
return
|
||||
item["csv_hint"] = [enum_hint, *ch]
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Inyecta labels.en en definiciones de columnas TEMPLATE_COLUMNS tras cargar el dict.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .csv_headers_en import HEADER_LABEL_EN
|
||||
|
||||
|
||||
def inject_en_labels_into_columns(columns: Optional[List[Dict[str, Any]]]) -> None:
|
||||
"""Añade item['labels']['en'] cuando existe traducción para canonical."""
|
||||
if not columns:
|
||||
return
|
||||
for item in columns:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
c = item.get("canonical")
|
||||
if not c:
|
||||
continue
|
||||
en = HEADER_LABEL_EN.get(str(c).strip())
|
||||
if en:
|
||||
lab = item.setdefault("labels", {})
|
||||
lab["en"] = en
|
||||
|
||||
|
||||
def inject_en_labels_into_template_columns(template_columns: Dict[str, Optional[List[Dict[str, Any]]]]) -> None:
|
||||
"""Recorre todas las plantillas de un TEMPLATE_COLUMNS."""
|
||||
for cols in template_columns.values():
|
||||
inject_en_labels_into_columns(cols)
|
||||
160
backend/api/v1/modules/a76/layouts_csv/common/template_locale.py
Normal file
160
backend/api/v1/modules/a76/layouts_csv/common/template_locale.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Helpers for localized CSV template header labels (ES/EN) and alias registration.
|
||||
|
||||
csv_hint (opcional en cada columna TEMPLATE_COLUMNS): un dict o lista de dicts (orden importa).
|
||||
- kind "enum_codes" + codes: lista corta (≤3) mostrada como "Base (a, b, c)" en descarga.
|
||||
- kind "digits" + n: sufijo numérico, ES "(n dígitos)" / EN "(n digits)".
|
||||
- kind "max_chars" + n: máximo caracteres (modelo / validadores CSV).
|
||||
- kind "literal" + es / en: sufijo libre entre paréntesis por idioma.
|
||||
|
||||
cabeceras extendidas se registran en lookup de importación vía merge_download_header_into_lookup.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Dict, Iterator, Optional
|
||||
|
||||
CsvHintDict = Dict[str, Any]
|
||||
|
||||
|
||||
def normalize_locale(locale: Optional[str]) -> str:
|
||||
if not locale:
|
||||
return "es"
|
||||
l = str(locale).lower().strip()
|
||||
if l.startswith("en"):
|
||||
return "en"
|
||||
return "es"
|
||||
|
||||
|
||||
def display_header_for_locale(column: Dict[str, Any], locale: str) -> str:
|
||||
"""
|
||||
Pick the CSV header cell for one column definition.
|
||||
Falls back to canonical when labels are absent.
|
||||
For ES: no usar labels.en antes que canonical — si solo existe labels.en (inyectado),
|
||||
sin labels.es el CSV en español debe seguir usando el nombre canónico (Clarion), no el EN.
|
||||
"""
|
||||
loc = normalize_locale(locale)
|
||||
labels = column.get("labels")
|
||||
if isinstance(labels, dict):
|
||||
if loc == "en":
|
||||
return str(labels.get("en") or labels.get("es") or column.get("canonical") or "")
|
||||
return str(labels.get("es") or column.get("canonical") or labels.get("en") or "")
|
||||
return str(column.get("canonical") or "")
|
||||
|
||||
|
||||
def _csv_hints_iter(column: Dict[str, Any]) -> Iterator[CsvHintDict]:
|
||||
"""Siempre es generador (yield from ()) para filas sin csv_hint."""
|
||||
h = column.get("csv_hint")
|
||||
if h is None:
|
||||
yield from ()
|
||||
return
|
||||
if isinstance(h, list):
|
||||
for item in h:
|
||||
if isinstance(item, dict):
|
||||
yield item
|
||||
elif isinstance(h, dict):
|
||||
yield h
|
||||
|
||||
|
||||
def _append_suffix(text: str, suffix_body: str) -> str:
|
||||
inner = suffix_body.strip()
|
||||
if not inner:
|
||||
return text
|
||||
return f"{text} ({inner})" if text else f"({inner})"
|
||||
|
||||
|
||||
def _max_chars_suffix(n: int, locale: str) -> str:
|
||||
loc = normalize_locale(locale)
|
||||
if loc == "en":
|
||||
if n == 1:
|
||||
return "max. 1 character"
|
||||
return f"max. {n} characters"
|
||||
if n == 1:
|
||||
return "máx. 1 carácter"
|
||||
return f"máx. {n} caracteres"
|
||||
|
||||
|
||||
def _apply_one_csv_hint(text: str, hint: CsvHintDict, locale: str) -> str:
|
||||
"""Aplica un sufijo a la cabecera acumulada (display o ya con hints previos)."""
|
||||
loc = normalize_locale(locale)
|
||||
kind = hint.get("kind")
|
||||
if kind == "enum_codes":
|
||||
codes = hint.get("codes")
|
||||
if not isinstance(codes, list):
|
||||
return text
|
||||
if len(codes) == 0 or len(codes) > 3:
|
||||
return text
|
||||
inner = ", ".join(str(c) for c in codes)
|
||||
return _append_suffix(text, inner)
|
||||
if kind == "digits":
|
||||
n = hint.get("n")
|
||||
if n is None:
|
||||
return text
|
||||
try:
|
||||
ni = int(n)
|
||||
except (TypeError, ValueError):
|
||||
return text
|
||||
suf = f"{ni} digits" if loc == "en" else f"{ni} dígitos"
|
||||
return _append_suffix(text, suf)
|
||||
if kind == "max_chars":
|
||||
n = hint.get("n")
|
||||
if n is None:
|
||||
return text
|
||||
try:
|
||||
ni = int(n)
|
||||
except (TypeError, ValueError):
|
||||
return text
|
||||
if ni < 0:
|
||||
return text
|
||||
return _append_suffix(text, _max_chars_suffix(ni, loc))
|
||||
if kind == "literal":
|
||||
lit = hint.get("en") if loc == "en" else hint.get("es")
|
||||
if not lit and isinstance(hint.get("es"), str):
|
||||
lit = hint.get("es")
|
||||
if not lit and isinstance(hint.get("en"), str):
|
||||
lit = hint.get("en")
|
||||
if lit:
|
||||
return _append_suffix(text, str(lit))
|
||||
return text
|
||||
return text
|
||||
|
||||
|
||||
def download_header_cell(column: Dict[str, Any], locale: Optional[str] = None) -> str:
|
||||
"""
|
||||
Texto final de la celda de cabecera en plantilla descargada (incl. sufijos de ayuda).
|
||||
csv_hint puede ser un dict o lista de dicts (orden: p. ej. enum_codes luego max_chars).
|
||||
enum_codes solo si len(codes) ≤ 3.
|
||||
"""
|
||||
loc = normalize_locale(locale)
|
||||
text = display_header_for_locale(column, loc)
|
||||
for hint in _csv_hints_iter(column):
|
||||
text = _apply_one_csv_hint(text, hint, loc)
|
||||
return text
|
||||
|
||||
|
||||
def merge_download_header_into_lookup(
|
||||
item: Dict[str, Any],
|
||||
canonical: str,
|
||||
normalize_header_fn: Callable[[str], str],
|
||||
lookup: Dict[str, str],
|
||||
) -> None:
|
||||
"""Registra cabeceras de descarga ES/EN (con sufijos csv_hint) como alias del canonical."""
|
||||
for loc in ("es", "en"):
|
||||
cell = download_header_cell(item, loc)
|
||||
if cell:
|
||||
lookup[normalize_header_fn(cell)] = canonical
|
||||
|
||||
|
||||
def merge_labels_into_lookup(
|
||||
item: Dict[str, Any],
|
||||
canonical: str,
|
||||
normalize_header_fn: Callable[[str], str],
|
||||
lookup: Dict[str, str],
|
||||
) -> None:
|
||||
"""Register labels.es / labels.en as extra normalized aliases for canonical."""
|
||||
labels = item.get("labels")
|
||||
if not isinstance(labels, dict):
|
||||
return
|
||||
for key in ("es", "en"):
|
||||
lab = labels.get(key)
|
||||
if lab:
|
||||
lookup[normalize_header_fn(str(lab))] = canonical
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Máximos y enums para cabeceras CSV agentes aduanales (validadores / mappers)."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.mappers import MAX_LEN
|
||||
|
||||
CUSTOMS_BROKERS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"CLAVE": MAX_LEN["broker_key"],
|
||||
"LICENCIA": MAX_LEN["license"],
|
||||
"NOMBRE": MAX_LEN["name"],
|
||||
"RFC": MAX_LEN["tax_id"],
|
||||
"DIRECCION": MAX_LEN["address"],
|
||||
"CODIGO POSTAL": MAX_LEN["postal_code"],
|
||||
"CIUDAD": MAX_LEN["city"],
|
||||
"ESTADO": MAX_LEN["state"],
|
||||
"PAIS": MAX_LEN["country"],
|
||||
"TELEFONO": MAX_LEN["phone"],
|
||||
"FAX": MAX_LEN["fax"],
|
||||
"EMAIL": MAX_LEN["email"],
|
||||
"PERSONAL_ID": MAX_LEN["personal_id"],
|
||||
"POSICION": MAX_LEN["position"],
|
||||
"EMPRESA": MAX_LEN["company"],
|
||||
"CONTACTO": MAX_LEN["contact"],
|
||||
}
|
||||
@@ -9,6 +9,10 @@ Solo se leen columnas definidas aquí; el resto se ignora.
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns, merge_enum_hint_before_max
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import CUSTOMS_BROKERS_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"customs_brokers": [
|
||||
@@ -49,6 +53,15 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(
|
||||
TEMPLATE_COLUMNS.get("customs_brokers"), CUSTOMS_BROKERS_CSV_MAX_CHARS
|
||||
)
|
||||
for _col in TEMPLATE_COLUMNS.get("customs_brokers") or []:
|
||||
if _col.get("canonical") == "TIPO":
|
||||
merge_enum_hint_before_max(_col, ["MEX", "AME"])
|
||||
|
||||
# Orden oficial de columnas (para CSV sin encabezado o detección)
|
||||
CUSTOMS_BROKERS_FIELDNAMES_ORDER = [
|
||||
"TIPO",
|
||||
@@ -84,6 +97,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
for idx, name in enumerate(CUSTOMS_BROKERS_FIELDNAMES_ORDER):
|
||||
lookup[normalize_header_fn(f"_COL_{idx}_")] = name
|
||||
return lookup
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
# common validators, mappers, fk_loader for drivers CSV import
|
||||
from .fk_loader import load_drivers_fk_sets
|
||||
|
||||
__all__ = ["load_drivers_fk_sets"]
|
||||
"""Drivers CSV helpers (validators, FK loader). Import submodules explicitly to avoid heavy deps at package init."""
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Límites alineados a drivers/common/common_validators.MAX_LEN (canonical CSV)."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.common_validators import MAX_LEN as D_MAX
|
||||
|
||||
DRIVERS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"TRANSPORTISTA": D_MAX["transporter_key"],
|
||||
"LINEA": 30,
|
||||
"CLAVE CONDUCTOR": D_MAX["driver_name"],
|
||||
"LICENCIA": D_MAX["license_number"],
|
||||
"PERMISO LINEA EXPRESS": D_MAX["express_line_id"],
|
||||
"IDENTIFICACION ACE": D_MAX["ace_id"],
|
||||
"FECHA NACIMIENTO": 30,
|
||||
"SEXO": D_MAX["gender"],
|
||||
"PAIS NACIMIENTO": D_MAX["birth_country"],
|
||||
"TRANSPORTA MAT. PELIGROSO?": D_MAX["hazardous_material_auth"],
|
||||
"PERMISO MAT. PELIGROSO": D_MAX["hazardous_material_state"],
|
||||
"NOMBRE(S)": D_MAX["first_name"],
|
||||
"APELLIDO PATERNO": D_MAX["last_name"],
|
||||
"FORMA IDENTIFICACION 1": D_MAX["id_key1"],
|
||||
"NUM. IDENTIFICACION 1": D_MAX["id_number1"],
|
||||
"ESTADO": D_MAX["id_state1"],
|
||||
"PAIS": D_MAX["id_country1"],
|
||||
"FORMA IDENTIFICACION 2": D_MAX["id_key2"],
|
||||
"NUM. IDENTIFICACION 2": D_MAX["id_number2"],
|
||||
"ESTADO 2": D_MAX["id_state2"],
|
||||
"PAIS 2": D_MAX["id_country2"],
|
||||
}
|
||||
@@ -5,6 +5,10 @@ Configuracion de plantilla CSV para Conductores (EstructuraCatConductor.xls).
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns, merge_enum_hint_before_max
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import DRIVERS_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"drivers": [
|
||||
@@ -33,6 +37,16 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(TEMPLATE_COLUMNS.get("drivers"), DRIVERS_CSV_MAX_CHARS)
|
||||
for _dc in TEMPLATE_COLUMNS.get("drivers") or []:
|
||||
c = _dc.get("canonical")
|
||||
if c == "SEXO":
|
||||
merge_enum_hint_before_max(_dc, ["M", "F"])
|
||||
elif c == "TRANSPORTA MAT. PELIGROSO?":
|
||||
merge_enum_hint_before_max(_dc, ["SI", "NO"])
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("drivers")
|
||||
@@ -44,6 +58,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Límites cabecera plantilla tipos de cambio."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.common_validators import CURRENCY_MAX
|
||||
|
||||
EXCHANGE_RATE_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"FECHA": 30,
|
||||
"VALOR": 24,
|
||||
"MONEDA_LOCAL": CURRENCY_MAX,
|
||||
"MONEDA_EXTRANJERA": CURRENCY_MAX,
|
||||
}
|
||||
@@ -6,6 +6,10 @@ Solo se leen columnas definidas aquí; el resto se ignora.
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import EXCHANGE_RATE_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"exchange_rates": [
|
||||
@@ -16,6 +20,12 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(
|
||||
TEMPLATE_COLUMNS.get("exchange_rates"), EXCHANGE_RATE_CSV_MAX_CHARS
|
||||
)
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn, template_id: str = "exchange_rates") -> Dict[str, str]:
|
||||
"""normalized_header -> canonical_name para plantilla exchange_rates."""
|
||||
@@ -28,6 +38,8 @@ def build_normalized_lookup(normalize_header_fn, template_id: str = "exchange_ra
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ Misma estructura que facturas exp_def_header / exp_def_details; módulo autocont
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from ..facturas.invoice_csv_column_hints import apply_invoice_csv_hints
|
||||
|
||||
# Columnas para encabezado y partidas de exportación (EstructuraEncFacExpoCamReg / EstructuraParExpoCamReg)
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
@@ -101,6 +104,10 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
apply_invoice_csv_hints(TEMPLATE_COLUMNS)
|
||||
|
||||
|
||||
def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
return TEMPLATE_COLUMNS.get(template_id)
|
||||
@@ -117,6 +124,8 @@ def build_normalized_lookup(template_id: str, normalize_header_fn) -> Dict[str,
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Hints de cabecera para plantillas de factura (longitudes conservadoras + enums ≤3).
|
||||
|
||||
Valores orientativos respecto a validación en tasks.py y modelos de factura; afinar si cambian ORM.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns, merge_enum_hint_before_max
|
||||
|
||||
# Unión de canónicos frecuentes en encabezados y partidas (multiplantilla)
|
||||
INVOICE_MAX_CHARS_BY_CANONICAL: Dict[str, int] = {
|
||||
"NUMERO FACTURA": 100,
|
||||
"FECHA FACTURA": 30,
|
||||
"FECHA EMISION": 30,
|
||||
"TIPO DE CAMBIO": 24,
|
||||
"REGIMEN": 10,
|
||||
"PEDIMENTO": 30,
|
||||
"REMESA": 12,
|
||||
"CLAVE PROVEEDOR": 40,
|
||||
"CLAVE VENDIDO A": 40,
|
||||
"CLAVE ENVIADO A": 40,
|
||||
"AGENTE ADUANAL": 20,
|
||||
"CLAVE TRANSPORTISTA": 30,
|
||||
"NOMBRE CONDUCTOR": 80,
|
||||
"TIPO TRANSPORTE": 10,
|
||||
"CLAVE TRANSPORTE": 30,
|
||||
"NUMERO CAJA": 20,
|
||||
"NUMERO TRANSPORTE": 30,
|
||||
"TIPO MONEDA": 10,
|
||||
"CLAVE MONEDA": 10,
|
||||
"FLETES": 24,
|
||||
"VALOR SEGUROS": 24,
|
||||
"SEGUROS": 24,
|
||||
"EMBALAJES": 24,
|
||||
"OTROS INCREMENTABLES": 24,
|
||||
"CLAVE INCOTERM": 10,
|
||||
"PRECINTO": 49,
|
||||
"TIPO PESO": 10,
|
||||
"E DOCUMENT": 5,
|
||||
"NUM OPERACION": 30,
|
||||
"ADUANA DE CRUCE": 80,
|
||||
"OBSERVACIONES E": 500,
|
||||
"OBSERVACIONES I": 500,
|
||||
"FACTURA ALTERNA": 99,
|
||||
"NUM PROYECTO": 14,
|
||||
"ORDEN COMPRA": 50,
|
||||
"FACTURA EXPO REF": 19,
|
||||
"MANIFIESTO": 50,
|
||||
"ENVIADO POR": 80,
|
||||
"LINEA": 10,
|
||||
"CLASE": 20,
|
||||
"CANTIDAD IMPORTADA": 24,
|
||||
"UNIDAD DE MEDIDA": 10,
|
||||
"COSTO UNITARIO": 24,
|
||||
"PESO NETO": 24,
|
||||
"PESO BRUTO": 24,
|
||||
"CANTIDAD BULTOS": 24,
|
||||
"CLAVE BULTOS": 20,
|
||||
"PAIS ORIGEN": 3,
|
||||
"FRACCION ARANCELARIA": 20,
|
||||
"PREFERENCIA ARANCELARIA": 10,
|
||||
"SECTOR": 20,
|
||||
"FRACCION AMERICANA": 20,
|
||||
"ORDEN DE COMPRA": 50,
|
||||
"DESCRIPCION ESPAÑOL": 256,
|
||||
"DESCRIPCION INGLES": 256,
|
||||
"MARCA": 40,
|
||||
"MODELO": 40,
|
||||
"NUM. PARTE": 70,
|
||||
"SE PAGO IMPUESTO": 10,
|
||||
"FORMA DE PAGO": 30,
|
||||
"METODO DE VALORACION": 30,
|
||||
"DESCRIPCION EXTRA": 256,
|
||||
"INFORMACION ADICIONAL": 500,
|
||||
"AGREGAR/SUSTITUIR": 10,
|
||||
"TOTAL": 24,
|
||||
"NUMERO ENTRADA": 30,
|
||||
"LOTE": 40,
|
||||
"ID TYPE": 20,
|
||||
"NUMERO FACTURA EXPO": 100,
|
||||
"LINEA EXPO": 10,
|
||||
"TIPO DE IMPO": 10,
|
||||
"FACTURA IMPO": 100,
|
||||
"LINEA IMPO": 10,
|
||||
"GENERA DESCARGA": 5,
|
||||
"CANTIDAD EXPORTADA/DESCARGAR": 24,
|
||||
"AGREGAR(A)/SUSTITUIR(S)": 12,
|
||||
"ES PARTIDA O SUBPARTIDA": 10,
|
||||
"ES PARTIDA/SUBPARTIDA": 10,
|
||||
"LINEA FACTURA": 10,
|
||||
"LINEA SERIE": 10,
|
||||
"SERIE": 40,
|
||||
"NUM PARTE": 70,
|
||||
"SUB MODELO": 40,
|
||||
"NUMERO ID": 40,
|
||||
}
|
||||
|
||||
|
||||
def apply_invoice_csv_hints(template_columns: Dict[str, Optional[List[Dict[str, Any]]]]) -> None:
|
||||
"""Inyecta max_chars y enums en todas las listas de columnas no nulas."""
|
||||
for _tid, cols in template_columns.items():
|
||||
if not cols:
|
||||
continue
|
||||
inject_max_char_hints_into_columns(cols, INVOICE_MAX_CHARS_BY_CANONICAL)
|
||||
for item in cols:
|
||||
c = item.get("canonical")
|
||||
if c == "E DOCUMENT":
|
||||
merge_enum_hint_before_max(item, ["S", "N"])
|
||||
elif c in ("SE PAGO IMPUESTO",):
|
||||
merge_enum_hint_before_max(item, ["SI", "NO"])
|
||||
elif c == "GENERA DESCARGA":
|
||||
merge_enum_hint_before_max(item, ["SI", "NO"])
|
||||
elif c == "TIPO DE IMPO":
|
||||
merge_enum_hint_before_max(item, ["I", "E"])
|
||||
@@ -7,6 +7,9 @@ Solo se escribe en BD lo que los modelos de facturas aceptan (respetando models)
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .invoice_csv_column_hints import apply_invoice_csv_hints
|
||||
|
||||
# Cada plantilla define sus columnas canónicas y alias (otros nombres que aceptamos en el CSV).
|
||||
# canonical = nombre estándar con el que trabajamos internamente; debe coincidir con lo que
|
||||
@@ -266,6 +269,10 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
apply_invoice_csv_hints(TEMPLATE_COLUMNS)
|
||||
|
||||
|
||||
def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
cols = TEMPLATE_COLUMNS.get(template_id)
|
||||
@@ -300,6 +307,8 @@ def build_normalized_lookup(template_id: str, normalize_header_fn) -> Dict[str,
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Máximos alineados a parts/validators/common.py (canonical interno)."""
|
||||
from typing import Dict
|
||||
|
||||
PART_NUMBERS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"NUMPARTE": 70,
|
||||
"NUMPARTECOM": 70,
|
||||
"DESCRIPCIONE": 256,
|
||||
"DESCRIPCIONI": 256,
|
||||
"CLASE": 20,
|
||||
"UNIMED": 5,
|
||||
"COSTOUNIT": 24,
|
||||
"TIPOMONEDA": 3,
|
||||
"CLAVEMONEDA": 3,
|
||||
"PESOUNIT": 24,
|
||||
"TIPOPESO": 6,
|
||||
"FRACCION": 10,
|
||||
"FRACCIONAME": 10,
|
||||
"PAIS": 3,
|
||||
"PREFERENCIA": 7,
|
||||
"SECTOR": 5,
|
||||
"RUTAIMAGEN": 255,
|
||||
"FDAKEY": 30,
|
||||
"FCCKEY": 30,
|
||||
"LICENCIA": 30,
|
||||
"ECCN": 20,
|
||||
"EXPORTCODE": 20,
|
||||
"EXCLUSION": 20,
|
||||
"ACTIVO": 5,
|
||||
}
|
||||
@@ -8,10 +8,19 @@ from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common import csv_reader as common_csv_reader
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import (
|
||||
download_header_cell,
|
||||
merge_download_header_into_lookup,
|
||||
merge_labels_into_lookup,
|
||||
normalize_locale,
|
||||
)
|
||||
from .csv_max_chars_for_headers import PART_NUMBERS_CSV_MAX_CHARS
|
||||
|
||||
|
||||
# Valores que indican que la primera fila es cabecera (primera columna normalizada)
|
||||
FIRST_COLUMN_HEADER_VALUES = ("NUMERO DE PARTE", "NUMPARTE")
|
||||
FIRST_COLUMN_HEADER_VALUES = ("NUMERO DE PARTE", "NUMPARTE", "PART NUMBER")
|
||||
|
||||
|
||||
def detect_headers_or_data(
|
||||
@@ -76,6 +85,27 @@ TEMPLATE_DOWNLOAD_HEADERS: List[str] = [
|
||||
"RUTA DE LA IMAGEN",
|
||||
]
|
||||
|
||||
# Misma longitud que TEMPLATE_DOWNLOAD_HEADERS; columnas I/J vacías.
|
||||
TEMPLATE_DOWNLOAD_HEADERS_EN: List[str] = [
|
||||
"PART NUMBER",
|
||||
"DESCRIPTION (SPANISH)",
|
||||
"DESCRIPTION (ENGLISH)",
|
||||
"CLASS",
|
||||
"COMMERCIAL UNIT OF MEASURE",
|
||||
"UNIT COST",
|
||||
"COST CURRENCY TYPE",
|
||||
"CURRENCY CODE",
|
||||
"",
|
||||
"",
|
||||
"UNIT WEIGHT",
|
||||
"WEIGHT TYPE",
|
||||
"FRACTION",
|
||||
"COUNTRY",
|
||||
"PREFERENCE",
|
||||
"SECTOR",
|
||||
"IMAGE PATH",
|
||||
]
|
||||
|
||||
# Para lectura cuando la primera fila es dato (sin cabecera): nombres únicos para columnas I y J
|
||||
TEMPLATE_FIELDNAMES_FOR_READING: List[str] = [
|
||||
"NUMERO DE PARTE",
|
||||
@@ -126,6 +156,53 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(
|
||||
TEMPLATE_COLUMNS.get("part_numbers"), PART_NUMBERS_CSV_MAX_CHARS
|
||||
)
|
||||
|
||||
_PART_ORDER_DOWNLOAD = [
|
||||
"NUMPARTE",
|
||||
"DESCRIPCIONE",
|
||||
"DESCRIPCIONI",
|
||||
"CLASE",
|
||||
"UNIMED",
|
||||
"COSTOUNIT",
|
||||
"TIPOMONEDA",
|
||||
"CLAVEMONEDA",
|
||||
None,
|
||||
None,
|
||||
"PESOUNIT",
|
||||
"TIPOPESO",
|
||||
"FRACCION",
|
||||
"PAIS",
|
||||
"PREFERENCIA",
|
||||
"SECTOR",
|
||||
"RUTAIMAGEN",
|
||||
]
|
||||
|
||||
_PN_BY_CANON = {c["canonical"]: c for c in (TEMPLATE_COLUMNS.get("part_numbers") or [])}
|
||||
|
||||
|
||||
def _parts_download_column_defs() -> List[Dict[str, Any]]:
|
||||
out: List[Dict[str, Any]] = []
|
||||
for key in _PART_ORDER_DOWNLOAD:
|
||||
if key is None:
|
||||
out.append({"canonical": ""})
|
||||
else:
|
||||
base = _PN_BY_CANON.get(key)
|
||||
out.append(dict(base) if base else {"canonical": key})
|
||||
return out
|
||||
|
||||
|
||||
_PARTS_DOWNLOAD_DEFS = _parts_download_column_defs()
|
||||
|
||||
|
||||
def download_headers_for_locale(locale: str) -> List[str]:
|
||||
loc = normalize_locale(locale)
|
||||
return [download_header_cell(d, loc) for d in _PARTS_DOWNLOAD_DEFS]
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("part_numbers")
|
||||
@@ -137,6 +214,16 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
# Plantilla EN: mismas posiciones que TEMPLATE_DOWNLOAD_HEADERS
|
||||
for es_h, en_h in zip(TEMPLATE_DOWNLOAD_HEADERS, TEMPLATE_DOWNLOAD_HEADERS_EN):
|
||||
if not es_h or not en_h:
|
||||
continue
|
||||
es_key = normalize_header_fn(es_h)
|
||||
canon = lookup.get(es_key)
|
||||
if canon:
|
||||
lookup[normalize_header_fn(en_h)] = canon
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Hints cabecera CSV pedimentos (dígitos col Clarion, enums acotados, longitudes típicas)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..common.csv_hint_inject import (
|
||||
append_csv_hint,
|
||||
inject_max_char_hints_into_columns,
|
||||
merge_enum_hint_before_max,
|
||||
)
|
||||
|
||||
# Cols 1–3: solo dígitos con longitud fija
|
||||
_PEDIMENTOS_DIGITS: Dict[str, int] = {
|
||||
"AÑO": 2,
|
||||
"PATENTE": 4,
|
||||
"NUMERO": 7,
|
||||
}
|
||||
|
||||
# Resto de campos con tope razonable (validadores/modelos variados)
|
||||
_PEDIMENTOS_MAX_CHARS: Dict[str, int] = {
|
||||
"TIPO_OPERACION": 8,
|
||||
"CLAVE_PEDIMENTO": 30,
|
||||
"REGIMEN": 10,
|
||||
"FECHA_INICIO": 30,
|
||||
"FECHA_FINAL": 30,
|
||||
"FECHA_PAGO": 30,
|
||||
"ADUANA_SECCION_CRUCE": 120,
|
||||
"ACUSE_ELECTRONICO": 80,
|
||||
"INDIVIDUAL_CONSOLIDADO": 10,
|
||||
"MET_TRANSP_ENTRADA": 30,
|
||||
"MET_TRANSP_ARRIVO": 30,
|
||||
"MET_TRANSP_SALIDA": 30,
|
||||
"PEDIMENTO": 25,
|
||||
"CLIENTE_SHORT_NAME": 30,
|
||||
"TIPO_PEDIMENTO": 20,
|
||||
"OBSERVACIONES": 500,
|
||||
}
|
||||
|
||||
|
||||
def apply_pedimentos_csv_hints(columns: Optional[List[Dict[str, Any]]]) -> None:
|
||||
if not columns:
|
||||
return
|
||||
inject_max_char_hints_into_columns(columns, _PEDIMENTOS_MAX_CHARS)
|
||||
for item in columns:
|
||||
canon = item.get("canonical")
|
||||
if not canon:
|
||||
continue
|
||||
nd = _PEDIMENTOS_DIGITS.get(str(canon).strip())
|
||||
if nd is not None:
|
||||
append_csv_hint(item, {"kind": "digits", "n": nd})
|
||||
if canon == "TIPO_OPERACION":
|
||||
merge_enum_hint_before_max(item, ["I", "E"])
|
||||
elif canon == "PAGO_IMPUESTO":
|
||||
merge_enum_hint_before_max(item, ["S", "N"])
|
||||
elif canon == "ES_MIXTO":
|
||||
merge_enum_hint_before_max(item, ["SI", "NO"])
|
||||
elif canon == "INDIVIDUAL_CONSOLIDADO":
|
||||
merge_enum_hint_before_max(item, ["IND", "CON"])
|
||||
@@ -10,6 +10,9 @@ from typing import Dict, List, Any, Optional, Tuple
|
||||
# Convierte valor de celda a str; si es lista (p. ej. CSV con columnas duplicadas), toma el primer elemento.
|
||||
# Re-exportado desde common para uso en validators; ver layouts_csv.common.cell_value.
|
||||
from ..common.cell_value import cell_to_str as _cell_to_str
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .pedimentos_csv_hints import apply_pedimentos_csv_hints
|
||||
from ..common import csv_reader as common_csv_reader
|
||||
|
||||
|
||||
@@ -116,6 +119,10 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
apply_pedimentos_csv_hints(TEMPLATE_COLUMNS.get("pedimentos"))
|
||||
|
||||
# Orden de columnas para CSV sin cabecera (primera fila = datos). Usado por detect_headers_or_data.
|
||||
PEDIMENTOS_TEMPLATE_ORDER: List[str] = [
|
||||
item["canonical"] for item in TEMPLATE_COLUMNS["pedimentos"]
|
||||
@@ -190,6 +197,8 @@ def build_normalized_lookup(normalize_header_fn, template_id: str = "pedimentos"
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Límites alineados a trailers/common/common_validators.MAX_LEN."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.common_validators import MAX_LEN as T_MAX
|
||||
|
||||
TRAILERS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"NUMERO TRAILER": T_MAX["trailer_number"],
|
||||
"CLAVE ACE": T_MAX["ace_trailer_number"],
|
||||
"TIPO TRAILER": T_MAX["trailer_type_key"],
|
||||
"PRECINTO": T_MAX["seal"],
|
||||
"CODIGO ENTIDAD": T_MAX["entity_code"],
|
||||
"PLACAS": T_MAX["plate_number"],
|
||||
"ESTADO": T_MAX["state"],
|
||||
"PAIS": T_MAX["country"],
|
||||
"CLAVE CONTENEDOR": T_MAX["container_key"],
|
||||
}
|
||||
@@ -6,6 +6,10 @@ Mapeo: NUMERO TRAILER → trailer_number, CLAVE ACE → ace_trailer_number, etc.
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import TRAILERS_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"trailers": [
|
||||
@@ -22,6 +26,10 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(TEMPLATE_COLUMNS.get("trailers"), TRAILERS_CSV_MAX_CHARS)
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("trailers")
|
||||
@@ -33,6 +41,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Límites alineados a transportistas/common/common_validators.MAX_LEN."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.common_validators import MAX_LEN as TR_MAX
|
||||
|
||||
TRANSPORTISTAS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"CLAVE TRANSPORTISTA": TR_MAX["transporter_key"],
|
||||
"NOMBRE": TR_MAX["name"],
|
||||
"NOMBRE CORTO": TR_MAX["short_name"],
|
||||
"RESPONSABLE": TR_MAX["responsible"],
|
||||
"RFC": TR_MAX["rfc"],
|
||||
"CALLES": TR_MAX["streets"],
|
||||
"CODIGO POSTAL": TR_MAX["postal_code"],
|
||||
"CIUDAD": TR_MAX["city"],
|
||||
"ESTADO": TR_MAX["state"],
|
||||
"PAIS": TR_MAX["country"],
|
||||
"CODIGO CARGADOR": TR_MAX["loader_code"],
|
||||
"CODIGO CAAT": TR_MAX["caat_code"],
|
||||
"CODIGO TRANS": TR_MAX["transport_code"],
|
||||
"TIPO INTERFASE TRANS": TR_MAX["transport_interface_type"],
|
||||
"SERVIDOR FTP": TR_MAX["ftp_server"],
|
||||
"USUARIO FTP": TR_MAX["ftp_user"],
|
||||
"CLAVE ACCESO FTP": TR_MAX["ftp_password"],
|
||||
"DIRECTORIO FTP": TR_MAX["ftp_directory"],
|
||||
}
|
||||
@@ -6,6 +6,10 @@ Mapeo Clarion: Col A = CLAVE TRANSPORTISTA, B = NOMBRE, ... R = DIRECTORIO FTP,
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import TRANSPORTISTAS_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"transporters": [
|
||||
@@ -31,6 +35,12 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(
|
||||
TEMPLATE_COLUMNS.get("transporters"), TRANSPORTISTAS_CSV_MAX_CHARS
|
||||
)
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("transporters")
|
||||
@@ -42,6 +52,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Fracción americana: common_validators CODE_MAX, etc."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.common_validators import CODE_MAX, PREFIX_MAX, UNIT_MAX, TYPE_MAX
|
||||
|
||||
US_TARIFF_FRACTIONS_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"FRACCION_ARANCELARIA": CODE_MAX,
|
||||
"PREFIJO": PREFIX_MAX,
|
||||
"UNIDAD_DE_MEDIDA": UNIT_MAX,
|
||||
"DESCRIPCION": 500,
|
||||
"TIPO_DE_ADVALOREM": TYPE_MAX,
|
||||
"ADVALOREM_PCT": 24,
|
||||
"ADVALOREM_DLLS": 24,
|
||||
}
|
||||
@@ -6,6 +6,10 @@ Solo se leen columnas definidas aquí; el resto se ignora.
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from ..common.cell_value import cell_to_str as _cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns, merge_enum_hint_before_max
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import US_TARIFF_FRACTIONS_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"us_tariff_fractions": [
|
||||
@@ -20,6 +24,15 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(
|
||||
TEMPLATE_COLUMNS.get("us_tariff_fractions"), US_TARIFF_FRACTIONS_CSV_MAX_CHARS
|
||||
)
|
||||
for _uc in TEMPLATE_COLUMNS.get("us_tariff_fractions") or []:
|
||||
if _uc.get("canonical") == "TIPO_DE_ADVALOREM":
|
||||
merge_enum_hint_before_max(_uc, ["PO", "ME"])
|
||||
|
||||
# Orden A..H para detectar desfase en 8ª columna (raw_row.values()[7])
|
||||
TEMPLATE_ORDER = [item["canonical"] for item in TEMPLATE_COLUMNS["us_tariff_fractions"]]
|
||||
DESFASE_COLUMN_INDEX = 7
|
||||
@@ -36,6 +49,8 @@ def build_normalized_lookup(normalize_header_fn, template_id: str = "us_tariff_f
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Límites de cabecera alineados a vehicles/common/common_validators.MAX_LEN."""
|
||||
from typing import Dict
|
||||
|
||||
from .common.common_validators import MAX_LEN as V_MAX
|
||||
|
||||
VEHICLES_CSV_MAX_CHARS: Dict[str, int] = {
|
||||
"CLAVE": V_MAX["vehicle_key"],
|
||||
"CLAVE ACE": V_MAX["ace_vehicle_key"],
|
||||
"CLAVE TRANSPORTE": V_MAX["transporter_key"],
|
||||
"VIN": V_MAX["transport_identifier"],
|
||||
"TIPO TRANSPORTE": V_MAX["transport_type"],
|
||||
"CODIGO DE ENTIDAD": V_MAX["entity_code"],
|
||||
"TRANSPONDEDOR": V_MAX["transponder_number"],
|
||||
"NUMERO DOT": V_MAX["dot_number"],
|
||||
"PLACAS": V_MAX["plate_number"],
|
||||
"CIUDAD": V_MAX["city"],
|
||||
"ESTADO": V_MAX["state"],
|
||||
"PAIS": V_MAX["country"],
|
||||
"PRECINTO": V_MAX["seal"],
|
||||
"EMPRESA ASEGURADORA": V_MAX["insurance_company_name"],
|
||||
"NUM. ASEGURADORA": V_MAX["insurance_number"],
|
||||
"MONTO ASEGURADO": 24,
|
||||
"FECHA DE ASEGURADORA": 30,
|
||||
}
|
||||
@@ -6,6 +6,10 @@ Mapeo: CLAVE → vehicle_key, CLAVE ACE → ace_vehicle_key, etc.
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from ..common.cell_value import cell_to_str
|
||||
from ..common.csv_hint_inject import inject_max_char_hints_into_columns
|
||||
from ..common.inject_csv_labels_en import inject_en_labels_into_template_columns
|
||||
from ..common.template_locale import merge_download_header_into_lookup, merge_labels_into_lookup
|
||||
from .csv_max_chars_for_headers import VEHICLES_CSV_MAX_CHARS
|
||||
|
||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
"vehicles": [
|
||||
@@ -30,6 +34,10 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
inject_en_labels_into_template_columns(TEMPLATE_COLUMNS)
|
||||
|
||||
inject_max_char_hints_into_columns(TEMPLATE_COLUMNS.get("vehicles"), VEHICLES_CSV_MAX_CHARS)
|
||||
|
||||
|
||||
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
cols = TEMPLATE_COLUMNS.get("vehicles")
|
||||
@@ -41,6 +49,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||
lookup[normalize_header_fn(canonical)] = canonical
|
||||
for alias in item.get("aliases") or []:
|
||||
lookup[normalize_header_fn(alias)] = canonical
|
||||
merge_labels_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
merge_download_header_into_lookup(item, canonical, normalize_header_fn, lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
|
||||
143
backend/tests/unit/a76/layouts_csv/test_csv_header_hints.py
Normal file
143
backend/tests/unit/a76/layouts_csv/test_csv_header_hints.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Tests for CSV template header hints (inline) and import lookup aliases (stdlib unittest)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from api.v1.modules.a76.csv_templates.registry import (
|
||||
_normalize_header_for_match,
|
||||
_TEMPLATE_HEADERS,
|
||||
get_download_headers,
|
||||
)
|
||||
from api.v1.modules.a76.layouts_csv.common.template_locale import (
|
||||
download_header_cell,
|
||||
display_header_for_locale,
|
||||
)
|
||||
from api.v1.modules.a76.layouts_csv.clients_and_providers import template_config as cp_tc
|
||||
|
||||
|
||||
class TestRegistryCsvDownloadSmoke(unittest.TestCase):
|
||||
"""Smoke: cada template_id del registry tiene fila de descarga misma longitud que columnas base."""
|
||||
|
||||
def test_download_headers_length_matches_registry_for_all_templates(self):
|
||||
for tid in sorted(_TEMPLATE_HEADERS.keys()):
|
||||
row = get_download_headers(tid, "es")
|
||||
self.assertIsNotNone(row, tid)
|
||||
self.assertEqual(len(row), len(_TEMPLATE_HEADERS[tid]), tid)
|
||||
|
||||
def test_customs_brokers_lookup_resolves_decorated_tip_header(self):
|
||||
from api.v1.modules.a76.layouts_csv.customs_brokers import template_config as cb_tc
|
||||
|
||||
lookup = cb_tc.build_normalized_lookup(_normalize_header_for_match)
|
||||
hdr0 = get_download_headers("customs_brokers", "es")[0]
|
||||
self.assertEqual(lookup[_normalize_header_for_match(hdr0)], "TIPO")
|
||||
|
||||
def test_imp_temp_header_lookup_resolves_decorated_e_document(self):
|
||||
from api.v1.modules.a76.layouts_csv.facturas import template_config as ft_tc
|
||||
|
||||
lookup = ft_tc.build_normalized_lookup("imp_temp_header", _normalize_header_for_match)
|
||||
for cell in get_download_headers("imp_temp_header", "es"):
|
||||
if lookup.get(_normalize_header_for_match(cell)) == "E DOCUMENT":
|
||||
self.assertIn("(S, N)", cell)
|
||||
return
|
||||
self.fail("E DOCUMENT column missing from imp_temp_header download row")
|
||||
|
||||
def test_pedimentos_lookup_resolves_tipo_operacion_decorated(self):
|
||||
from api.v1.modules.a76.layouts_csv.pedmientos import template_config as ped_tc
|
||||
|
||||
lookup = ped_tc.build_normalized_lookup(_normalize_header_for_match, "pedimentos")
|
||||
for cell in get_download_headers("pedimentos", "es"):
|
||||
if lookup.get(_normalize_header_for_match(cell)) == "TIPO_OPERACION":
|
||||
self.assertIn("(I, E)", cell)
|
||||
return
|
||||
self.fail("TIPO_OPERACION column missing from pedimentos download row")
|
||||
|
||||
def test_material_classes_lookup_resolves_revfisica_enum_header(self):
|
||||
from api.v1.modules.a76.layouts_csv.classes import template_config as cl_tc
|
||||
|
||||
lookup = cl_tc.build_normalized_lookup(_normalize_header_for_match)
|
||||
for cell in get_download_headers("material_classes", "es"):
|
||||
if lookup.get(_normalize_header_for_match(cell)) == "REVFISICA":
|
||||
self.assertIn("(1, 0)", cell)
|
||||
return
|
||||
self.fail("REVFISICA column missing from material_classes download row")
|
||||
|
||||
|
||||
class TestCsvHeaderHints(unittest.TestCase):
|
||||
def test_download_header_cell_enum_within_limit(self):
|
||||
col = {
|
||||
"canonical": "TIPO",
|
||||
"csv_hint": {"kind": "enum_codes", "codes": ["C", "P", "A"]},
|
||||
}
|
||||
self.assertEqual(download_header_cell(col, "es"), "TIPO (C, P, A)")
|
||||
col_en = {**col, "labels": {"en": "Entity type"}}
|
||||
self.assertEqual(download_header_cell(col_en, "en"), "Entity type (C, P, A)")
|
||||
|
||||
def test_download_header_cell_enum_over_limit_ignored(self):
|
||||
col = {
|
||||
"canonical": "X",
|
||||
"csv_hint": {"kind": "enum_codes", "codes": ["a", "b", "c", "d"]},
|
||||
}
|
||||
self.assertEqual(download_header_cell(col, "es"), "X")
|
||||
|
||||
def test_download_header_cell_digits_es_en(self):
|
||||
col = {"canonical": "NUMERO", "csv_hint": {"kind": "digits", "n": 7}}
|
||||
self.assertEqual(download_header_cell(col, "es"), "NUMERO (7 dígitos)")
|
||||
self.assertEqual(download_header_cell(col, "en"), "NUMERO (7 digits)")
|
||||
|
||||
def test_clients_providers_lookup_accepts_extended_headers(self):
|
||||
lookup = cp_tc.build_normalized_lookup(_normalize_header_for_match)
|
||||
self.assertEqual(lookup[_normalize_header_for_match("PROCEDENCIA")], "PROCEDENCIA")
|
||||
self.assertEqual(
|
||||
lookup[_normalize_header_for_match("PROCEDENCIA (N, E)")],
|
||||
"PROCEDENCIA",
|
||||
)
|
||||
self.assertEqual(lookup[_normalize_header_for_match("TIPO")], "TIPO")
|
||||
self.assertEqual(lookup[_normalize_header_for_match("TIPO (C, P, A)")], "TIPO")
|
||||
self.assertEqual(
|
||||
lookup[_normalize_header_for_match("Origin (foreign/domestic) (N, E)")],
|
||||
"PROCEDENCIA",
|
||||
)
|
||||
self.assertEqual(
|
||||
lookup[_normalize_header_for_match("Entity type (C, P, A)")],
|
||||
"TIPO",
|
||||
)
|
||||
self.assertEqual(
|
||||
lookup[_normalize_header_for_match("RFC (máx. 30 caracteres)")],
|
||||
"RFC",
|
||||
)
|
||||
self.assertEqual(
|
||||
lookup[_normalize_header_for_match("SHORT_NAME (máx. 10 caracteres)")],
|
||||
"SHORT_NAME",
|
||||
)
|
||||
self.assertEqual(
|
||||
lookup[_normalize_header_for_match("Short code (max. 10 characters)")],
|
||||
"SHORT_NAME",
|
||||
)
|
||||
|
||||
def test_download_header_cell_max_chars(self):
|
||||
col = {"canonical": "RFC", "csv_hint": {"kind": "max_chars", "n": 30}}
|
||||
self.assertEqual(download_header_cell(col, "es"), "RFC (máx. 30 caracteres)")
|
||||
self.assertEqual(download_header_cell(col, "en"), "RFC (max. 30 characters)")
|
||||
col_one = {"canonical": "X", "csv_hint": {"kind": "max_chars", "n": 1}}
|
||||
self.assertEqual(download_header_cell(col_one, "es"), "X (máx. 1 carácter)")
|
||||
self.assertEqual(download_header_cell(col_one, "en"), "X (max. 1 character)")
|
||||
|
||||
def test_download_header_cell_enum_then_max_chars(self):
|
||||
col = {
|
||||
"canonical": "TIPO",
|
||||
"csv_hint": [
|
||||
{"kind": "enum_codes", "codes": ["C", "P", "A"]},
|
||||
{"kind": "max_chars", "n": 1},
|
||||
],
|
||||
}
|
||||
self.assertEqual(download_header_cell(col, "es"), "TIPO (C, P, A) (máx. 1 carácter)")
|
||||
|
||||
def test_display_header_unchanged_without_hint(self):
|
||||
col = {"canonical": "ZZZ_NO_SCHEMA_LEN"}
|
||||
self.assertEqual(
|
||||
download_header_cell(col, "es"),
|
||||
display_header_for_locale(col, "es"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -103,6 +103,11 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"dashboard": "Dashboard",
|
||||
"management_label": "Management",
|
||||
"bulk_upload": {
|
||||
"title": "Bulk uploads",
|
||||
"entry": "CSV import"
|
||||
},
|
||||
"reference_data": {
|
||||
"title": "Fixed Catalogs",
|
||||
"codes_pedimento_regimen": "Pedimento and Regime Codes",
|
||||
@@ -1601,5 +1606,198 @@
|
||||
"cancel": "Cancel",
|
||||
"accept": "Accept"
|
||||
}
|
||||
},
|
||||
"csv_upload": {
|
||||
"page_title": "CSV import",
|
||||
"intro_help": "Left-click: upload CSV file. Right-click: download template.",
|
||||
"tab_catalogos": "Catalogs",
|
||||
"tab_transportes": "Transportation",
|
||||
"tab_importacion": "Import",
|
||||
"tab_exportacion": "Export",
|
||||
"section_catalogs": "General Catalogs",
|
||||
"section_transport": "Transportation",
|
||||
"section_import": "Import operations",
|
||||
"section_export": "Export operations",
|
||||
"params_header": "Global parameters",
|
||||
"config_prefix": "Settings",
|
||||
"soon": "Coming soon",
|
||||
"drop_here": "Drop the file!",
|
||||
"groups": {
|
||||
"permisos": "Permissions",
|
||||
"impo_temp": "Temporary import",
|
||||
"impo_def": "Definitive import",
|
||||
"cmex": "Mexican purchases",
|
||||
"expo_def": "Definitive export / regime change",
|
||||
"expo_rep": "Export replenishment",
|
||||
"manifest": "Manifest"
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"material_classes": "Classes",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"items": "Lines (permissions)",
|
||||
"headers": "Headers (permissions)",
|
||||
"historical_fractions": "Historical tariff fractions",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Carriers",
|
||||
"transports": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Header",
|
||||
"imp_temp_details": "Lines",
|
||||
"imp_temp_series": "Serial numbers",
|
||||
"imp_def_header": "Header",
|
||||
"imp_def_details": "Lines",
|
||||
"imp_def_series": "Serial numbers",
|
||||
"comp_mex_header": "Header",
|
||||
"comp_mex_details": "Lines",
|
||||
"comp_mex_series": "Serial numbers",
|
||||
"exp_def_header": "Header",
|
||||
"exp_def_details": "Lines",
|
||||
"exp_def_series": "Serial numbers",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Header",
|
||||
"exp_rep_details": "Lines",
|
||||
"exp_rep_series": "Serial numbers",
|
||||
"manifest_header": "Header"
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Load mode",
|
||||
"date_format": "Date format",
|
||||
"weight_unit": "Weight unit",
|
||||
"autonumber_series": "Autonumber lines/series",
|
||||
"load_subpartidas": "Load sub-lines",
|
||||
"recalculate_pedimento_date": "Recalculate pedimento date",
|
||||
"autonumber_remesas": "Autonumber consignments",
|
||||
"recalculate_dates": "Recalculate dates",
|
||||
"invoice_type": "Invoice type",
|
||||
"is_regime_change": "Regime change"
|
||||
},
|
||||
"options": {
|
||||
"update": "Update",
|
||||
"replace": "Replace",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"kgs": "Kilograms (kg)",
|
||||
"lbs": "Pounds (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL"
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Uploading CSV file",
|
||||
"scan": "Validating records on the server",
|
||||
"commit": "Saving records to the database",
|
||||
"upload_known": "Uploading file…",
|
||||
"upload_unknown": "Uploading file (unknown size in browser)…",
|
||||
"in_progress": "In progress…",
|
||||
"resume_hint": "Resuming import saved in this tab…",
|
||||
"rows_file": "File: ~{n} data row(s) — uploading (not yet validated on server)…",
|
||||
"rows_scan": "Records processed: {current} / {total}",
|
||||
"rows_commit": "Records saved: {current} / {total}",
|
||||
"rows_commit_fallback": "Saving to database… ({current} / {total} using last known total)"
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Invalid format. Only .csv files are allowed.",
|
||||
"download_loading": "Downloading template…",
|
||||
"download_ok": "Template downloaded.",
|
||||
"download_err": "Could not download the template.",
|
||||
"upload_err": "Could not upload the file.",
|
||||
"upload_err_generic": "Unexpected error uploading the file.",
|
||||
"scan_done": "Scan complete. Review the results.",
|
||||
"import_done": "Import completed. Review the record list.",
|
||||
"import_maybe_done": "Import may have completed. Review the record list.",
|
||||
"stale_job": "This import is no longer available (session expired or job removed). You can start a new upload.",
|
||||
"poll_err": "Could not fetch status",
|
||||
"commit_err": "Could not start import",
|
||||
"scan_alt": "Scan finished. If you do not see the modal, check the record list.",
|
||||
"finished_none": "No records inserted. Review the errors below.",
|
||||
"commit_warning_ok": "{inserted} inserted, {updated} updated. {skipped} rejected.",
|
||||
"commit_warning_none": "No records inserted or updated. {skipped} rejected.",
|
||||
"success_counts": "Import completed: {msg}",
|
||||
"warn_skipped": "{n} records rejected or skipped",
|
||||
"error_processing": "Processing error: {msg}",
|
||||
"n_inserted": "{n} inserted",
|
||||
"n_updated": "{n} updated",
|
||||
"err_fetch_scan_result": "Could not fetch the scan result. Check the results modal.",
|
||||
"err_unknown": "Unknown error",
|
||||
"err_processing_fallback": "Processing error. Check the modal or details."
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pending",
|
||||
"title": "Imports pending confirmation",
|
||||
"description": "Scans ready to save to the database. Expired jobs disappear when you refresh.",
|
||||
"refresh": "Refresh",
|
||||
"empty": "No pending imports for this company.",
|
||||
"checking": "Checking with the server…",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"resume": "Resume",
|
||||
"remove": "Remove",
|
||||
"profiles": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Classes",
|
||||
"vehicles": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Carriers",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Export operations",
|
||||
"imports": "Import operations"
|
||||
}
|
||||
},
|
||||
"config_empty": "No module-specific settings.",
|
||||
"modal": {
|
||||
"title_pending": "Import validation",
|
||||
"title_success": "Import successful",
|
||||
"title_warning": "Import with remarks",
|
||||
"desc_pending": "Review the preliminary analysis before confirming.",
|
||||
"desc_done": "The import process has finished.",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"invalid_rows": "Invalid",
|
||||
"errors": "Errors",
|
||||
"errors_heading": "Scan errors (fix in your CSV)",
|
||||
"errors_badge": "{shown} of {total} error(s)",
|
||||
"errors_truncated": "Download the CSV to see all errors.",
|
||||
"errors_missing_detail": "{count} row(s) had errors but details are not available. Ensure the server is up to date and upload again.",
|
||||
"scan_ok_title": "File validated successfully",
|
||||
"scan_ok_body": "All rows look correct and ready to import.",
|
||||
"scan_problems_title": "Problems found in the file",
|
||||
"scan_problems_body": "Fix the issues listed below in your CSV and upload again, or confirm to import only valid rows (invalid rows will be skipped).",
|
||||
"inserted": "Inserted",
|
||||
"updated": "Updated",
|
||||
"rejected": "Rejected",
|
||||
"rejected_hint": "See line-by-line detail in the table below.",
|
||||
"ref_gaps_title": "Reference gaps (FK / catalogs)",
|
||||
"ref_gaps_body": "There are {n} critical reference gap(s). Review catalogs and rejected rows before retrying.",
|
||||
"ref_state_title": "Reference state",
|
||||
"ref_state_ok": "References ready to operate (no critical gaps reported).",
|
||||
"ref_state_other": "No numeric gaps; review the server message if applicable.",
|
||||
"skipped_reasons_heading": "Rejection reasons summary",
|
||||
"commit_errors_heading": "Error detail",
|
||||
"rows_badge": "{n} rows",
|
||||
"importing_records": "Importing records…",
|
||||
"cancel_operation": "Cancel",
|
||||
"processing": "Processing…",
|
||||
"confirm_load": "Confirm import",
|
||||
"close": "Close",
|
||||
"th_line": "Line",
|
||||
"th_column": "Column",
|
||||
"th_message": "Message",
|
||||
"th_solution": "Solution",
|
||||
"th_reference": "Reference",
|
||||
"th_reason": "Reason",
|
||||
"download_csv": "Download CSV"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,11 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"dashboard": "Dashboard",
|
||||
"management_label": "Gestión",
|
||||
"bulk_upload": {
|
||||
"title": "Cargas masivas",
|
||||
"entry": "Importación CSV"
|
||||
},
|
||||
"reference_data": {
|
||||
"title": "Catálogos Fijos",
|
||||
"codes_pedimento_regimen": "Códigos de Pedimento y Régimen",
|
||||
@@ -1601,5 +1606,198 @@
|
||||
"cancel": "Cancelar",
|
||||
"accept": "Aceptar"
|
||||
}
|
||||
},
|
||||
"csv_upload": {
|
||||
"page_title": "Importación CSV",
|
||||
"intro_help": "Clic izquierdo: cargar archivo CSV. Clic derecho: descargar plantilla.",
|
||||
"tab_catalogos": "Catálogos",
|
||||
"tab_transportes": "Transportes",
|
||||
"tab_importacion": "Importación",
|
||||
"tab_exportacion": "Exportación",
|
||||
"section_catalogs": "Catalogos Generales",
|
||||
"section_transport": "Transportes",
|
||||
"section_import": "Operaciones de importación",
|
||||
"section_export": "Operaciones de exportación",
|
||||
"params_header": "Parámetros globales",
|
||||
"config_prefix": "Configuración",
|
||||
"soon": "Próximamente",
|
||||
"drop_here": "¡Suelta el archivo!",
|
||||
"groups": {
|
||||
"permisos": "Permisos",
|
||||
"impo_temp": "Impo. temp.",
|
||||
"impo_def": "Impo. def.",
|
||||
"cmex": "Compras mex.",
|
||||
"expo_def": "Expo. def./Cam. reg.",
|
||||
"expo_rep": "Expo. rep.",
|
||||
"manifest": "Manifiesto"
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"material_classes": "Clases",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"items": "Partidas (permisos)",
|
||||
"headers": "Encabezados (permisos)",
|
||||
"historical_fractions": "Fracciones históricas",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Transportistas",
|
||||
"transports": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Encabezado",
|
||||
"imp_temp_details": "Partidas",
|
||||
"imp_temp_series": "Series",
|
||||
"imp_def_header": "Encabezado",
|
||||
"imp_def_details": "Partidas",
|
||||
"imp_def_series": "Series",
|
||||
"comp_mex_header": "Encabezado",
|
||||
"comp_mex_details": "Partidas",
|
||||
"comp_mex_series": "Series",
|
||||
"exp_def_header": "Encabezado",
|
||||
"exp_def_details": "Partidas",
|
||||
"exp_def_series": "Series",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Encabezado",
|
||||
"exp_rep_details": "Partidas",
|
||||
"exp_rep_series": "Series",
|
||||
"manifest_header": "Encabezado"
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Modo de carga",
|
||||
"date_format": "Formato de fecha",
|
||||
"weight_unit": "Unidad de peso",
|
||||
"autonumber_series": "Autonumerar partidas/series",
|
||||
"load_subpartidas": "Levantar subpartidas",
|
||||
"recalculate_pedimento_date": "Recalcular fecha pedimento",
|
||||
"autonumber_remesas": "Autonumerar remesas",
|
||||
"recalculate_dates": "Recalcular fechas",
|
||||
"invoice_type": "Tipo de factura",
|
||||
"is_regime_change": "Es cambio de régimen"
|
||||
},
|
||||
"options": {
|
||||
"update": "Actualizar",
|
||||
"replace": "Reemplazar",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"kgs": "Kilos (kg)",
|
||||
"lbs": "Libras (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL"
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Subiendo archivo CSV",
|
||||
"scan": "Validando registros en el servidor",
|
||||
"commit": "Grabando registros en base de datos",
|
||||
"upload_known": "Subiendo archivo…",
|
||||
"upload_unknown": "Subiendo archivo (tamaño desconocido en el navegador)…",
|
||||
"in_progress": "En proceso…",
|
||||
"resume_hint": "Reanudando la importación guardada en esta pestaña…",
|
||||
"rows_file": "Archivo: ~{n} fila(s) de datos — subiendo (aún no se validan registros en servidor)…",
|
||||
"rows_scan": "Registros procesados: {current} / {total}",
|
||||
"rows_commit": "Registros grabados: {current} / {total}",
|
||||
"rows_commit_fallback": "Grabando en base de datos… ({current} / {total} según último total conocido)"
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Formato inválido. Solo se permiten archivos .csv",
|
||||
"download_loading": "Descargando plantilla…",
|
||||
"download_ok": "Plantilla descargada.",
|
||||
"download_err": "Error al descargar la plantilla",
|
||||
"upload_err": "Error al subir el archivo",
|
||||
"upload_err_generic": "Error inesperado al subir el archivo",
|
||||
"scan_done": "Escaneo completado. Revisa los resultados.",
|
||||
"import_done": "Importación completada. Revisa el listado de registros.",
|
||||
"import_maybe_done": "La importación pudo completarse. Revisa el listado de registros.",
|
||||
"stale_job": "Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.",
|
||||
"poll_err": "Error al consultar el estado",
|
||||
"commit_err": "Error al iniciar la importación",
|
||||
"scan_alt": "El escaneo terminó. Si no ves el modal, revisa el listado de registros.",
|
||||
"finished_none": "No se insertaron registros. Revisa los errores a continuación.",
|
||||
"commit_warning_ok": "{inserted} insertados, {updated} actualizados. {skipped} rechazados.",
|
||||
"commit_warning_none": "No se insertaron ni actualizaron registros. {skipped} rechazados.",
|
||||
"success_counts": "Importación completada: {msg}",
|
||||
"warn_skipped": "{n} registros fueron rechazados u omitidos",
|
||||
"error_processing": "Error en el procesamiento: {msg}",
|
||||
"n_inserted": "{n} insertados",
|
||||
"n_updated": "{n} actualizados",
|
||||
"err_fetch_scan_result": "Error al obtener el resultado. Revisa el modal de resultados.",
|
||||
"err_unknown": "Error desconocido",
|
||||
"err_processing_fallback": "Error en el procesamiento. Revisa el modal o los detalles."
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pendientes",
|
||||
"title": "Importaciones pendientes de confirmar",
|
||||
"description": "Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al actualizar.",
|
||||
"refresh": "Actualizar",
|
||||
"empty": "No hay importaciones pendientes para esta empresa.",
|
||||
"checking": "Comprobando con el servidor…",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidas",
|
||||
"resume": "Reanudar",
|
||||
"remove": "Quitar",
|
||||
"profiles": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Clases",
|
||||
"vehicles": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Transportistas",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Exportación (operaciones)",
|
||||
"imports": "Importación (operaciones)"
|
||||
}
|
||||
},
|
||||
"config_empty": "No hay configuraciones específicas para este módulo.",
|
||||
"modal": {
|
||||
"title_pending": "Validación de importación",
|
||||
"title_success": "Importación exitosa",
|
||||
"title_warning": "Importación con observaciones",
|
||||
"desc_pending": "Revise el análisis preliminar antes de confirmar la carga de datos.",
|
||||
"desc_done": "El proceso de importación ha finalizado.",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidos",
|
||||
"invalid_rows": "Inválidos",
|
||||
"errors": "Errores",
|
||||
"errors_heading": "Detalle de errores (para corregir en el CSV)",
|
||||
"errors_badge": "{shown} de {total} error(es)",
|
||||
"errors_truncated": "Para consultar el resto de errores, descargue el CSV.",
|
||||
"errors_missing_detail": "Se detectaron {count} fila(s) con errores pero el detalle no está disponible. Asegúrese de que el servidor esté actualizado y vuelva a subir el archivo.",
|
||||
"scan_ok_title": "Archivo validado correctamente",
|
||||
"scan_ok_body": "Todos los registros parecen correctos y listos para importar.",
|
||||
"scan_problems_title": "Se detectaron problemas en el archivo",
|
||||
"scan_problems_body": "Corrija los datos indicados abajo en su CSV y vuelva a subir, o confirme para importar solo las filas válidas (las erróneas se omitirán).",
|
||||
"inserted": "Insertados",
|
||||
"updated": "Actualizados",
|
||||
"rejected": "Rechazados",
|
||||
"rejected_hint": "Revisa el detalle por línea en la tabla inferior.",
|
||||
"ref_gaps_title": "Brechas de referencia (FK / catálogos)",
|
||||
"ref_gaps_body": "Hay {n} brecha(s) crítica(s) de referencia. Revisa catálogos y el detalle de filas rechazadas antes de reintentar.",
|
||||
"ref_state_title": "Estado de referencias",
|
||||
"ref_state_ok": "Referencias listas para operar (sin brechas críticas reportadas).",
|
||||
"ref_state_other": "Sin brechas numéricas; revisa el mensaje del servidor si aplica.",
|
||||
"skipped_reasons_heading": "Resumen de motivos de rechazo",
|
||||
"commit_errors_heading": "Detalle de errores",
|
||||
"rows_badge": "{n} filas",
|
||||
"importing_records": "Importando registros…",
|
||||
"cancel_operation": "Cancelar",
|
||||
"processing": "Procesando…",
|
||||
"confirm_load": "Confirmar carga",
|
||||
"close": "Cerrar",
|
||||
"th_line": "Línea",
|
||||
"th_column": "Columna",
|
||||
"th_message": "Mensaje",
|
||||
"th_solution": "Solución",
|
||||
"th_reference": "Referencia",
|
||||
"th_reason": "Motivo",
|
||||
"download_csv": "Descargar CSV"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
358
frontend/scripts/merge_csv_upload_i18n.py
Normal file
358
frontend/scripts/merge_csv_upload_i18n.py
Normal file
@@ -0,0 +1,358 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fusiona bloques csv_upload en messages/en.json y messages/es.json y vuelca a src/lib/i18n/csv-upload-messages.*.json.
|
||||
|
||||
La fuente de verdad del copy CSV es `messages/{en,es}.json` (alineado con sidebar, dashboard, facturas).
|
||||
Si editas solo esos JSON, sincroniza con:
|
||||
node -e "const fs=require('fs'),p=require('path'),r='.../frontend';for(const l of['en','es']){const j=JSON.parse(fs.readFileSync(p.join(r,'messages',l+'.json'),'utf8'));fs.writeFileSync(p.join(r,'src/lib/i18n','csv-upload-messages.'+l+'.json'),JSON.stringify(j.csv_upload,null,'\\t')+'\\n')}"
|
||||
|
||||
Ejecutar desde frontend/: python scripts/merge_csv_upload_i18n.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
MESSAGES = ROOT / "messages"
|
||||
|
||||
EN_CSV = {
|
||||
"page_title": "CSV import",
|
||||
"intro_help": "Left-click: upload CSV file. Right-click: download template.",
|
||||
"tab_catalogos": "Catalogs",
|
||||
"tab_transportes": "Transportation",
|
||||
"tab_importacion": "Import",
|
||||
"tab_exportacion": "Export",
|
||||
"section_catalogs": "General Catalogs",
|
||||
"section_transport": "Transportation",
|
||||
"section_import": "Import operations",
|
||||
"section_export": "Export operations",
|
||||
"params_header": "Global parameters",
|
||||
"config_prefix": "Settings",
|
||||
"soon": "Coming soon",
|
||||
"drop_here": "Drop the file!",
|
||||
"groups": {
|
||||
"permisos": "Permissions",
|
||||
"impo_temp": "Temporary import",
|
||||
"impo_def": "Definitive import",
|
||||
"cmex": "Mexican purchases",
|
||||
"expo_def": "Definitive export / regime change",
|
||||
"expo_rep": "Export replenishment",
|
||||
"manifest": "Manifest",
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"material_classes": "Classes",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"items": "Lines (permissions)",
|
||||
"headers": "Headers (permissions)",
|
||||
"historical_fractions": "Historical tariff fractions",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Carriers",
|
||||
"transports": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Header",
|
||||
"imp_temp_details": "Lines",
|
||||
"imp_temp_series": "Serial numbers",
|
||||
"imp_def_header": "Header",
|
||||
"imp_def_details": "Lines",
|
||||
"imp_def_series": "Serial numbers",
|
||||
"comp_mex_header": "Header",
|
||||
"comp_mex_details": "Lines",
|
||||
"comp_mex_series": "Serial numbers",
|
||||
"exp_def_header": "Header",
|
||||
"exp_def_details": "Lines",
|
||||
"exp_def_series": "Serial numbers",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Header",
|
||||
"exp_rep_details": "Lines",
|
||||
"exp_rep_series": "Serial numbers",
|
||||
"manifest_header": "Header",
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Load mode",
|
||||
"date_format": "Date format",
|
||||
"weight_unit": "Weight unit",
|
||||
"autonumber_series": "Autonumber lines/series",
|
||||
"load_subpartidas": "Load sub-lines",
|
||||
"recalculate_pedimento_date": "Recalculate pedimento date",
|
||||
"autonumber_remesas": "Autonumber consignments",
|
||||
"recalculate_dates": "Recalculate dates",
|
||||
"invoice_type": "Invoice type",
|
||||
"is_regime_change": "Regime change",
|
||||
},
|
||||
"options": {
|
||||
"update": "Update",
|
||||
"replace": "Replace",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"kgs": "Kilograms (kg)",
|
||||
"lbs": "Pounds (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL",
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Uploading CSV file",
|
||||
"scan": "Validating records on the server",
|
||||
"commit": "Saving records to the database",
|
||||
"upload_known": "Uploading file…",
|
||||
"upload_unknown": "Uploading file (unknown size in browser)…",
|
||||
"in_progress": "In progress…",
|
||||
"resume_hint": "Resuming import saved in this tab…",
|
||||
"rows_file": "File: ~{n} data row(s) — uploading (not yet validated on server)…",
|
||||
"rows_scan": "Records processed: {current} / {total}",
|
||||
"rows_commit": "Records saved: {current} / {total}",
|
||||
"rows_commit_fallback": "Saving to database… ({current} / {total} using last known total)",
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Invalid format. Only .csv files are allowed.",
|
||||
"download_loading": "Downloading template…",
|
||||
"download_ok": "Template downloaded.",
|
||||
"download_err": "Could not download the template.",
|
||||
"upload_err": "Could not upload the file.",
|
||||
"upload_err_generic": "Unexpected error uploading the file.",
|
||||
"scan_done": "Scan complete. Review the results.",
|
||||
"import_done": "Import completed. Review the record list.",
|
||||
"import_maybe_done": "Import may have completed. Review the record list.",
|
||||
"stale_job": "This import is no longer available (session expired or job removed). You can start a new upload.",
|
||||
"poll_err": "Could not fetch status",
|
||||
"commit_err": "Could not start import",
|
||||
"scan_alt": "Scan finished. If you do not see the modal, check the record list.",
|
||||
"finished_none": "No records inserted. Review the errors below.",
|
||||
"commit_warning_ok": "{inserted} inserted, {updated} updated. {skipped} rejected.",
|
||||
"commit_warning_none": "No records inserted or updated. {skipped} rejected.",
|
||||
"success_counts": "Import completed: {msg}",
|
||||
"warn_skipped": "{n} records rejected or skipped",
|
||||
"error_processing": "Processing error: {msg}",
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pending",
|
||||
"title": "Imports pending confirmation",
|
||||
"description": "Scans ready to save to the database. Expired jobs disappear when you refresh.",
|
||||
"refresh": "Refresh",
|
||||
"empty": "No pending imports for this company.",
|
||||
"checking": "Checking with the server…",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"resume": "Resume",
|
||||
"remove": "Remove",
|
||||
"profiles": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Classes",
|
||||
"vehicles": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Carriers",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Export operations",
|
||||
"imports": "Import operations",
|
||||
},
|
||||
},
|
||||
"modal": {
|
||||
"title_pending": "Import validation",
|
||||
"title_success": "Import successful",
|
||||
"title_warning": "Import with remarks",
|
||||
"desc_pending": "Review the preliminary analysis before confirming.",
|
||||
"desc_done": "The import process has finished.",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"invalid_rows": "Invalid",
|
||||
},
|
||||
}
|
||||
|
||||
ES_CSV = {
|
||||
"page_title": "Importación CSV",
|
||||
"intro_help": "Clic izquierdo: cargar archivo CSV. Clic derecho: descargar plantilla.",
|
||||
"tab_catalogos": "Catálogos",
|
||||
"tab_transportes": "Transportes",
|
||||
"tab_importacion": "Importación",
|
||||
"tab_exportacion": "Exportación",
|
||||
"section_catalogs": "Catalogos Generales",
|
||||
"section_transport": "Transportes",
|
||||
"section_import": "Operaciones de importación",
|
||||
"section_export": "Operaciones de exportación",
|
||||
"params_header": "Parámetros globales",
|
||||
"config_prefix": "Configuración",
|
||||
"soon": "Próximamente",
|
||||
"drop_here": "¡Suelta el archivo!",
|
||||
"groups": {
|
||||
"permisos": "Permisos",
|
||||
"impo_temp": "Impo. temp.",
|
||||
"impo_def": "Impo. def.",
|
||||
"cmex": "Compras mex.",
|
||||
"expo_def": "Expo. def./Cam. reg.",
|
||||
"expo_rep": "Expo. rep.",
|
||||
"manifest": "Manifiesto",
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"material_classes": "Clases",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"items": "Partidas (permisos)",
|
||||
"headers": "Encabezados (permisos)",
|
||||
"historical_fractions": "Fracciones históricas",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Transportistas",
|
||||
"transports": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Encabezado",
|
||||
"imp_temp_details": "Partidas",
|
||||
"imp_temp_series": "Series",
|
||||
"imp_def_header": "Encabezado",
|
||||
"imp_def_details": "Partidas",
|
||||
"imp_def_series": "Series",
|
||||
"comp_mex_header": "Encabezado",
|
||||
"comp_mex_details": "Partidas",
|
||||
"comp_mex_series": "Series",
|
||||
"exp_def_header": "Encabezado",
|
||||
"exp_def_details": "Partidas",
|
||||
"exp_def_series": "Series",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Encabezado",
|
||||
"exp_rep_details": "Partidas",
|
||||
"exp_rep_series": "Series",
|
||||
"manifest_header": "Encabezado",
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Modo de carga",
|
||||
"date_format": "Formato de fecha",
|
||||
"weight_unit": "Unidad de peso",
|
||||
"autonumber_series": "Autonumerar partidas/series",
|
||||
"load_subpartidas": "Levantar subpartidas",
|
||||
"recalculate_pedimento_date": "Recalcular fecha pedimento",
|
||||
"autonumber_remesas": "Autonumerar remesas",
|
||||
"recalculate_dates": "Recalcular fechas",
|
||||
"invoice_type": "Tipo de factura",
|
||||
"is_regime_change": "Es cambio de régimen",
|
||||
},
|
||||
"options": {
|
||||
"update": "Actualizar",
|
||||
"replace": "Reemplazar",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"kgs": "Kilos (kg)",
|
||||
"lbs": "Libras (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL",
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Subiendo archivo CSV",
|
||||
"scan": "Validando registros en el servidor",
|
||||
"commit": "Grabando registros en base de datos",
|
||||
"upload_known": "Subiendo archivo…",
|
||||
"upload_unknown": "Subiendo archivo (tamaño desconocido en el navegador)…",
|
||||
"in_progress": "En proceso…",
|
||||
"resume_hint": "Reanudando la importación guardada en esta pestaña…",
|
||||
"rows_file": "Archivo: ~{n} fila(s) de datos — subiendo (aún no se validan registros en servidor)…",
|
||||
"rows_scan": "Registros procesados: {current} / {total}",
|
||||
"rows_commit": "Registros grabados: {current} / {total}",
|
||||
"rows_commit_fallback": "Grabando en base de datos… ({current} / {total} según último total conocido)",
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Formato inválido. Solo se permiten archivos .csv",
|
||||
"download_loading": "Descargando plantilla…",
|
||||
"download_ok": "Plantilla descargada.",
|
||||
"download_err": "Error al descargar la plantilla",
|
||||
"upload_err": "Error al subir el archivo",
|
||||
"upload_err_generic": "Error inesperado al subir el archivo",
|
||||
"scan_done": "Escaneo completado. Revisa los resultados.",
|
||||
"import_done": "Importación completada. Revisa el listado de registros.",
|
||||
"import_maybe_done": "La importación pudo completarse. Revisa el listado de registros.",
|
||||
"stale_job": "Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.",
|
||||
"poll_err": "Error al consultar el estado",
|
||||
"commit_err": "Error al iniciar la importación",
|
||||
"scan_alt": "El escaneo terminó. Si no ves el modal, revisa el listado de registros.",
|
||||
"finished_none": "No se insertaron registros. Revisa los errores a continuación.",
|
||||
"commit_warning_ok": "{inserted} insertados, {updated} actualizados. {skipped} rechazados.",
|
||||
"commit_warning_none": "No se insertaron ni actualizaron registros. {skipped} rechazados.",
|
||||
"success_counts": "Importación completada: {msg}",
|
||||
"warn_skipped": "{n} registros fueron rechazados u omitidos",
|
||||
"error_processing": "Error en el procesamiento: {msg}",
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pendientes",
|
||||
"title": "Importaciones pendientes de confirmar",
|
||||
"description": "Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al actualizar.",
|
||||
"refresh": "Actualizar",
|
||||
"empty": "No hay importaciones pendientes para esta empresa.",
|
||||
"checking": "Comprobando con el servidor…",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidas",
|
||||
"resume": "Reanudar",
|
||||
"remove": "Quitar",
|
||||
"profiles": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Clases",
|
||||
"vehicles": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Transportistas",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Exportación (operaciones)",
|
||||
"imports": "Importación (operaciones)",
|
||||
},
|
||||
},
|
||||
"modal": {
|
||||
"title_pending": "Validación de importación",
|
||||
"title_success": "Importación exitosa",
|
||||
"title_warning": "Importación con observaciones",
|
||||
"desc_pending": "Revise el análisis preliminar antes de confirmar la carga de datos.",
|
||||
"desc_done": "El proceso de importación ha finalizado.",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidos",
|
||||
"invalid_rows": "Inválidos",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def merge_locale(filename: str, csv_obj: dict) -> None:
|
||||
path = MESSAGES / filename
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
data["csv_upload"] = csv_obj
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent="\t") + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def extract_csv_upload_to_lib() -> None:
|
||||
"""Copia `csv_upload` a src/lib/i18n/csv-upload-messages.*.json (lo que importa csv-msg.ts)."""
|
||||
dest_dir = ROOT / "src/lib/i18n"
|
||||
for filename, suffix in (("en.json", "en"), ("es.json", "es")):
|
||||
path = MESSAGES / filename
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
cu = data.get("csv_upload")
|
||||
if cu is None:
|
||||
raise SystemExit(f"merge_csv_upload_i18n: falta csv_upload en {filename}")
|
||||
out = dest_dir / f"csv-upload-messages.{suffix}.json"
|
||||
out.write_text(json.dumps(cu, ensure_ascii=False, indent="\t") + "\n", encoding="utf-8")
|
||||
print(f"Wrote {out.relative_to(ROOT)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
merge_locale("en.json", EN_CSV)
|
||||
merge_locale("es.json", ES_CSV)
|
||||
print("Merged csv_upload into en.json and es.json")
|
||||
extract_csv_upload_to_lib()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -681,16 +681,23 @@ export const api = {
|
||||
* Returns blob and suggested filename for the browser download.
|
||||
*/
|
||||
async getCsvTemplateDownload(
|
||||
templateId: string
|
||||
templateId: string,
|
||||
locale?: string
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const response = await fetch(`${API_BASE_URL}/v1/a76/csv-templates/${templateId}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
credentials: 'include'
|
||||
});
|
||||
const loc = locale === 'en' ? 'en' : 'es';
|
||||
const qs = new URLSearchParams({ locale: loc });
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/v1/a76/csv-templates/${templateId}?${qs.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const msg = response.status === 404 ? 'Plantilla no encontrada' : `Error ${response.status}`;
|
||||
throw new Error(msg);
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||
import { Settings2 } from 'lucide-svelte';
|
||||
import { tabSettings } from '$lib/config/csv-upload';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
const TAB_LABEL: Record<string, string> = {
|
||||
catalogos: 'tab_catalogos',
|
||||
transportes: 'tab_transportes',
|
||||
importacion: 'tab_importacion',
|
||||
exportacion: 'tab_exportacion'
|
||||
};
|
||||
|
||||
let {
|
||||
activeTab,
|
||||
@@ -38,7 +46,9 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2 text-muted-foreground border-b pb-2">
|
||||
<Settings2 class="h-4 w-4" />
|
||||
<span class="text-xs font-semibold uppercase tracking-wider">Configuración: {activeTab}</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wider"
|
||||
>{csvMsg('config_prefix')}: {csvMsg(TAB_LABEL[activeTab] ?? activeTab)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if currentFields.length > 0}
|
||||
@@ -47,7 +57,7 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if field.type !== 'boolean'}
|
||||
<Label class="text-xs font-medium text-muted-foreground uppercase"
|
||||
>{field.label}</Label
|
||||
>{csvMsg(field.labelKey)}</Label
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -59,7 +69,7 @@
|
||||
<Label
|
||||
for={field.name}
|
||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>{field.label}</Label
|
||||
>{csvMsg(field.labelKey)}</Label
|
||||
>
|
||||
</div>
|
||||
{:else if field.type === 'select' && field.options}
|
||||
@@ -68,7 +78,7 @@
|
||||
bind:value={settings[field.name]}
|
||||
>
|
||||
{#each field.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if field.type === 'radio' && field.options}
|
||||
@@ -79,7 +89,7 @@
|
||||
{#each field.options as opt}
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={opt.value} id={`${field.name}-${opt.value}`} />
|
||||
<Label for={`${field.name}-${opt.value}`}>{opt.label}</Label>
|
||||
<Label for={`${field.name}-${opt.value}`}>{csvMsg(opt.labelKey)}</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Root>
|
||||
@@ -89,7 +99,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-8 text-sm text-muted-foreground italic">
|
||||
No hay configuraciones específicas para este módulo.
|
||||
{csvMsg('config_empty')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
import { Settings2 } from 'lucide-svelte';
|
||||
import { globalCsvParams, tabSettings, type CsvUploadField } from '$lib/config/csv-upload';
|
||||
import { cn } from '$lib/utils';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
const TAB_LABEL: Record<string, string> = {
|
||||
catalogos: 'tab_catalogos',
|
||||
transportes: 'tab_transportes',
|
||||
importacion: 'tab_importacion',
|
||||
exportacion: 'tab_exportacion'
|
||||
};
|
||||
|
||||
let {
|
||||
globalSettings = $bindable(),
|
||||
@@ -45,13 +53,13 @@
|
||||
<div class="flex items-center gap-2 text-muted-foreground border-r border-border pr-4">
|
||||
<Settings2 class="h-4 w-4 shrink-0" />
|
||||
<span class="text-xs font-semibold uppercase tracking-wider whitespace-nowrap"
|
||||
>Parámetros globales</span
|
||||
>{csvMsg('params_header')}</span
|
||||
>
|
||||
</div>
|
||||
{#each globalCsvParams as param}
|
||||
<div class="flex flex-col gap-1">
|
||||
<Label for="global-{param.name}" class="text-xs font-medium text-muted-foreground"
|
||||
>{param.label}</Label
|
||||
>{csvMsg(param.labelKey)}</Label
|
||||
>
|
||||
<select
|
||||
id="global-{param.name}"
|
||||
@@ -62,7 +70,7 @@
|
||||
}}
|
||||
>
|
||||
{#each param.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -73,13 +81,13 @@
|
||||
{#if currentTabFields.length > 0}
|
||||
<div class="flex flex-wrap items-center gap-4 border-l border-border pl-4">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-muted-foreground whitespace-nowrap"
|
||||
>Configuración: {activeTab}</span
|
||||
>{csvMsg('config_prefix')}: {csvMsg(TAB_LABEL[activeTab] ?? activeTab)}</span
|
||||
>
|
||||
{#each currentTabFields as field}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#if field.type !== 'boolean'}
|
||||
<Label for="tab-{field.name}" class="text-xs font-medium text-muted-foreground"
|
||||
>{field.label}</Label
|
||||
>{csvMsg(field.labelKey)}</Label
|
||||
>
|
||||
{/if}
|
||||
{#if field.type === 'select' && field.options}
|
||||
@@ -92,7 +100,7 @@
|
||||
}}
|
||||
>
|
||||
{#each field.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if field.type === 'radio' && field.options}
|
||||
@@ -108,7 +116,7 @@
|
||||
if (tabSettingsValues) tabSettingsValues[field.name] = opt.value;
|
||||
}}
|
||||
/>
|
||||
{opt.label}
|
||||
{csvMsg(opt.labelKey)}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -121,7 +129,7 @@
|
||||
if (tabSettingsValues) tabSettingsValues[field.name] = e.currentTarget.checked;
|
||||
}}
|
||||
/>
|
||||
{field.label}
|
||||
{csvMsg(field.labelKey)}
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
} from '$lib/csv-import-pending';
|
||||
import { fetchCsvImportStatus, isWaitingConfirmationPayload } from '$lib/csv-import-status-api';
|
||||
import { Loader2, RefreshCw } from 'lucide-svelte';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
type ValidatedRow = CsvImportPendingEntry & { checking?: boolean };
|
||||
|
||||
@@ -36,22 +37,9 @@
|
||||
}
|
||||
|
||||
function profileLabel(p: CsvImportPendingEntry['profile']): string {
|
||||
const map: Record<CsvImportPendingEntry['profile'], string> = {
|
||||
customs_brokers: 'Agentes aduanales',
|
||||
clients_providers: 'Clientes / proveedores',
|
||||
exchange_rates: 'Tipos de cambio',
|
||||
pedimentos: 'Pedimentos',
|
||||
material_classes: 'Clases de material',
|
||||
vehicles: 'Vehículos',
|
||||
drivers: 'Conductores',
|
||||
trailers: 'Remolques',
|
||||
transporters: 'Transportistas',
|
||||
part_numbers: 'Números de parte',
|
||||
boms: 'BOMs',
|
||||
exportacion: 'Exportación (operaciones)',
|
||||
imports: 'Importación (operaciones)'
|
||||
};
|
||||
return map[p] ?? p;
|
||||
const key = `pending.profiles.${p}` as const;
|
||||
const t = csvMsg(key);
|
||||
return t === key ? p : t;
|
||||
}
|
||||
|
||||
function isStaleJob(status: number, err: string | undefined): boolean {
|
||||
@@ -134,7 +122,7 @@
|
||||
</script>
|
||||
|
||||
<Button variant="outline" size="sm" class="shrink-0" type="button" onclick={() => (open = true)}>
|
||||
Pendientes
|
||||
{csvMsg('pending.badge')}
|
||||
{#if badgeCount > 0}
|
||||
<span class="ml-1.5 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-primary">
|
||||
{badgeCount}
|
||||
@@ -145,10 +133,9 @@
|
||||
<Sheet.Root bind:open>
|
||||
<Sheet.Content side="right" class="flex w-full max-w-lg flex-col sm:max-w-xl">
|
||||
<Sheet.Header>
|
||||
<Sheet.Title>Importaciones pendientes de confirmar</Sheet.Title>
|
||||
<Sheet.Title>{csvMsg('pending.title')}</Sheet.Title>
|
||||
<Sheet.Description>
|
||||
Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al
|
||||
actualizar.
|
||||
{csvMsg('pending.description')}
|
||||
</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
<div class="flex items-center justify-end gap-2 border-b px-4 py-2">
|
||||
@@ -158,14 +145,14 @@
|
||||
{:else}
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Actualizar
|
||||
{csvMsg('pending.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||
{#if rows.length === 0 && !refreshing}
|
||||
<p class="text-sm text-muted-foreground">No hay importaciones pendientes para esta empresa.</p>
|
||||
<p class="text-sm text-muted-foreground">{csvMsg('pending.empty')}</p>
|
||||
{:else if rows.length === 0 && refreshing}
|
||||
<p class="text-sm text-muted-foreground">Comprobando con el servidor…</p>
|
||||
<p class="text-sm text-muted-foreground">{csvMsg('pending.checking')}</p>
|
||||
{:else}
|
||||
<ul class="space-y-3">
|
||||
{#each rows as row (row.jobId)}
|
||||
@@ -177,10 +164,10 @@
|
||||
{#if row.totalRows != null || row.validRows != null}
|
||||
<div class="mt-1 text-xs text-muted-foreground">
|
||||
{#if row.totalRows != null}
|
||||
Total filas: {row.totalRows}
|
||||
{csvMsg('pending.total_rows')}: {row.totalRows}
|
||||
{/if}
|
||||
{#if row.validRows != null}
|
||||
<span class={row.totalRows != null ? ' · ' : ''}>Válidas: {row.validRows}</span>
|
||||
<span class={row.totalRows != null ? ' · ' : ''}>{csvMsg('pending.valid_rows')}: {row.validRows}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -201,9 +188,9 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reanudar
|
||||
{csvMsg('pending.resume')}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onclick={() => removeLocal(row.jobId)}>Quitar</Button>
|
||||
<Button size="sm" variant="ghost" onclick={() => removeLocal(row.jobId)}>{csvMsg('pending.remove')}</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
criticalReferenceGaps,
|
||||
referenceStateReady
|
||||
} from '$lib/csv-import-commit-metrics';
|
||||
import { csvFmt, csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -249,16 +250,16 @@
|
||||
<div class="flex-1">
|
||||
<Dialog.Title class="text-xl font-semibold tracking-tight text-foreground">
|
||||
{#if isPending}
|
||||
Validación de Importación
|
||||
{csvMsg('modal.title_pending')}
|
||||
{:else if isFinished}
|
||||
{hasErrors ? 'Importación con Observaciones' : 'Importación Exitosa'}
|
||||
{hasErrors ? csvMsg('modal.title_warning') : csvMsg('modal.title_success')}
|
||||
{/if}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="mt-1 text-muted-foreground">
|
||||
{#if isPending}
|
||||
Revise el análisis preliminar antes de confirmar la carga de datos.
|
||||
{csvMsg('modal.desc_pending')}
|
||||
{:else if isFinished}
|
||||
El proceso de importación ha finalizado.
|
||||
{csvMsg('modal.desc_done')}
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
@@ -274,7 +275,7 @@
|
||||
class="bg-card p-4 rounded-lg border flex flex-col items-center justify-center text-center shadow-sm"
|
||||
>
|
||||
<span class="text-muted-foreground text-xs uppercase font-bold tracking-wider mb-1"
|
||||
>Total Filas</span
|
||||
>{csvMsg('modal.total_rows')}</span
|
||||
>
|
||||
<span class="text-2xl font-bold text-foreground">{scanResults.total_rows || 0}</span>
|
||||
</div>
|
||||
@@ -285,7 +286,7 @@
|
||||
>
|
||||
<span
|
||||
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider mb-1"
|
||||
>Válidos</span
|
||||
>{csvMsg('modal.valid_rows')}</span
|
||||
>
|
||||
<span class="text-2xl font-bold text-green-700 dark:text-green-300"
|
||||
>{scanResults.valid_rows || 0}</span
|
||||
@@ -297,7 +298,7 @@
|
||||
class="bg-destructive/5 p-4 rounded-lg border border-destructive/10 flex flex-col items-center justify-center text-center shadow-sm"
|
||||
>
|
||||
<span class="text-destructive text-xs uppercase font-bold tracking-wider mb-1"
|
||||
>Errores</span
|
||||
>{csvMsg('modal.errors')}</span
|
||||
>
|
||||
<span class="text-2xl font-bold text-destructive">{scanResults.error_count || 0}</span>
|
||||
</div>
|
||||
@@ -327,10 +328,9 @@
|
||||
>
|
||||
<XCircle class="w-5 h-5 text-destructive mt-0.5 shrink-0" />
|
||||
<div class="text-sm text-destructive-foreground/90">
|
||||
<p class="font-semibold mb-1">Se detectaron problemas en el archivo</p>
|
||||
<p class="font-semibold mb-1">{csvMsg('modal.scan_problems_title')}</p>
|
||||
<p>
|
||||
Corrija los datos indicados abajo en su CSV y vuelva a subir, o confirme para
|
||||
importar solo las filas válidas (las erróneas se omitirán).
|
||||
{csvMsg('modal.scan_problems_body')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,13 +338,13 @@
|
||||
<div class="border rounded-lg overflow-hidden shadow-sm">
|
||||
<div class="bg-muted/50 px-4 py-2 border-b flex justify-between items-center">
|
||||
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
|
||||
Detalle de errores (para corregir en el CSV)
|
||||
{csvMsg('modal.errors_heading')}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
||||
>
|
||||
{scanErrorsShown} de {scanErrorsTotal} error(es)
|
||||
{csvFmt('modal.errors_badge', { shown: scanErrorsShown, total: scanErrorsTotal })}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -352,7 +352,7 @@
|
||||
class="h-7 px-2 text-xs"
|
||||
>
|
||||
<FileText class="w-3.5 h-3.5 mr-1" />
|
||||
Descargar CSV
|
||||
{csvMsg('modal.download_csv')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,10 +362,10 @@
|
||||
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-4 py-2 w-16">Línea</th>
|
||||
<th class="px-4 py-2 w-40">Columna</th>
|
||||
<th class="px-4 py-2">Mensaje</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">Solución</th>
|
||||
<th class="px-4 py-2 w-16">{csvMsg('modal.th_line')}</th>
|
||||
<th class="px-4 py-2 w-40">{csvMsg('modal.th_column')}</th>
|
||||
<th class="px-4 py-2">{csvMsg('modal.th_message')}</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@@ -382,21 +382,21 @@
|
||||
</div>
|
||||
{#if scanErrorsTruncated}
|
||||
<p class="text-xs text-muted-foreground px-4 py-2">
|
||||
Para consultar el resto de errores, descargue el CSV.
|
||||
{csvMsg('modal.errors_truncated')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
Se detectaron {scanResults.error_count} fila(s) con errores pero el detalle no está disponible. Asegúrese de que el servidor esté actualizado y vuelva a subir el archivo.
|
||||
{csvFmt('modal.errors_missing_detail', { count: scanResults.error_count })}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="rounded-md bg-primary/5 border border-primary/10 p-4 flex items-start gap-3">
|
||||
<CheckCircle2 class="w-5 h-5 text-primary mt-0.5 shrink-0" />
|
||||
<div class="text-sm text-primary/90">
|
||||
<p class="font-semibold mb-1">Archivo validado correctamente</p>
|
||||
<p>Todos los registros parecen correctos y listos para importar.</p>
|
||||
<p class="font-semibold mb-1">{csvMsg('modal.scan_ok_title')}</p>
|
||||
<p>{csvMsg('modal.scan_ok_body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -413,7 +413,7 @@
|
||||
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
||||
>Insertados</span
|
||||
>{csvMsg('modal.inserted')}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-3xl font-bold text-green-700 dark:text-green-300">{insertedCount}</span>
|
||||
@@ -425,7 +425,7 @@
|
||||
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
||||
>Actualizados</span
|
||||
>{csvMsg('modal.updated')}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-3xl font-bold text-green-700 dark:text-green-300">{updatedCount}</span>
|
||||
@@ -436,13 +436,13 @@
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<XCircle class="h-4 w-4 text-destructive" />
|
||||
<span class="text-xs font-bold uppercase tracking-wider text-destructive"
|
||||
>Rechazados</span
|
||||
>{csvMsg('modal.rejected')}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-3xl font-bold text-destructive">{totalSkipped}</span>
|
||||
{#if totalSkipped > 0 && commitResults.skipped_details && commitResults.skipped_details.length > 0}
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
Revisa el detalle por línea en la tabla inferior.
|
||||
{csvMsg('modal.rejected_hint')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -456,20 +456,17 @@
|
||||
{#if refGaps > 0}
|
||||
<AlertTriangle class="mt-0.5 h-5 w-5 shrink-0 text-amber-600 dark:text-amber-500" />
|
||||
<div>
|
||||
<p class="font-semibold text-foreground">Brechas de referencia (FK / catálogos)</p>
|
||||
<p class="font-semibold text-foreground">{csvMsg('modal.ref_gaps_title')}</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
Hay {refGaps} brecha(s) crítica(s) de referencia. Revisa catálogos y el detalle de
|
||||
filas rechazadas antes de reintentar.
|
||||
{csvFmt('modal.ref_gaps_body', { n: refGaps })}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<CheckCircle2 class="mt-0.5 h-5 w-5 shrink-0 text-green-600 dark:text-green-500" />
|
||||
<div>
|
||||
<p class="font-semibold text-foreground">Estado de referencias</p>
|
||||
<p class="font-semibold text-foreground">{csvMsg('modal.ref_state_title')}</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
{refReady
|
||||
? 'Referencias listas para operar (sin brechas críticas reportadas).'
|
||||
: 'Sin brechas numéricas; revisa el mensaje del servidor si aplica.'}
|
||||
{refReady ? csvMsg('modal.ref_state_ok') : csvMsg('modal.ref_state_other')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -483,7 +480,7 @@
|
||||
{#if commitSkippedSummary.length > 0}
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
Resumen de motivos de rechazo
|
||||
{csvMsg('modal.skipped_reasons_heading')}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each commitSkippedSummary as item}
|
||||
@@ -504,13 +501,13 @@
|
||||
<div id="detalle-errores-import" class="border rounded-lg overflow-hidden mt-2 shadow-sm">
|
||||
<div class="bg-muted/50 px-4 py-2 border-b flex justify-between items-center">
|
||||
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
|
||||
Detalle de Errores
|
||||
{csvMsg('modal.commit_errors_heading')}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
||||
>
|
||||
{commitResults.skipped_details.length} filas
|
||||
{csvFmt('modal.rows_badge', { n: commitResults.skipped_details.length })}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -518,7 +515,7 @@
|
||||
class="h-7 px-2 text-xs"
|
||||
>
|
||||
<FileText class="w-3.5 h-3.5 mr-1" />
|
||||
Descargar CSV
|
||||
{csvMsg('modal.download_csv')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -528,10 +525,10 @@
|
||||
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-4 py-2 w-20">Línea</th>
|
||||
<th class="px-4 py-2 w-32">Referencia</th>
|
||||
<th class="px-4 py-2">Motivo</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">Solución</th>
|
||||
<th class="px-4 py-2 w-20">{csvMsg('modal.th_line')}</th>
|
||||
<th class="px-4 py-2 w-32">{csvMsg('modal.th_reference')}</th>
|
||||
<th class="px-4 py-2">{csvMsg('modal.th_reason')}</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@@ -558,7 +555,7 @@
|
||||
<div class="flex flex-col gap-3 border-t bg-muted/20 px-6 py-4">
|
||||
{#if isPending && isUploading}
|
||||
<div class="space-y-2" role="status" aria-live="polite" aria-busy="true">
|
||||
<p class="text-xs font-medium text-muted-foreground">Importando registros…</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{csvMsg('modal.importing_records')}</p>
|
||||
<div class="relative h-2 w-full overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
class="csv-commit-indeterminate-bar absolute top-0 h-full w-2/5 rounded-full bg-primary"
|
||||
@@ -574,7 +571,7 @@
|
||||
disabled={isUploading}
|
||||
class="text-muted-foreground hover:bg-muted/50"
|
||||
>
|
||||
Cancelar Operación
|
||||
{csvMsg('modal.cancel_operation')}
|
||||
</Button>
|
||||
<Button
|
||||
onclick={onConfirm}
|
||||
@@ -583,14 +580,14 @@
|
||||
>
|
||||
{#if isUploading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Procesando...
|
||||
{csvMsg('modal.processing')}
|
||||
{:else}
|
||||
<UploadCloud class="mr-2 h-4 w-4" />
|
||||
Confirmar Carga
|
||||
{csvMsg('modal.confirm_load')}
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if isFinished}
|
||||
<Button variant="outline" onclick={onClose} class="min-w-[100px]">Cerrar</Button>
|
||||
<Button variant="outline" onclick={onClose} class="min-w-[100px]">{csvMsg('modal.close')}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,21 @@
|
||||
import { UploadCloud, Lock } from 'lucide-svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { browser } from '$app/environment';
|
||||
import { api } from '$lib/api';
|
||||
import { cookieName, getLocale } from '$lib/paraglide/runtime';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
/** Misma fuente que el switch de idioma (cookie Paraglide); `getLocale()` puede quedar desincronizado. */
|
||||
function csvTemplateLocale(): 'es' | 'en' {
|
||||
if (browser) {
|
||||
const cookies = document.cookie.split(';').map((c) => c.trim());
|
||||
const localeCookie = cookies.find((c) => c.startsWith(`${cookieName}=`));
|
||||
const current = localeCookie ? localeCookie.split('=')[1] : '';
|
||||
if (current) return current.toLowerCase().startsWith('en') ? 'en' : 'es';
|
||||
}
|
||||
return String(getLocale()).toLowerCase().startsWith('en') ? 'en' : 'es';
|
||||
}
|
||||
|
||||
let {
|
||||
items,
|
||||
@@ -62,7 +76,7 @@
|
||||
const isValidExtension = file.name.toLowerCase().endsWith('.csv');
|
||||
|
||||
if (!isValidExtension) {
|
||||
toast.error('Formato inválido. Solo se permiten archivos .csv');
|
||||
toast.error(csvMsg('toast.invalid_csv'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,8 +120,12 @@
|
||||
if (!item.templateId) return;
|
||||
|
||||
try {
|
||||
toast.info(`Descargando plantilla para ${item.title}...`);
|
||||
const { blob, filename } = await api.getCsvTemplateDownload(item.templateId);
|
||||
const itemLabel = csvMsg(`items.${item.id}`);
|
||||
toast.info(`${csvMsg('toast.download_loading')} ${itemLabel}`);
|
||||
const { blob, filename } = await api.getCsvTemplateDownload(
|
||||
item.templateId,
|
||||
csvTemplateLocale()
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -116,9 +134,9 @@
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(`Plantilla descargada: ${filename}`);
|
||||
toast.success(`${csvMsg('toast.download_ok')} ${filename}`);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Error al descargar la plantilla');
|
||||
toast.error(err instanceof Error ? err.message : csvMsg('toast.download_err'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -128,6 +146,10 @@
|
||||
class:opacity-60={gridLocked}
|
||||
aria-busy={gridLocked ? true : undefined}
|
||||
>
|
||||
{#snippet cardTitle(itemId: string)}
|
||||
<div class="font-medium text-sm text-balance">{csvMsg(`items.${itemId}`)}</div>
|
||||
{/snippet}
|
||||
|
||||
{#if groupedItems.ungrouped.length > 0}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{#each groupedItems.ungrouped as item}
|
||||
@@ -171,7 +193,7 @@
|
||||
<span
|
||||
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
|
||||
>
|
||||
<Lock class="h-3 w-3" /> Próximamente
|
||||
<Lock class="h-3 w-3" /> {csvMsg('soon')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -183,7 +205,7 @@
|
||||
<div class="animate-bounce">
|
||||
<UploadCloud class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-primary">¡Suelta el archivo!</span>
|
||||
<span class="text-sm font-semibold text-primary">{csvMsg('drop_here')}</span>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
@@ -193,7 +215,7 @@
|
||||
>
|
||||
<item.icon class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div class="font-medium text-sm text-balance">{item.title}</div>
|
||||
{@render cardTitle(item.id)}
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -205,7 +227,7 @@
|
||||
{#each Object.entries(groupedItems.groups) as [groupName, groupItems]}
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">
|
||||
{groupName}
|
||||
{csvMsg(`groups.${groupName}`)}
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{#each groupItems as item}
|
||||
@@ -251,7 +273,7 @@
|
||||
<span
|
||||
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
|
||||
>
|
||||
<Lock class="h-3 w-3" /> Próximamente
|
||||
<Lock class="h-3 w-3" /> {csvMsg('soon')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -263,7 +285,7 @@
|
||||
<div class="animate-bounce">
|
||||
<UploadCloud class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-primary">¡Suelta el archivo!</span>
|
||||
<span class="text-sm font-semibold text-primary">{csvMsg('drop_here')}</span>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
@@ -273,7 +295,7 @@
|
||||
>
|
||||
<item.icon class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div class="font-medium text-sm text-balance">{item.title}</div>
|
||||
{@render cardTitle(item.id)}
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -91,6 +91,18 @@ export function getSidebarData(): SidebarData {
|
||||
items: [],
|
||||
permission: 'audit_logs.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.bulk_upload.title"](),
|
||||
url: "#",
|
||||
icon: ArrowUpFromLine,
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.bulk_upload.entry"](),
|
||||
url: "/dashboard/csv-upload",
|
||||
permission: "csv_upload.process",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: m["sidebar.reference_data.title"](),
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||
import FolderIcon from '@lucide/svelte/icons/folder';
|
||||
import ForwardIcon from '@lucide/svelte/icons/forward';
|
||||
import Trash2Icon from '@lucide/svelte/icons/trash-2';
|
||||
import { helpStore } from '$lib/stores/help.svelte';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
projects
|
||||
@@ -15,28 +8,14 @@
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
// This should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
}[];
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
let open = $state(false);
|
||||
let position = $state({ x: 0, y: 0 });
|
||||
|
||||
async function handleMoreClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
open = false;
|
||||
position = { x: e.clientX, y: e.clientY };
|
||||
await tick();
|
||||
open = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||
<Sidebar.GroupLabel>Gestion</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupLabel>{m['sidebar.management_label']()}</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each projects as item (item.name)}
|
||||
<Sidebar.MenuItem>
|
||||
@@ -48,58 +27,7 @@
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuAction showOnHover {...props}>
|
||||
<EllipsisIcon />
|
||||
<span class="sr-only">More</span>
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-48 rounded-lg"
|
||||
side={sidebar.isMobile ? 'bottom' : 'right'}
|
||||
align={sidebar.isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenu.Item>
|
||||
<FolderIcon class="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<ForwardIcon class="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item>
|
||||
<Trash2Icon class="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton class="text-sidebar-foreground/70" onclick={handleMoreClick}>
|
||||
<EllipsisIcon class="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</Sidebar.MenuButton>
|
||||
|
||||
<DropdownMenu.Root open={open} onOpenChange={(v) => (open = v)}>
|
||||
<DropdownMenu.Trigger
|
||||
class="fixed z-50 size-0"
|
||||
style="top: {position.y}px; left: {position.x}px"
|
||||
/>
|
||||
<DropdownMenu.Content class="w-48 rounded-lg" side="right" align="start">
|
||||
<DropdownMenu.Item>
|
||||
<a href="/dashboard/csv-upload" class="flex w-full items-center gap-2">
|
||||
<FolderIcon class="size-4 text-muted-foreground" />
|
||||
<span>Carga CSV</span>
|
||||
</a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
|
||||
export interface CsvUploadItem {
|
||||
id: string;
|
||||
title: string;
|
||||
/** Etiqueta vía i18n: csv_upload.items.<id> */
|
||||
icon: any;
|
||||
group?: string; // For grouping within a tab
|
||||
/** Clave i18n csv_upload.groups.<group> */
|
||||
group?: string;
|
||||
modelTarget?: string; // The backend model this maps to
|
||||
description?: string;
|
||||
/** Backend template id for CSV download (e.g. customs_brokers, part_numbers). No physical file. */
|
||||
@@ -36,9 +37,10 @@ export interface CsvUploadItem {
|
||||
|
||||
export interface CsvUploadField {
|
||||
name: string;
|
||||
label: string;
|
||||
/** Ruta bajo csv_upload.* (p. ej. params.load_mode) */
|
||||
labelKey: string;
|
||||
type: 'text' | 'select' | 'boolean' | 'date' | 'radio';
|
||||
options?: { label: string; value: string | boolean | number }[];
|
||||
options?: { labelKey: string; value: string | boolean | number }[];
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
@@ -46,9 +48,9 @@ export interface CsvUploadField {
|
||||
/** Global parameters shown in the CSV upload footer bar (one option or the other via select). */
|
||||
export interface GlobalCsvParam {
|
||||
name: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
type: 'select';
|
||||
options: { label: string; value: string }[];
|
||||
options: { labelKey: string; value: string }[];
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
@@ -56,62 +58,62 @@ export interface GlobalCsvParam {
|
||||
export const globalCsvParams: GlobalCsvParam[] = [
|
||||
{
|
||||
name: 'mode',
|
||||
label: 'Modo de Carga',
|
||||
labelKey: 'params.load_mode',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Actualizar', value: 'update' },
|
||||
{ label: 'Reemplazar', value: 'replace' }
|
||||
{ labelKey: 'options.update', value: 'update' },
|
||||
{ labelKey: 'options.replace', value: 'replace' }
|
||||
],
|
||||
defaultValue: 'update'
|
||||
},
|
||||
{
|
||||
name: 'dateFormat',
|
||||
label: 'Formato de Fecha',
|
||||
labelKey: 'params.date_format',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
||||
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||
],
|
||||
defaultValue: 'dd/mm/yyyy'
|
||||
},
|
||||
{
|
||||
name: 'weight_unit',
|
||||
label: 'Unidad de Peso',
|
||||
labelKey: 'params.weight_unit',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Kilos (Kgs)', value: 'kgs' },
|
||||
{ label: 'Libras (Lbs)', value: 'lbs' }
|
||||
{ labelKey: 'options.kgs', value: 'kgs' },
|
||||
{ labelKey: 'options.lbs', value: 'lbs' }
|
||||
],
|
||||
defaultValue: 'kgs'
|
||||
},
|
||||
{
|
||||
name: 'autonumber_series',
|
||||
label: 'Autonumerar Partidas/Series',
|
||||
labelKey: 'params.autonumber_series',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Sí', value: 'true' },
|
||||
{ label: 'No', value: 'false' }
|
||||
{ labelKey: 'options.yes', value: 'true' },
|
||||
{ labelKey: 'options.no', value: 'false' }
|
||||
],
|
||||
defaultValue: 'false'
|
||||
},
|
||||
{
|
||||
name: 'load_subpartidas',
|
||||
label: 'Levantar Subpartidas',
|
||||
labelKey: 'params.load_subpartidas',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Sí', value: 'true' },
|
||||
{ label: 'No', value: 'false' }
|
||||
{ labelKey: 'options.yes', value: 'true' },
|
||||
{ labelKey: 'options.no', value: 'false' }
|
||||
],
|
||||
defaultValue: 'false'
|
||||
},
|
||||
{
|
||||
name: 'recalculate_pedimento_date',
|
||||
label: 'Recalcular Fecha Pedimento',
|
||||
labelKey: 'params.recalculate_pedimento_date',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Sí', value: 'true' },
|
||||
{ label: 'No', value: 'false' }
|
||||
{ labelKey: 'options.yes', value: 'true' },
|
||||
{ labelKey: 'options.no', value: 'false' }
|
||||
],
|
||||
defaultValue: 'false'
|
||||
}
|
||||
@@ -122,11 +124,11 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
catalogos: [
|
||||
{
|
||||
name: 'mode',
|
||||
label: 'Modo de Carga',
|
||||
labelKey: 'params.load_mode',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: 'Actualizar', value: 'update' },
|
||||
{ label: 'Reemplazar', value: 'replace' }
|
||||
{ labelKey: 'options.update', value: 'update' },
|
||||
{ labelKey: 'options.replace', value: 'replace' }
|
||||
],
|
||||
defaultValue: 'update'
|
||||
}
|
||||
@@ -134,11 +136,11 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
transportes: [
|
||||
{
|
||||
name: 'mode',
|
||||
label: 'Modo de Carga',
|
||||
labelKey: 'params.load_mode',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: 'Actualizar', value: 'update' },
|
||||
{ label: 'Reemplazar', value: 'replace' }
|
||||
{ labelKey: 'options.update', value: 'update' },
|
||||
{ labelKey: 'options.replace', value: 'replace' }
|
||||
],
|
||||
defaultValue: 'update'
|
||||
}
|
||||
@@ -146,24 +148,24 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
importacion: [
|
||||
{
|
||||
name: 'autonumber_remesas',
|
||||
label: 'Autonumerar Remesas',
|
||||
labelKey: 'params.autonumber_remesas',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
name: 'recalculate_dates',
|
||||
label: 'Recalcular Fechas',
|
||||
labelKey: 'params.recalculate_dates',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
name: 'dateFormat',
|
||||
label: 'Formato de Fecha',
|
||||
labelKey: 'params.date_format',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
||||
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||
],
|
||||
defaultValue: 'dd/mm/yyyy'
|
||||
}
|
||||
@@ -171,28 +173,28 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
exportacion: [
|
||||
{
|
||||
name: 'invoice_type',
|
||||
label: 'Tipo de Factura',
|
||||
labelKey: 'params.invoice_type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'AFIJO', value: 'AFIJO' },
|
||||
{ label: 'NORMAL', value: 'NORMAL' },
|
||||
{ labelKey: 'options.afi', value: 'AFIJO' },
|
||||
{ labelKey: 'options.normal', value: 'NORMAL' },
|
||||
],
|
||||
defaultValue: 'AFIJO',
|
||||
},
|
||||
{
|
||||
name: 'is_regime_change',
|
||||
label: 'Es Cambio de Régimen',
|
||||
labelKey: 'params.is_regime_change',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'dateFormat',
|
||||
label: 'Formato de Fecha',
|
||||
labelKey: 'params.date_format',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
||||
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||
],
|
||||
defaultValue: 'dd/mm/yyyy'
|
||||
}
|
||||
@@ -204,7 +206,6 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
export const catalogosConfig: CsvUploadItem[] = [
|
||||
{
|
||||
id: 'customs_brokers',
|
||||
title: 'Agentes Aduanales',
|
||||
icon: User,
|
||||
modelTarget: 'CustomsBroker',
|
||||
templateId: 'customs_brokers',
|
||||
@@ -212,7 +213,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'clients_providers',
|
||||
title: 'Clientes y Proveedores',
|
||||
icon: Users,
|
||||
modelTarget: 'ClientProvider',
|
||||
templateId: 'clients_providers',
|
||||
@@ -220,7 +220,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'exchange_rates',
|
||||
title: 'Tipo de Cambios',
|
||||
icon: DollarSign,
|
||||
modelTarget: 'ExchangeRate',
|
||||
templateId: 'exchange_rates',
|
||||
@@ -228,7 +227,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'material_classes',
|
||||
title: 'Clases de Materiales',
|
||||
icon: Package,
|
||||
modelTarget: 'MaterialClass',
|
||||
templateId: 'material_classes',
|
||||
@@ -236,7 +234,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'part_numbers',
|
||||
title: 'Números de parte',
|
||||
icon: Hash,
|
||||
modelTarget: 'Part',
|
||||
templateId: 'part_numbers',
|
||||
@@ -244,7 +241,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'boms',
|
||||
title: 'BOMs',
|
||||
icon: Briefcase,
|
||||
modelTarget: 'Bom',
|
||||
templateId: 'boms',
|
||||
@@ -253,30 +249,26 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'items',
|
||||
title: 'Partidas (Permisos)',
|
||||
icon: FileText,
|
||||
group: 'Permisos',
|
||||
group: 'permisos',
|
||||
modelTarget: 'ItemPermission',
|
||||
templateId: 'part_numbers'
|
||||
},
|
||||
{
|
||||
id: 'headers',
|
||||
title: 'Encabezados (Permisos)',
|
||||
icon: FileText,
|
||||
group: 'Permisos',
|
||||
group: 'permisos',
|
||||
modelTarget: 'HeaderPermission',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'historical_fractions',
|
||||
title: 'Fracciones Históricas',
|
||||
icon: Calendar,
|
||||
modelTarget: 'HistoricalFraction',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'pedimentos',
|
||||
title: 'Pedimentos',
|
||||
icon: FileDigit,
|
||||
modelTarget: 'Pedimento',
|
||||
templateId: 'pedimentos',
|
||||
@@ -288,7 +280,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
export const transportesConfig: CsvUploadItem[] = [
|
||||
{
|
||||
id: 'transporters',
|
||||
title: 'Transportistas',
|
||||
icon: Ship,
|
||||
modelTarget: 'Transporter',
|
||||
templateId: 'transporters',
|
||||
@@ -296,7 +287,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'transports',
|
||||
title: 'Transportes',
|
||||
icon: Truck,
|
||||
modelTarget: 'Transport',
|
||||
templateId: 'transports',
|
||||
@@ -304,7 +294,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'drivers',
|
||||
title: 'Conductores',
|
||||
icon: User,
|
||||
modelTarget: 'Driver',
|
||||
templateId: 'drivers',
|
||||
@@ -312,7 +301,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'trailers',
|
||||
title: 'Trailers y Cajas',
|
||||
icon: Container,
|
||||
modelTarget: 'Trailer',
|
||||
templateId: 'trailers',
|
||||
@@ -326,27 +314,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
||||
// Impo Temp
|
||||
{
|
||||
id: 'imp_temp_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Impo. Temp.',
|
||||
group: 'impo_temp',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'imp_temp_header',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_temp_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Impo. Temp.',
|
||||
group: 'impo_temp',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'imp_temp_details',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_temp_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Impo. Temp.',
|
||||
group: 'impo_temp',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'imp_temp_series',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
@@ -354,27 +339,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
||||
// Impo Def
|
||||
{
|
||||
id: 'imp_def_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Impo. Def.',
|
||||
group: 'impo_def',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'imp_def_header',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_def_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Impo. Def.',
|
||||
group: 'impo_def',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'imp_def_details',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_def_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Impo. Def.',
|
||||
group: 'impo_def',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'imp_def_series',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
@@ -382,27 +364,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
||||
// Compras Mex
|
||||
{
|
||||
id: 'comp_mex_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Compras Mex.',
|
||||
group: 'cmex',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'cmex_header',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'comp_mex_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Compras Mex.',
|
||||
group: 'cmex',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'cmex_details',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'comp_mex_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Compras Mex.',
|
||||
group: 'cmex',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'cmex_series',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
@@ -415,70 +394,62 @@ export const exportacionConfig: CsvUploadItem[] = [
|
||||
// Expo Def / Cam. Reg.
|
||||
{
|
||||
id: 'exp_def_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'exp_def_header',
|
||||
layoutModule: 'layouts_csv/exportacion'
|
||||
},
|
||||
{
|
||||
id: 'exp_def_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'exp_def_details',
|
||||
layoutModule: 'layouts_csv/exportacion'
|
||||
},
|
||||
{
|
||||
id: 'exp_def_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'exp_def_series',
|
||||
layoutModule: 'layouts_csv/exportacion'
|
||||
},
|
||||
{
|
||||
id: 'exp_def_nodes',
|
||||
title: 'NODES',
|
||||
icon: Briefcase,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'Nodes',
|
||||
disabled: true,
|
||||
},
|
||||
// Expo Rep
|
||||
{
|
||||
id: 'exp_rep_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Expo. Rep.',
|
||||
group: 'expo_rep',
|
||||
modelTarget: 'InvoiceHeader',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'exp_rep_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Expo. Rep.',
|
||||
group: 'expo_rep',
|
||||
modelTarget: 'InvoiceSalesDetails',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'exp_rep_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Expo. Rep.',
|
||||
group: 'expo_rep',
|
||||
modelTarget: 'InvoiceSeries',
|
||||
disabled: true,
|
||||
},
|
||||
// Manifiesto
|
||||
{
|
||||
id: 'manifest_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Manifiesto',
|
||||
group: 'manifest',
|
||||
modelTarget: 'Manifest',
|
||||
disabled: true,
|
||||
},
|
||||
|
||||
46
frontend/src/lib/i18n/csv-msg.ts
Normal file
46
frontend/src/lib/i18n/csv-msg.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Textos CSV desde `csv-upload-messages.{en,es}.json` (rama `csv_upload` extraída de messages/).
|
||||
* Mantener en sync al editar `messages/*.json` (p. ej. volver a exportar la clave csv_upload).
|
||||
*/
|
||||
import enPack from './csv-upload-messages.en.json';
|
||||
import esPack from './csv-upload-messages.es.json';
|
||||
import { baseLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
type CsvUploadPack = Record<string, unknown>;
|
||||
|
||||
function pickPack(): CsvUploadPack {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = String(getLocale()).toLowerCase();
|
||||
} catch {
|
||||
raw = String(baseLocale).toLowerCase();
|
||||
}
|
||||
const root = raw.startsWith('en') ? enPack : esPack;
|
||||
return root as CsvUploadPack;
|
||||
}
|
||||
|
||||
function walk(root: Record<string, unknown>, keys: string[]): unknown {
|
||||
let cur: unknown = root;
|
||||
for (const k of keys) {
|
||||
if (cur === null || typeof cur !== 'object') return undefined;
|
||||
cur = (cur as Record<string, unknown>)[k];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/** Navega `csv_upload.a.b.c` a partir de `subPath` = `a.b.c`. */
|
||||
export function csvMsg(subPath: string): string {
|
||||
const pack = pickPack();
|
||||
if (!pack) return subPath;
|
||||
const v = walk(pack as Record<string, unknown>, subPath.split('.'));
|
||||
return typeof v === 'string' ? v : subPath;
|
||||
}
|
||||
|
||||
/** Sustituye `{clave}` en el string del mensaje. */
|
||||
export function csvFmt(subPath: string, vars: Record<string, string | number> = {}): string {
|
||||
let s = csvMsg(subPath);
|
||||
for (const [k, val] of Object.entries(vars)) {
|
||||
s = s.replaceAll(`{${k}}`, String(val));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
193
frontend/src/lib/i18n/csv-upload-messages.en.json
Normal file
193
frontend/src/lib/i18n/csv-upload-messages.en.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"page_title": "CSV import",
|
||||
"intro_help": "Left-click: upload CSV file. Right-click: download template.",
|
||||
"tab_catalogos": "Catalogs",
|
||||
"tab_transportes": "Transportation",
|
||||
"tab_importacion": "Import",
|
||||
"tab_exportacion": "Export",
|
||||
"section_catalogs": "General Catalogs",
|
||||
"section_transport": "Transportation",
|
||||
"section_import": "Import operations",
|
||||
"section_export": "Export operations",
|
||||
"params_header": "Global parameters",
|
||||
"config_prefix": "Settings",
|
||||
"soon": "Coming soon",
|
||||
"drop_here": "Drop the file!",
|
||||
"groups": {
|
||||
"permisos": "Permissions",
|
||||
"impo_temp": "Temporary import",
|
||||
"impo_def": "Definitive import",
|
||||
"cmex": "Mexican purchases",
|
||||
"expo_def": "Definitive export / regime change",
|
||||
"expo_rep": "Export replenishment",
|
||||
"manifest": "Manifest"
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"material_classes": "Classes",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"items": "Lines (permissions)",
|
||||
"headers": "Headers (permissions)",
|
||||
"historical_fractions": "Historical tariff fractions",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Carriers",
|
||||
"transports": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Header",
|
||||
"imp_temp_details": "Lines",
|
||||
"imp_temp_series": "Serial numbers",
|
||||
"imp_def_header": "Header",
|
||||
"imp_def_details": "Lines",
|
||||
"imp_def_series": "Serial numbers",
|
||||
"comp_mex_header": "Header",
|
||||
"comp_mex_details": "Lines",
|
||||
"comp_mex_series": "Serial numbers",
|
||||
"exp_def_header": "Header",
|
||||
"exp_def_details": "Lines",
|
||||
"exp_def_series": "Serial numbers",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Header",
|
||||
"exp_rep_details": "Lines",
|
||||
"exp_rep_series": "Serial numbers",
|
||||
"manifest_header": "Header"
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Load mode",
|
||||
"date_format": "Date format",
|
||||
"weight_unit": "Weight unit",
|
||||
"autonumber_series": "Autonumber lines/series",
|
||||
"load_subpartidas": "Load sub-lines",
|
||||
"recalculate_pedimento_date": "Recalculate pedimento date",
|
||||
"autonumber_remesas": "Autonumber consignments",
|
||||
"recalculate_dates": "Recalculate dates",
|
||||
"invoice_type": "Invoice type",
|
||||
"is_regime_change": "Regime change"
|
||||
},
|
||||
"options": {
|
||||
"update": "Update",
|
||||
"replace": "Replace",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"kgs": "Kilograms (kg)",
|
||||
"lbs": "Pounds (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL"
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Uploading CSV file",
|
||||
"scan": "Validating records on the server",
|
||||
"commit": "Saving records to the database",
|
||||
"upload_known": "Uploading file…",
|
||||
"upload_unknown": "Uploading file (unknown size in browser)…",
|
||||
"in_progress": "In progress…",
|
||||
"resume_hint": "Resuming import saved in this tab…",
|
||||
"rows_file": "File: ~{n} data row(s) — uploading (not yet validated on server)…",
|
||||
"rows_scan": "Records processed: {current} / {total}",
|
||||
"rows_commit": "Records saved: {current} / {total}",
|
||||
"rows_commit_fallback": "Saving to database… ({current} / {total} using last known total)"
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Invalid format. Only .csv files are allowed.",
|
||||
"download_loading": "Downloading template…",
|
||||
"download_ok": "Template downloaded.",
|
||||
"download_err": "Could not download the template.",
|
||||
"upload_err": "Could not upload the file.",
|
||||
"upload_err_generic": "Unexpected error uploading the file.",
|
||||
"scan_done": "Scan complete. Review the results.",
|
||||
"import_done": "Import completed. Review the record list.",
|
||||
"import_maybe_done": "Import may have completed. Review the record list.",
|
||||
"stale_job": "This import is no longer available (session expired or job removed). You can start a new upload.",
|
||||
"poll_err": "Could not fetch status",
|
||||
"commit_err": "Could not start import",
|
||||
"scan_alt": "Scan finished. If you do not see the modal, check the record list.",
|
||||
"finished_none": "No records inserted. Review the errors below.",
|
||||
"commit_warning_ok": "{inserted} inserted, {updated} updated. {skipped} rejected.",
|
||||
"commit_warning_none": "No records inserted or updated. {skipped} rejected.",
|
||||
"success_counts": "Import completed: {msg}",
|
||||
"warn_skipped": "{n} records rejected or skipped",
|
||||
"error_processing": "Processing error: {msg}",
|
||||
"n_inserted": "{n} inserted",
|
||||
"n_updated": "{n} updated",
|
||||
"err_fetch_scan_result": "Could not fetch the scan result. Check the results modal.",
|
||||
"err_unknown": "Unknown error",
|
||||
"err_processing_fallback": "Processing error. Check the modal or details."
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pending",
|
||||
"title": "Imports pending confirmation",
|
||||
"description": "Scans ready to save to the database. Expired jobs disappear when you refresh.",
|
||||
"refresh": "Refresh",
|
||||
"empty": "No pending imports for this company.",
|
||||
"checking": "Checking with the server…",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"resume": "Resume",
|
||||
"remove": "Remove",
|
||||
"profiles": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Classes",
|
||||
"vehicles": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Carriers",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Export operations",
|
||||
"imports": "Import operations"
|
||||
}
|
||||
},
|
||||
"config_empty": "No module-specific settings.",
|
||||
"modal": {
|
||||
"title_pending": "Import validation",
|
||||
"title_success": "Import successful",
|
||||
"title_warning": "Import with remarks",
|
||||
"desc_pending": "Review the preliminary analysis before confirming.",
|
||||
"desc_done": "The import process has finished.",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"invalid_rows": "Invalid",
|
||||
"errors": "Errors",
|
||||
"errors_heading": "Scan errors (fix in your CSV)",
|
||||
"errors_badge": "{shown} of {total} error(s)",
|
||||
"errors_truncated": "Download the CSV to see all errors.",
|
||||
"errors_missing_detail": "{count} row(s) had errors but details are not available. Ensure the server is up to date and upload again.",
|
||||
"scan_ok_title": "File validated successfully",
|
||||
"scan_ok_body": "All rows look correct and ready to import.",
|
||||
"scan_problems_title": "Problems found in the file",
|
||||
"scan_problems_body": "Fix the issues listed below in your CSV and upload again, or confirm to import only valid rows (invalid rows will be skipped).",
|
||||
"inserted": "Inserted",
|
||||
"updated": "Updated",
|
||||
"rejected": "Rejected",
|
||||
"rejected_hint": "See line-by-line detail in the table below.",
|
||||
"ref_gaps_title": "Reference gaps (FK / catalogs)",
|
||||
"ref_gaps_body": "There are {n} critical reference gap(s). Review catalogs and rejected rows before retrying.",
|
||||
"ref_state_title": "Reference state",
|
||||
"ref_state_ok": "References ready to operate (no critical gaps reported).",
|
||||
"ref_state_other": "No numeric gaps; review the server message if applicable.",
|
||||
"skipped_reasons_heading": "Rejection reasons summary",
|
||||
"commit_errors_heading": "Error detail",
|
||||
"rows_badge": "{n} rows",
|
||||
"importing_records": "Importing records…",
|
||||
"cancel_operation": "Cancel",
|
||||
"processing": "Processing…",
|
||||
"confirm_load": "Confirm import",
|
||||
"close": "Close",
|
||||
"th_line": "Line",
|
||||
"th_column": "Column",
|
||||
"th_message": "Message",
|
||||
"th_solution": "Solution",
|
||||
"th_reference": "Reference",
|
||||
"th_reason": "Reason",
|
||||
"download_csv": "Download CSV"
|
||||
}
|
||||
}
|
||||
193
frontend/src/lib/i18n/csv-upload-messages.es.json
Normal file
193
frontend/src/lib/i18n/csv-upload-messages.es.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"page_title": "Importación CSV",
|
||||
"intro_help": "Clic izquierdo: cargar archivo CSV. Clic derecho: descargar plantilla.",
|
||||
"tab_catalogos": "Catálogos",
|
||||
"tab_transportes": "Transportes",
|
||||
"tab_importacion": "Importación",
|
||||
"tab_exportacion": "Exportación",
|
||||
"section_catalogs": "Catalogos Generales",
|
||||
"section_transport": "Transportes",
|
||||
"section_import": "Operaciones de importación",
|
||||
"section_export": "Operaciones de exportación",
|
||||
"params_header": "Parámetros globales",
|
||||
"config_prefix": "Configuración",
|
||||
"soon": "Próximamente",
|
||||
"drop_here": "¡Suelta el archivo!",
|
||||
"groups": {
|
||||
"permisos": "Permisos",
|
||||
"impo_temp": "Impo. temp.",
|
||||
"impo_def": "Impo. def.",
|
||||
"cmex": "Compras mex.",
|
||||
"expo_def": "Expo. def./Cam. reg.",
|
||||
"expo_rep": "Expo. rep.",
|
||||
"manifest": "Manifiesto"
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"material_classes": "Clases",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"items": "Partidas (permisos)",
|
||||
"headers": "Encabezados (permisos)",
|
||||
"historical_fractions": "Fracciones históricas",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Transportistas",
|
||||
"transports": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Encabezado",
|
||||
"imp_temp_details": "Partidas",
|
||||
"imp_temp_series": "Series",
|
||||
"imp_def_header": "Encabezado",
|
||||
"imp_def_details": "Partidas",
|
||||
"imp_def_series": "Series",
|
||||
"comp_mex_header": "Encabezado",
|
||||
"comp_mex_details": "Partidas",
|
||||
"comp_mex_series": "Series",
|
||||
"exp_def_header": "Encabezado",
|
||||
"exp_def_details": "Partidas",
|
||||
"exp_def_series": "Series",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Encabezado",
|
||||
"exp_rep_details": "Partidas",
|
||||
"exp_rep_series": "Series",
|
||||
"manifest_header": "Encabezado"
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Modo de carga",
|
||||
"date_format": "Formato de fecha",
|
||||
"weight_unit": "Unidad de peso",
|
||||
"autonumber_series": "Autonumerar partidas/series",
|
||||
"load_subpartidas": "Levantar subpartidas",
|
||||
"recalculate_pedimento_date": "Recalcular fecha pedimento",
|
||||
"autonumber_remesas": "Autonumerar remesas",
|
||||
"recalculate_dates": "Recalcular fechas",
|
||||
"invoice_type": "Tipo de factura",
|
||||
"is_regime_change": "Es cambio de régimen"
|
||||
},
|
||||
"options": {
|
||||
"update": "Actualizar",
|
||||
"replace": "Reemplazar",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"kgs": "Kilos (kg)",
|
||||
"lbs": "Libras (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL"
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Subiendo archivo CSV",
|
||||
"scan": "Validando registros en el servidor",
|
||||
"commit": "Grabando registros en base de datos",
|
||||
"upload_known": "Subiendo archivo…",
|
||||
"upload_unknown": "Subiendo archivo (tamaño desconocido en el navegador)…",
|
||||
"in_progress": "En proceso…",
|
||||
"resume_hint": "Reanudando la importación guardada en esta pestaña…",
|
||||
"rows_file": "Archivo: ~{n} fila(s) de datos — subiendo (aún no se validan registros en servidor)…",
|
||||
"rows_scan": "Registros procesados: {current} / {total}",
|
||||
"rows_commit": "Registros grabados: {current} / {total}",
|
||||
"rows_commit_fallback": "Grabando en base de datos… ({current} / {total} según último total conocido)"
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Formato inválido. Solo se permiten archivos .csv",
|
||||
"download_loading": "Descargando plantilla…",
|
||||
"download_ok": "Plantilla descargada.",
|
||||
"download_err": "Error al descargar la plantilla",
|
||||
"upload_err": "Error al subir el archivo",
|
||||
"upload_err_generic": "Error inesperado al subir el archivo",
|
||||
"scan_done": "Escaneo completado. Revisa los resultados.",
|
||||
"import_done": "Importación completada. Revisa el listado de registros.",
|
||||
"import_maybe_done": "La importación pudo completarse. Revisa el listado de registros.",
|
||||
"stale_job": "Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.",
|
||||
"poll_err": "Error al consultar el estado",
|
||||
"commit_err": "Error al iniciar la importación",
|
||||
"scan_alt": "El escaneo terminó. Si no ves el modal, revisa el listado de registros.",
|
||||
"finished_none": "No se insertaron registros. Revisa los errores a continuación.",
|
||||
"commit_warning_ok": "{inserted} insertados, {updated} actualizados. {skipped} rechazados.",
|
||||
"commit_warning_none": "No se insertaron ni actualizaron registros. {skipped} rechazados.",
|
||||
"success_counts": "Importación completada: {msg}",
|
||||
"warn_skipped": "{n} registros fueron rechazados u omitidos",
|
||||
"error_processing": "Error en el procesamiento: {msg}",
|
||||
"n_inserted": "{n} insertados",
|
||||
"n_updated": "{n} actualizados",
|
||||
"err_fetch_scan_result": "Error al obtener el resultado. Revisa el modal de resultados.",
|
||||
"err_unknown": "Error desconocido",
|
||||
"err_processing_fallback": "Error en el procesamiento. Revisa el modal o los detalles."
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pendientes",
|
||||
"title": "Importaciones pendientes de confirmar",
|
||||
"description": "Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al actualizar.",
|
||||
"refresh": "Actualizar",
|
||||
"empty": "No hay importaciones pendientes para esta empresa.",
|
||||
"checking": "Comprobando con el servidor…",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidas",
|
||||
"resume": "Reanudar",
|
||||
"remove": "Quitar",
|
||||
"profiles": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Clases",
|
||||
"vehicles": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Transportistas",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Exportación (operaciones)",
|
||||
"imports": "Importación (operaciones)"
|
||||
}
|
||||
},
|
||||
"config_empty": "No hay configuraciones específicas para este módulo.",
|
||||
"modal": {
|
||||
"title_pending": "Validación de importación",
|
||||
"title_success": "Importación exitosa",
|
||||
"title_warning": "Importación con observaciones",
|
||||
"desc_pending": "Revise el análisis preliminar antes de confirmar la carga de datos.",
|
||||
"desc_done": "El proceso de importación ha finalizado.",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidos",
|
||||
"invalid_rows": "Inválidos",
|
||||
"errors": "Errores",
|
||||
"errors_heading": "Detalle de errores (para corregir en el CSV)",
|
||||
"errors_badge": "{shown} de {total} error(es)",
|
||||
"errors_truncated": "Para consultar el resto de errores, descargue el CSV.",
|
||||
"errors_missing_detail": "Se detectaron {count} fila(s) con errores pero el detalle no está disponible. Asegúrese de que el servidor esté actualizado y vuelva a subir el archivo.",
|
||||
"scan_ok_title": "Archivo validado correctamente",
|
||||
"scan_ok_body": "Todos los registros parecen correctos y listos para importar.",
|
||||
"scan_problems_title": "Se detectaron problemas en el archivo",
|
||||
"scan_problems_body": "Corrija los datos indicados abajo en su CSV y vuelva a subir, o confirme para importar solo las filas válidas (las erróneas se omitirán).",
|
||||
"inserted": "Insertados",
|
||||
"updated": "Actualizados",
|
||||
"rejected": "Rechazados",
|
||||
"rejected_hint": "Revisa el detalle por línea en la tabla inferior.",
|
||||
"ref_gaps_title": "Brechas de referencia (FK / catálogos)",
|
||||
"ref_gaps_body": "Hay {n} brecha(s) crítica(s) de referencia. Revisa catálogos y el detalle de filas rechazadas antes de reintentar.",
|
||||
"ref_state_title": "Estado de referencias",
|
||||
"ref_state_ok": "Referencias listas para operar (sin brechas críticas reportadas).",
|
||||
"ref_state_other": "Sin brechas numéricas; revisa el mensaje del servidor si aplica.",
|
||||
"skipped_reasons_heading": "Resumen de motivos de rechazo",
|
||||
"commit_errors_heading": "Detalle de errores",
|
||||
"rows_badge": "{n} filas",
|
||||
"importing_records": "Importando registros…",
|
||||
"cancel_operation": "Cancelar",
|
||||
"processing": "Procesando…",
|
||||
"confirm_load": "Confirmar carga",
|
||||
"close": "Cerrar",
|
||||
"th_line": "Línea",
|
||||
"th_column": "Columna",
|
||||
"th_message": "Mensaje",
|
||||
"th_solution": "Solución",
|
||||
"th_reference": "Referencia",
|
||||
"th_reason": "Motivo",
|
||||
"download_csv": "Descargar CSV"
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
import { totalSkippedFromCommit } from '$lib/csv-import-commit-metrics';
|
||||
import { countCsvDataRows } from '$lib/csv-upload-row-count';
|
||||
import CsvPendingImportsSheet from '$lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte';
|
||||
import { csvFmt, csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
/** Intenta extraer un objeto tipo scan desde string tipo repr de Python. */
|
||||
function parsePythonReprScan(s: string): Record<string, unknown> | null {
|
||||
@@ -240,11 +241,11 @@
|
||||
const csvProgressStepTitle = $derived.by(() => {
|
||||
switch (csvProgressPhase) {
|
||||
case 'upload':
|
||||
return 'Subiendo archivo CSV';
|
||||
return csvMsg('progress.upload');
|
||||
case 'scan':
|
||||
return 'Validando registros en el servidor';
|
||||
return csvMsg('progress.scan');
|
||||
case 'commit':
|
||||
return 'Grabando registros en base de datos';
|
||||
return csvMsg('progress.commit');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -261,17 +262,26 @@
|
||||
switch (csvProgressPhase) {
|
||||
case 'upload':
|
||||
if (csvImportRowTotal > 0) {
|
||||
return `Archivo: ~${csvImportRowTotal} fila(s) de datos — subiendo (aún no se validan registros en servidor)…`;
|
||||
return csvFmt('progress.rows_file', { n: csvImportRowTotal });
|
||||
}
|
||||
return uploadLengthComputable
|
||||
? 'Subiendo archivo…'
|
||||
: 'Subiendo archivo (tamaño desconocido en el navegador)…';
|
||||
? csvMsg('progress.upload_known')
|
||||
: csvMsg('progress.upload_unknown');
|
||||
case 'scan':
|
||||
return `Registros procesados: ${Math.min(scanProgressCurrent, denom)} / ${denom}`;
|
||||
return csvFmt('progress.rows_scan', {
|
||||
current: Math.min(scanProgressCurrent, denom),
|
||||
total: denom
|
||||
});
|
||||
case 'commit':
|
||||
return scanProgressTotal > 0
|
||||
? `Registros grabados: ${Math.min(scanProgressCurrent, denom)} / ${denom}`
|
||||
: `Grabando en base de datos… (${Math.min(scanProgressCurrent, denom)} / ${denom} según último total conocido)`;
|
||||
? csvFmt('progress.rows_commit', {
|
||||
current: Math.min(scanProgressCurrent, denom),
|
||||
total: denom
|
||||
})
|
||||
: csvFmt('progress.rows_commit_fallback', {
|
||||
current: Math.min(scanProgressCurrent, denom),
|
||||
total: denom
|
||||
});
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -306,7 +316,7 @@
|
||||
|
||||
const csvFooterAriaValueText = $derived(
|
||||
csvFooterProgressIndeterminate
|
||||
? `${csvProgressStepTitle}. ${csvProgressDetailLine || 'En proceso…'}`
|
||||
? `${csvProgressStepTitle}. ${csvProgressDetailLine || csvMsg('progress.in_progress')}`
|
||||
: csvProgressDetailLine
|
||||
? `${csvProgressStepTitle}, ${csvFooterPercentLabel}. ${csvProgressDetailLine}`
|
||||
: `${csvProgressStepTitle}, ${csvFooterPercentLabel}`
|
||||
@@ -591,7 +601,7 @@
|
||||
csvResumeOverlayHint = false;
|
||||
skipScanCompleteToastOnce = false;
|
||||
scanPhaseJobId = null;
|
||||
currentImportLabel = (config.title && String(config.title).trim()) || null;
|
||||
currentImportLabel = csvMsg(`items.${config.id}`);
|
||||
|
||||
const onCsvFileUploadProgress = (e: { loaded: number; total: number }) => {
|
||||
if (e.total > 0) {
|
||||
@@ -626,12 +636,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -646,12 +656,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -673,12 +683,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -698,12 +708,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -721,12 +731,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -744,12 +754,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -764,12 +774,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -787,12 +797,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -810,12 +820,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -834,12 +844,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -854,12 +864,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -888,12 +898,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -915,12 +925,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
}
|
||||
@@ -943,11 +953,9 @@
|
||||
const errMsg = res.error || '';
|
||||
if (isStaleImportJobError(res.status, errMsg)) {
|
||||
removePendingLinkedToCurrentFlow();
|
||||
toast.info(
|
||||
'Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.'
|
||||
);
|
||||
toast.info(csvMsg('toast.stale_job'));
|
||||
} else {
|
||||
toast.error(errMsg || 'Error al consultar el estado');
|
||||
toast.error(errMsg || csvMsg('toast.poll_err'));
|
||||
}
|
||||
isUploading = false;
|
||||
clearCsvImportSession();
|
||||
@@ -978,7 +986,7 @@
|
||||
if (skipScanCompleteToastOnce) {
|
||||
skipScanCompleteToastOnce = false;
|
||||
} else {
|
||||
toast.success('Escaneo completado. Revisa los resultados.');
|
||||
toast.success(csvMsg('toast.scan_done'));
|
||||
}
|
||||
isUploading = false;
|
||||
} else if (res.data?.status === 'failed' || res.data?.status === 'FAILURE') {
|
||||
@@ -996,7 +1004,7 @@
|
||||
commitResults = parsedCommit;
|
||||
showResultModal = true;
|
||||
finalizeCommitAndClearPending();
|
||||
toast.success('Importación completada. Revisa el listado de registros.');
|
||||
toast.success(csvMsg('toast.import_done'));
|
||||
isUploading = false;
|
||||
clearCsvImportSession();
|
||||
currentJobId = null;
|
||||
@@ -1026,7 +1034,7 @@
|
||||
if (skipScanCompleteToastOnce) {
|
||||
skipScanCompleteToastOnce = false;
|
||||
} else {
|
||||
toast.success('Escaneo completado. Revisa los resultados.');
|
||||
toast.success(csvMsg('toast.scan_done'));
|
||||
}
|
||||
isUploading = false;
|
||||
} else {
|
||||
@@ -1036,30 +1044,32 @@
|
||||
(errRaw.includes('waiting_confirmation') || (errRaw.includes('total_rows') && errRaw.includes('job_id')));
|
||||
if (looksLikeScanInError) {
|
||||
removePendingLinkedToCurrentFlow();
|
||||
toast.info('El escaneo terminó. Si no ves el modal, revisa el listado de registros.');
|
||||
toast.info(csvMsg('toast.scan_alt'));
|
||||
isUploading = false;
|
||||
clearCsvImportSession();
|
||||
currentJobId = null;
|
||||
return;
|
||||
}
|
||||
let errText: string;
|
||||
const maybeDoneMsg = csvMsg('toast.import_maybe_done');
|
||||
if (typeof errRaw === 'string') {
|
||||
errText =
|
||||
errRaw.includes('finished') && errRaw.includes('inserted')
|
||||
? 'La importación pudo completarse. Revisa el listado de registros.'
|
||||
? maybeDoneMsg
|
||||
: errRaw.includes("'status'") && errRaw.includes('waiting_confirmation')
|
||||
? 'Error al obtener el resultado. Revisa el modal de resultados.'
|
||||
? csvMsg('toast.err_fetch_scan_result')
|
||||
: errRaw;
|
||||
} else if (typeof errRaw === 'object' && errRaw !== null) {
|
||||
errText = (errRaw as { message?: string })?.message || 'Error en el procesamiento. Revisa el modal o los detalles.';
|
||||
errText =
|
||||
(errRaw as { message?: string })?.message || csvMsg('toast.err_processing_fallback');
|
||||
} else {
|
||||
errText = 'Error desconocido';
|
||||
errText = csvMsg('toast.err_unknown');
|
||||
}
|
||||
// No mostrar como error si el mensaje indica éxito
|
||||
if (errText.includes('pudo completarse') || errText.includes('Revisa el listado')) {
|
||||
if (errText === maybeDoneMsg || errText.includes(maybeDoneMsg)) {
|
||||
toast.success(errText);
|
||||
} else {
|
||||
toast.error('Error en el procesamiento: ' + errText);
|
||||
toast.error(csvFmt('toast.error_processing', { msg: errText }));
|
||||
}
|
||||
isUploading = false;
|
||||
removePendingLinkedToCurrentFlow();
|
||||
@@ -1082,12 +1092,16 @@
|
||||
if (totalOk === 0) {
|
||||
toast.error(
|
||||
backendMessage ||
|
||||
`No se insertaron ni actualizaron registros. ${totalSkipped} fueron rechazados.`
|
||||
csvFmt('toast.commit_warning_none', { skipped: totalSkipped })
|
||||
);
|
||||
} else {
|
||||
toast.warning(
|
||||
backendMessage ||
|
||||
`Se aplicaron ${totalOk} registros (${inserted} insertados, ${updated} actualizados). ${totalSkipped} rechazados.`
|
||||
csvFmt('toast.commit_warning_ok', {
|
||||
inserted,
|
||||
updated,
|
||||
skipped: totalSkipped
|
||||
})
|
||||
);
|
||||
}
|
||||
finalizeCommitAndClearPending();
|
||||
@@ -1100,15 +1114,15 @@
|
||||
const totalSkipped = totalSkippedFromCommit(res.data as Record<string, unknown>);
|
||||
|
||||
if (inserted > 0 || updated > 0) {
|
||||
const parts = [];
|
||||
if (inserted > 0) parts.push(`${inserted} insertados`);
|
||||
if (updated > 0) parts.push(`${updated} actualizados`);
|
||||
toast.success(`Importación completada: ${parts.join(', ')}`);
|
||||
const parts: string[] = [];
|
||||
if (inserted > 0) parts.push(csvFmt('toast.n_inserted', { n: inserted }));
|
||||
if (updated > 0) parts.push(csvFmt('toast.n_updated', { n: updated }));
|
||||
toast.success(csvFmt('toast.success_counts', { msg: parts.join(', ') }));
|
||||
if (totalSkipped > 0) {
|
||||
toast.warning(`${totalSkipped} registros fueron rechazados u omitidos`);
|
||||
toast.warning(csvFmt('toast.warn_skipped', { n: totalSkipped }));
|
||||
}
|
||||
} else {
|
||||
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
||||
toast.error(csvMsg('toast.finished_none'));
|
||||
}
|
||||
finalizeCommitAndClearPending();
|
||||
isUploading = false;
|
||||
@@ -1136,26 +1150,26 @@
|
||||
<!-- Single scroll: content scrolls here; padding at bottom reserves space for fixed params bar -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto p-4 md:p-8 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 class="text-lg font-semibold md:text-2xl">Importación Masiva de Datos (CSV)</h1>
|
||||
<h1 class="text-lg font-semibold md:text-2xl">{csvMsg('page_title')}</h1>
|
||||
<CsvPendingImportsSheet companyId={companyStore.activeCompany?.id} onResume={resumePendingImport} />
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Click izquierdo: cargar archivo CSV. Click derecho: descargar estructura (plantilla).
|
||||
{csvMsg('intro_help')}
|
||||
</p>
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-2 md:grid-cols-4 lg:w-auto">
|
||||
<Tabs.Trigger value="catalogos">Catálogos</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transportes">Transportes</Tabs.Trigger>
|
||||
<Tabs.Trigger value="importacion">Importación</Tabs.Trigger>
|
||||
<Tabs.Trigger value="exportacion">Exportación</Tabs.Trigger>
|
||||
<Tabs.Trigger value="catalogos">{csvMsg('tab_catalogos')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transportes">{csvMsg('tab_transportes')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="importacion">{csvMsg('tab_importacion')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="exportacion">{csvMsg('tab_exportacion')}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<div class="mt-6">
|
||||
<Tabs.Content value="catalogos" class="space-y-4">
|
||||
<!-- Catálogos: backend layouts_csv (customs_brokers, clients_and_providers, parts, boms, etc.) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Catálogos Generales</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_catalogs')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={catalogosConfig}
|
||||
@@ -1167,7 +1181,7 @@
|
||||
<Tabs.Content value="transportes" class="space-y-4">
|
||||
<!-- Logística: backend layouts_csv (vehicles, drivers, trailers); transportistas sin layouts_csv -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Logística y Transporte</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_transport')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={transportesConfig}
|
||||
@@ -1179,7 +1193,7 @@
|
||||
<Tabs.Content value="importacion" class="space-y-4">
|
||||
<!-- Operaciones de Importación: backend layouts_csv/facturas (api.imports) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Operaciones de Importación</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_import')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={importacionConfig}
|
||||
@@ -1190,7 +1204,7 @@
|
||||
|
||||
<Tabs.Content value="exportacion" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Operaciones de Exportación</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_export')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={exportacionConfig}
|
||||
@@ -1224,7 +1238,7 @@
|
||||
aria-busy="true"
|
||||
>
|
||||
{#if csvResumeOverlayHint}
|
||||
<p class="text-xs text-muted-foreground">Reanudando la importación guardada en esta pestaña…</p>
|
||||
<p class="text-xs text-muted-foreground">{csvMsg('progress.resume_hint')}</p>
|
||||
{/if}
|
||||
{#if currentImportLabel}
|
||||
<p class="text-xs font-medium text-foreground">{currentImportLabel}</p>
|
||||
@@ -1340,7 +1354,7 @@
|
||||
pollStatus();
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Error al iniciar la importación');
|
||||
toast.error(csvMsg('toast.commit_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -40,16 +40,9 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 5173, // fija el puerto
|
||||
host: true, // escucha en 0.0.0.0
|
||||
allowedHosts: [
|
||||
'anexo76-dev.aduanasoft.com',
|
||||
// Requeridos para dev local, healthcheck del contenedor y E2E (Playwright
|
||||
// con --network host apunta a 127.0.0.1:5173). Al definir allowedHosts,
|
||||
// Vite 5.1+ reemplaza el default ['.localhost'] y bloquea todo lo que no
|
||||
// esté aquí, devolviendo 403 "Blocked request".
|
||||
'127.0.0.1',
|
||||
'localhost',
|
||||
// 'otro-host.com' si necesitas más
|
||||
],
|
||||
// Lista explícita + peticiones internas (p. ej. chunks JSON `?import`) pueden usar
|
||||
// Hosts distintos y recibir 403. `true` permite cualquier Host en dev.
|
||||
allowedHosts: true,
|
||||
proxy: {
|
||||
'/api/uploads': {
|
||||
target: 'http://backend:8000',
|
||||
|
||||
Reference in New Issue
Block a user