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 csv
|
||||||
import io
|
import io
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Set
|
||||||
|
|
||||||
# Importar configs de cada módulo
|
# Importar configs de cada módulo
|
||||||
from api.v1.modules.a76.layouts_csv.facturas.template_config import (
|
from api.v1.modules.a76.layouts_csv.facturas.template_config import (
|
||||||
TEMPLATE_COLUMNS as IMPORTS_TEMPLATE_COLUMNS,
|
|
||||||
_resolve_template_columns as resolve_imports_template,
|
_resolve_template_columns as resolve_imports_template,
|
||||||
)
|
)
|
||||||
from api.v1.modules.a76.layouts_csv.parts.template_config import (
|
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_temp_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||||
"imp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
"imp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||||
"cmex_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
"cmex_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||||
|
"exp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"},
|
||||||
# Catálogos y transportes
|
# Catálogos y transportes
|
||||||
"customs_brokers": {"TIPO", "CLAVE", "NOMBRE"},
|
"customs_brokers": {"TIPO", "CLAVE", "NOMBRE"},
|
||||||
"clients_providers": {"PROCEDENCIA", "SHORT_NAME", "NOMBRE", "RFC"},
|
"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]:
|
def _compute_required_indices_map(base_rows: Dict[str, List[str]]) -> Dict[str, Set[int]]:
|
||||||
"""Prefija '* ' a cabeceras siempre obligatorias para la plantilla."""
|
"""Índices de columnas obligatorias (según plantilla ES/base), para marcar * en cualquier idioma."""
|
||||||
required_headers = ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE.get(template_id)
|
out: Dict[str, Set[int]] = {}
|
||||||
if not required_headers:
|
for tid, headers in base_rows.items():
|
||||||
return headers
|
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] = []
|
out: List[str] = []
|
||||||
for header in headers:
|
for i, h in enumerate(headers):
|
||||||
norm_header = _normalize_header_for_match(header)
|
if i in idx_set and h and not str(h).startswith("* "):
|
||||||
if norm_header and norm_header in required_norm:
|
out.append(f"* {h}")
|
||||||
if header.startswith("* "):
|
|
||||||
out.append(header)
|
|
||||||
else:
|
|
||||||
out.append(f"* {header}")
|
|
||||||
else:
|
else:
|
||||||
out.append(header)
|
out.append(h)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -231,6 +243,7 @@ def _build_registry() -> Dict[str, List[str]]:
|
|||||||
"imp_def_series",
|
"imp_def_series",
|
||||||
"exp_def_header",
|
"exp_def_header",
|
||||||
"exp_def_details",
|
"exp_def_details",
|
||||||
|
"exp_def_series",
|
||||||
"cmex_header",
|
"cmex_header",
|
||||||
"cmex_details",
|
"cmex_details",
|
||||||
"cmex_series",
|
"cmex_series",
|
||||||
@@ -292,7 +305,9 @@ def _build_registry() -> Dict[str, List[str]]:
|
|||||||
return registry
|
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)
|
# Nombre de archivo sugerido para descarga (sin path)
|
||||||
TEMPLATE_FILENAMES: Dict[str, str] = {
|
TEMPLATE_FILENAMES: Dict[str, str] = {
|
||||||
@@ -320,15 +335,65 @@ TEMPLATE_FILENAMES: Dict[str, str] = {
|
|||||||
"cmex_series": "EstructuraSeriesFacComprasMex.csv",
|
"cmex_series": "EstructuraSeriesFacComprasMex.csv",
|
||||||
"exp_def_header": "EstructuraEncFacExpoCamReg.csv",
|
"exp_def_header": "EstructuraEncFacExpoCamReg.csv",
|
||||||
"exp_def_details": "EstructuraParExpoCamReg.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]]:
|
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."""
|
"""Cabeceras como en descarga ES histórica, con prefijo * en obligatorias."""
|
||||||
headers = _TEMPLATE_HEADERS.get(template_id)
|
raw = get_download_headers(template_id, "es")
|
||||||
if headers is None:
|
if raw is None:
|
||||||
return 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:
|
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")
|
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.
|
Genera el contenido CSV (solo fila de cabeceras) para el template_id.
|
||||||
UTF-8, opcionalmente con BOM para Excel.
|
UTF-8, opcionalmente con BOM para Excel.
|
||||||
"""
|
"""
|
||||||
headers = get_template_headers(template_id)
|
headers = get_download_headers(template_id, locale)
|
||||||
if not headers:
|
if not headers:
|
||||||
return None
|
return None
|
||||||
|
headers = _apply_required_prefix_indices(template_id, headers)
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
writer = csv.writer(buf, lineterminator="\n")
|
writer = csv.writer(buf, lineterminator="\n")
|
||||||
writer.writerow(headers)
|
writer.writerow(headers)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Rutas para descargar plantillas CSV generadas desde código (sin archivos XLS/XLSX).
|
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 fastapi.responses import Response
|
||||||
|
|
||||||
from core.security import get_current_user
|
from core.security import get_current_user
|
||||||
@@ -12,17 +12,23 @@ from .registry import generate_csv_content, get_template_filename
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
_LOCALE_ALLOWED = frozenset({"es", "en"})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{template_id}", response_class=Response)
|
@router.get("/{template_id}", response_class=Response)
|
||||||
async def download_csv_template(
|
async def download_csv_template(
|
||||||
template_id: str,
|
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),
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Devuelve un CSV con solo la fila de cabeceras para la plantilla indicada.
|
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:
|
if content is None:
|
||||||
raise HTTPException(status_code=404, detail=f"Plantilla desconocida: {template_id}")
|
raise HTTPException(status_code=404, detail=f"Plantilla desconocida: {template_id}")
|
||||||
filename = get_template_filename(template_id)
|
filename = get_template_filename(template_id)
|
||||||
@@ -31,5 +37,7 @@ async def download_csv_template(
|
|||||||
media_type="text/csv; charset=utf-8",
|
media_type="text/csv; charset=utf-8",
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
"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 typing import Dict, List, Any
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"boms": [
|
"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("boms")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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
|
# Cambio de régimen: cam_reg_header, cam_reg_details
|
||||||
# Regularización: regulariz_header, regulariz_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]]]:
|
def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
return TEMPLATE_COLUMNS.get(template_id)
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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.cell_value import cell_to_str
|
||||||
from ..common import csv_reader as common_csv_reader
|
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)
|
# 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(
|
def detect_headers_or_data(
|
||||||
file_path: str,
|
file_path: str,
|
||||||
@@ -71,6 +80,19 @@ TEMPLATE_DOWNLOAD_HEADERS: List[str] = [
|
|||||||
"CODIGO DE PRODUCTO/SERVICIO CP",
|
"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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"material_classes": [
|
"material_classes": [
|
||||||
{"canonical": "CLASE", "aliases": ["CLAVE CLASE", "CLASS", "CODIGO", "CLASE CODIGO"]},
|
{"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("material_classes")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"client_providers": [
|
"client_providers": [
|
||||||
# Col A - Procedencia (E=Extranjero, N=Nacional)
|
# 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"]},
|
{
|
||||||
# Col B - Tipo Cliente (C/P/A)
|
"canonical": "PROCEDENCIA",
|
||||||
{"canonical": "TIPO", "aliases": ["CLIENT_OR_PROVIDER", "TIPO ENTIDAD", "CLIENTE O PROVEEDOR"]},
|
"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)
|
# Col C - Clave cliente/proveedor (máx 8 Clarion)
|
||||||
{"canonical": "SHORT_NAME", "aliases": ["CLAVE", "CLAVE CORTA", "NOMBRE CORTO", "SIGLAS"]},
|
{"canonical": "SHORT_NAME", "aliases": ["CLAVE", "CLAVE CORTA", "NOMBRE CORTO", "SIGLAS"]},
|
||||||
# Col D - Nombre
|
# 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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
"""normalized_header -> canonical_name para plantilla client_providers."""
|
"""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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"customs_brokers": [
|
"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)
|
# Orden oficial de columnas (para CSV sin encabezado o detección)
|
||||||
CUSTOMS_BROKERS_FIELDNAMES_ORDER = [
|
CUSTOMS_BROKERS_FIELDNAMES_ORDER = [
|
||||||
"TIPO",
|
"TIPO",
|
||||||
@@ -84,6 +97,8 @@ def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
|||||||
lookup[normalize_header_fn(canonical)] = canonical
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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):
|
for idx, name in enumerate(CUSTOMS_BROKERS_FIELDNAMES_ORDER):
|
||||||
lookup[normalize_header_fn(f"_COL_{idx}_")] = name
|
lookup[normalize_header_fn(f"_COL_{idx}_")] = name
|
||||||
return lookup
|
return lookup
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
# common validators, mappers, fk_loader for drivers CSV import
|
"""Drivers CSV helpers (validators, FK loader). Import submodules explicitly to avoid heavy deps at package init."""
|
||||||
from .fk_loader import load_drivers_fk_sets
|
|
||||||
|
|
||||||
__all__ = ["load_drivers_fk_sets"]
|
|
||||||
|
|||||||
@@ -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 typing import Dict, List, Any
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"drivers": [
|
"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("drivers")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"exchange_rates": [
|
"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]:
|
def build_normalized_lookup(normalize_header_fn, template_id: str = "exchange_rates") -> Dict[str, str]:
|
||||||
"""normalized_header -> canonical_name para plantilla exchange_rates."""
|
"""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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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)
|
# Columnas para encabezado y partidas de exportación (EstructuraEncFacExpoCamReg / EstructuraParExpoCamReg)
|
||||||
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
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]]]:
|
def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
return TEMPLATE_COLUMNS.get(template_id)
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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).
|
# 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
|
# 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]]]:
|
def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
cols = TEMPLATE_COLUMNS.get(template_id)
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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.cell_value import cell_to_str
|
||||||
from ..common import csv_reader as common_csv_reader
|
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)
|
# 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(
|
def detect_headers_or_data(
|
||||||
@@ -76,6 +85,27 @@ TEMPLATE_DOWNLOAD_HEADERS: List[str] = [
|
|||||||
"RUTA DE LA IMAGEN",
|
"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
|
# Para lectura cuando la primera fila es dato (sin cabecera): nombres únicos para columnas I y J
|
||||||
TEMPLATE_FIELDNAMES_FOR_READING: List[str] = [
|
TEMPLATE_FIELDNAMES_FOR_READING: List[str] = [
|
||||||
"NUMERO DE PARTE",
|
"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("part_numbers")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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.
|
# 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.
|
# 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.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
|
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.
|
# Orden de columnas para CSV sin cabecera (primera fila = datos). Usado por detect_headers_or_data.
|
||||||
PEDIMENTOS_TEMPLATE_ORDER: List[str] = [
|
PEDIMENTOS_TEMPLATE_ORDER: List[str] = [
|
||||||
item["canonical"] for item in TEMPLATE_COLUMNS["pedimentos"]
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"trailers": [
|
"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("trailers")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"transporters": [
|
"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("transporters")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str as _cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"us_tariff_fractions": [
|
"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])
|
# 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"]]
|
TEMPLATE_ORDER = [item["canonical"] for item in TEMPLATE_COLUMNS["us_tariff_fractions"]]
|
||||||
DESFASE_COLUMN_INDEX = 7
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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 typing import Dict, List, Any
|
||||||
|
|
||||||
from ..common.cell_value import cell_to_str
|
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]]] = {
|
TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||||
"vehicles": [
|
"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]:
|
def build_normalized_lookup(normalize_header_fn) -> Dict[str, str]:
|
||||||
cols = TEMPLATE_COLUMNS.get("vehicles")
|
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
|
lookup[normalize_header_fn(canonical)] = canonical
|
||||||
for alias in item.get("aliases") or []:
|
for alias in item.get("aliases") or []:
|
||||||
lookup[normalize_header_fn(alias)] = canonical
|
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
|
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": {
|
"sidebar": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
"management_label": "Management",
|
||||||
|
"bulk_upload": {
|
||||||
|
"title": "Bulk uploads",
|
||||||
|
"entry": "CSV import"
|
||||||
|
},
|
||||||
"reference_data": {
|
"reference_data": {
|
||||||
"title": "Fixed Catalogs",
|
"title": "Fixed Catalogs",
|
||||||
"codes_pedimento_regimen": "Pedimento and Regime Codes",
|
"codes_pedimento_regimen": "Pedimento and Regime Codes",
|
||||||
@@ -1601,5 +1606,198 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"accept": "Accept"
|
"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": {
|
"sidebar": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
"management_label": "Gestión",
|
||||||
|
"bulk_upload": {
|
||||||
|
"title": "Cargas masivas",
|
||||||
|
"entry": "Importación CSV"
|
||||||
|
},
|
||||||
"reference_data": {
|
"reference_data": {
|
||||||
"title": "Catálogos Fijos",
|
"title": "Catálogos Fijos",
|
||||||
"codes_pedimento_regimen": "Códigos de Pedimento y Régimen",
|
"codes_pedimento_regimen": "Códigos de Pedimento y Régimen",
|
||||||
@@ -1601,5 +1606,198 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"accept": "Aceptar"
|
"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.
|
* Returns blob and suggested filename for the browser download.
|
||||||
*/
|
*/
|
||||||
async getCsvTemplateDownload(
|
async getCsvTemplateDownload(
|
||||||
templateId: string
|
templateId: string,
|
||||||
|
locale?: string
|
||||||
): Promise<{ blob: Blob; filename: string }> {
|
): Promise<{ blob: Blob; filename: string }> {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
const response = await fetch(`${API_BASE_URL}/v1/a76/csv-templates/${templateId}`, {
|
const loc = locale === 'en' ? 'en' : 'es';
|
||||||
method: 'GET',
|
const qs = new URLSearchParams({ locale: loc });
|
||||||
headers,
|
const response = await fetch(
|
||||||
credentials: 'include'
|
`${API_BASE_URL}/v1/a76/csv-templates/${templateId}?${qs.toString()}`,
|
||||||
});
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store'
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const msg = response.status === 404 ? 'Plantilla no encontrada' : `Error ${response.status}`;
|
const msg = response.status === 404 ? 'Plantilla no encontrada' : `Error ${response.status}`;
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||||
import { Settings2 } from 'lucide-svelte';
|
import { Settings2 } from 'lucide-svelte';
|
||||||
import { tabSettings } from '$lib/config/csv-upload';
|
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 {
|
let {
|
||||||
activeTab,
|
activeTab,
|
||||||
@@ -38,7 +46,9 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex items-center gap-2 text-muted-foreground border-b pb-2">
|
<div class="flex items-center gap-2 text-muted-foreground border-b pb-2">
|
||||||
<Settings2 class="h-4 w-4" />
|
<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>
|
</div>
|
||||||
|
|
||||||
{#if currentFields.length > 0}
|
{#if currentFields.length > 0}
|
||||||
@@ -47,7 +57,7 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if field.type !== 'boolean'}
|
{#if field.type !== 'boolean'}
|
||||||
<Label class="text-xs font-medium text-muted-foreground uppercase"
|
<Label class="text-xs font-medium text-muted-foreground uppercase"
|
||||||
>{field.label}</Label
|
>{csvMsg(field.labelKey)}</Label
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -59,7 +69,7 @@
|
|||||||
<Label
|
<Label
|
||||||
for={field.name}
|
for={field.name}
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
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>
|
</div>
|
||||||
{:else if field.type === 'select' && field.options}
|
{:else if field.type === 'select' && field.options}
|
||||||
@@ -68,7 +78,7 @@
|
|||||||
bind:value={settings[field.name]}
|
bind:value={settings[field.name]}
|
||||||
>
|
>
|
||||||
{#each field.options as opt}
|
{#each field.options as opt}
|
||||||
<option value={opt.value}>{opt.label}</option>
|
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else if field.type === 'radio' && field.options}
|
{:else if field.type === 'radio' && field.options}
|
||||||
@@ -79,7 +89,7 @@
|
|||||||
{#each field.options as opt}
|
{#each field.options as opt}
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<RadioGroup.Item value={opt.value} id={`${field.name}-${opt.value}`} />
|
<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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
@@ -89,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center h-8 text-sm text-muted-foreground italic">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
import { Settings2 } from 'lucide-svelte';
|
import { Settings2 } from 'lucide-svelte';
|
||||||
import { globalCsvParams, tabSettings, type CsvUploadField } from '$lib/config/csv-upload';
|
import { globalCsvParams, tabSettings, type CsvUploadField } from '$lib/config/csv-upload';
|
||||||
import { cn } from '$lib/utils';
|
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 {
|
let {
|
||||||
globalSettings = $bindable(),
|
globalSettings = $bindable(),
|
||||||
@@ -45,13 +53,13 @@
|
|||||||
<div class="flex items-center gap-2 text-muted-foreground border-r border-border pr-4">
|
<div class="flex items-center gap-2 text-muted-foreground border-r border-border pr-4">
|
||||||
<Settings2 class="h-4 w-4 shrink-0" />
|
<Settings2 class="h-4 w-4 shrink-0" />
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider whitespace-nowrap"
|
<span class="text-xs font-semibold uppercase tracking-wider whitespace-nowrap"
|
||||||
>Parámetros globales</span
|
>{csvMsg('params_header')}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{#each globalCsvParams as param}
|
{#each globalCsvParams as param}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<Label for="global-{param.name}" class="text-xs font-medium text-muted-foreground"
|
<Label for="global-{param.name}" class="text-xs font-medium text-muted-foreground"
|
||||||
>{param.label}</Label
|
>{csvMsg(param.labelKey)}</Label
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
id="global-{param.name}"
|
id="global-{param.name}"
|
||||||
@@ -62,7 +70,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each param.options as opt}
|
{#each param.options as opt}
|
||||||
<option value={opt.value}>{opt.label}</option>
|
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,13 +81,13 @@
|
|||||||
{#if currentTabFields.length > 0}
|
{#if currentTabFields.length > 0}
|
||||||
<div class="flex flex-wrap items-center gap-4 border-l border-border pl-4">
|
<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"
|
<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}
|
{#each currentTabFields as field}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{#if field.type !== 'boolean'}
|
{#if field.type !== 'boolean'}
|
||||||
<Label for="tab-{field.name}" class="text-xs font-medium text-muted-foreground"
|
<Label for="tab-{field.name}" class="text-xs font-medium text-muted-foreground"
|
||||||
>{field.label}</Label
|
>{csvMsg(field.labelKey)}</Label
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if field.type === 'select' && field.options}
|
{#if field.type === 'select' && field.options}
|
||||||
@@ -92,7 +100,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each field.options as opt}
|
{#each field.options as opt}
|
||||||
<option value={opt.value}>{opt.label}</option>
|
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else if field.type === 'radio' && field.options}
|
{:else if field.type === 'radio' && field.options}
|
||||||
@@ -108,7 +116,7 @@
|
|||||||
if (tabSettingsValues) tabSettingsValues[field.name] = opt.value;
|
if (tabSettingsValues) tabSettingsValues[field.name] = opt.value;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{opt.label}
|
{csvMsg(opt.labelKey)}
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +129,7 @@
|
|||||||
if (tabSettingsValues) tabSettingsValues[field.name] = e.currentTarget.checked;
|
if (tabSettingsValues) tabSettingsValues[field.name] = e.currentTarget.checked;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{field.label}
|
{csvMsg(field.labelKey)}
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
} from '$lib/csv-import-pending';
|
} from '$lib/csv-import-pending';
|
||||||
import { fetchCsvImportStatus, isWaitingConfirmationPayload } from '$lib/csv-import-status-api';
|
import { fetchCsvImportStatus, isWaitingConfirmationPayload } from '$lib/csv-import-status-api';
|
||||||
import { Loader2, RefreshCw } from 'lucide-svelte';
|
import { Loader2, RefreshCw } from 'lucide-svelte';
|
||||||
|
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||||
|
|
||||||
type ValidatedRow = CsvImportPendingEntry & { checking?: boolean };
|
type ValidatedRow = CsvImportPendingEntry & { checking?: boolean };
|
||||||
|
|
||||||
@@ -36,22 +37,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function profileLabel(p: CsvImportPendingEntry['profile']): string {
|
function profileLabel(p: CsvImportPendingEntry['profile']): string {
|
||||||
const map: Record<CsvImportPendingEntry['profile'], string> = {
|
const key = `pending.profiles.${p}` as const;
|
||||||
customs_brokers: 'Agentes aduanales',
|
const t = csvMsg(key);
|
||||||
clients_providers: 'Clientes / proveedores',
|
return t === key ? p : t;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStaleJob(status: number, err: string | undefined): boolean {
|
function isStaleJob(status: number, err: string | undefined): boolean {
|
||||||
@@ -134,7 +122,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button variant="outline" size="sm" class="shrink-0" type="button" onclick={() => (open = true)}>
|
<Button variant="outline" size="sm" class="shrink-0" type="button" onclick={() => (open = true)}>
|
||||||
Pendientes
|
{csvMsg('pending.badge')}
|
||||||
{#if badgeCount > 0}
|
{#if badgeCount > 0}
|
||||||
<span class="ml-1.5 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-primary">
|
<span class="ml-1.5 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-primary">
|
||||||
{badgeCount}
|
{badgeCount}
|
||||||
@@ -145,10 +133,9 @@
|
|||||||
<Sheet.Root bind:open>
|
<Sheet.Root bind:open>
|
||||||
<Sheet.Content side="right" class="flex w-full max-w-lg flex-col sm:max-w-xl">
|
<Sheet.Content side="right" class="flex w-full max-w-lg flex-col sm:max-w-xl">
|
||||||
<Sheet.Header>
|
<Sheet.Header>
|
||||||
<Sheet.Title>Importaciones pendientes de confirmar</Sheet.Title>
|
<Sheet.Title>{csvMsg('pending.title')}</Sheet.Title>
|
||||||
<Sheet.Description>
|
<Sheet.Description>
|
||||||
Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al
|
{csvMsg('pending.description')}
|
||||||
actualizar.
|
|
||||||
</Sheet.Description>
|
</Sheet.Description>
|
||||||
</Sheet.Header>
|
</Sheet.Header>
|
||||||
<div class="flex items-center justify-end gap-2 border-b px-4 py-2">
|
<div class="flex items-center justify-end gap-2 border-b px-4 py-2">
|
||||||
@@ -158,14 +145,14 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<RefreshCw class="mr-2 h-4 w-4" />
|
<RefreshCw class="mr-2 h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
Actualizar
|
{csvMsg('pending.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||||
{#if rows.length === 0 && !refreshing}
|
{#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}
|
{: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}
|
{:else}
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
{#each rows as row (row.jobId)}
|
{#each rows as row (row.jobId)}
|
||||||
@@ -177,10 +164,10 @@
|
|||||||
{#if row.totalRows != null || row.validRows != null}
|
{#if row.totalRows != null || row.validRows != null}
|
||||||
<div class="mt-1 text-xs text-muted-foreground">
|
<div class="mt-1 text-xs text-muted-foreground">
|
||||||
{#if row.totalRows != null}
|
{#if row.totalRows != null}
|
||||||
Total filas: {row.totalRows}
|
{csvMsg('pending.total_rows')}: {row.totalRows}
|
||||||
{/if}
|
{/if}
|
||||||
{#if row.validRows != null}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -201,9 +188,9 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reanudar
|
{csvMsg('pending.resume')}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
criticalReferenceGaps,
|
criticalReferenceGaps,
|
||||||
referenceStateReady
|
referenceStateReady
|
||||||
} from '$lib/csv-import-commit-metrics';
|
} from '$lib/csv-import-commit-metrics';
|
||||||
|
import { csvFmt, csvMsg } from '$lib/i18n/csv-msg';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = $bindable(false),
|
open = $bindable(false),
|
||||||
@@ -249,16 +250,16 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Dialog.Title class="text-xl font-semibold tracking-tight text-foreground">
|
<Dialog.Title class="text-xl font-semibold tracking-tight text-foreground">
|
||||||
{#if isPending}
|
{#if isPending}
|
||||||
Validación de Importación
|
{csvMsg('modal.title_pending')}
|
||||||
{:else if isFinished}
|
{:else if isFinished}
|
||||||
{hasErrors ? 'Importación con Observaciones' : 'Importación Exitosa'}
|
{hasErrors ? csvMsg('modal.title_warning') : csvMsg('modal.title_success')}
|
||||||
{/if}
|
{/if}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Description class="mt-1 text-muted-foreground">
|
<Dialog.Description class="mt-1 text-muted-foreground">
|
||||||
{#if isPending}
|
{#if isPending}
|
||||||
Revise el análisis preliminar antes de confirmar la carga de datos.
|
{csvMsg('modal.desc_pending')}
|
||||||
{:else if isFinished}
|
{:else if isFinished}
|
||||||
El proceso de importación ha finalizado.
|
{csvMsg('modal.desc_done')}
|
||||||
{/if}
|
{/if}
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +275,7 @@
|
|||||||
class="bg-card p-4 rounded-lg border flex flex-col items-center justify-center text-center shadow-sm"
|
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"
|
<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>
|
<span class="text-2xl font-bold text-foreground">{scanResults.total_rows || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +286,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider mb-1"
|
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"
|
<span class="text-2xl font-bold text-green-700 dark:text-green-300"
|
||||||
>{scanResults.valid_rows || 0}</span
|
>{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"
|
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"
|
<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>
|
<span class="text-2xl font-bold text-destructive">{scanResults.error_count || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,10 +328,9 @@
|
|||||||
>
|
>
|
||||||
<XCircle class="w-5 h-5 text-destructive mt-0.5 shrink-0" />
|
<XCircle class="w-5 h-5 text-destructive mt-0.5 shrink-0" />
|
||||||
<div class="text-sm text-destructive-foreground/90">
|
<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>
|
<p>
|
||||||
Corrija los datos indicados abajo en su CSV y vuelva a subir, o confirme para
|
{csvMsg('modal.scan_problems_body')}
|
||||||
importar solo las filas válidas (las erróneas se omitirán).
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,13 +338,13 @@
|
|||||||
<div class="border rounded-lg overflow-hidden shadow-sm">
|
<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">
|
<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">
|
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
|
||||||
Detalle de errores (para corregir en el CSV)
|
{csvMsg('modal.errors_heading')}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
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>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
class="h-7 px-2 text-xs"
|
class="h-7 px-2 text-xs"
|
||||||
>
|
>
|
||||||
<FileText class="w-3.5 h-3.5 mr-1" />
|
<FileText class="w-3.5 h-3.5 mr-1" />
|
||||||
Descargar CSV
|
{csvMsg('modal.download_csv')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2 w-16">Línea</th>
|
<th class="px-4 py-2 w-16">{csvMsg('modal.th_line')}</th>
|
||||||
<th class="px-4 py-2 w-40">Columna</th>
|
<th class="px-4 py-2 w-40">{csvMsg('modal.th_column')}</th>
|
||||||
<th class="px-4 py-2">Mensaje</th>
|
<th class="px-4 py-2">{csvMsg('modal.th_message')}</th>
|
||||||
<th class="px-4 py-2 min-w-[240px]">Solución</th>
|
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y">
|
<tbody class="divide-y">
|
||||||
@@ -382,21 +382,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if scanErrorsTruncated}
|
{#if scanErrorsTruncated}
|
||||||
<p class="text-xs text-muted-foreground px-4 py-2">
|
<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>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm text-muted-foreground mt-1">
|
<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>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="rounded-md bg-primary/5 border border-primary/10 p-4 flex items-start gap-3">
|
<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" />
|
<CheckCircle2 class="w-5 h-5 text-primary mt-0.5 shrink-0" />
|
||||||
<div class="text-sm text-primary/90">
|
<div class="text-sm text-primary/90">
|
||||||
<p class="font-semibold mb-1">Archivo validado correctamente</p>
|
<p class="font-semibold mb-1">{csvMsg('modal.scan_ok_title')}</p>
|
||||||
<p>Todos los registros parecen correctos y listos para importar.</p>
|
<p>{csvMsg('modal.scan_ok_body')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||||
<span
|
<span
|
||||||
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
||||||
>Insertados</span
|
>{csvMsg('modal.inserted')}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-3xl font-bold text-green-700 dark:text-green-300">{insertedCount}</span>
|
<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" />
|
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||||
<span
|
<span
|
||||||
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
||||||
>Actualizados</span
|
>{csvMsg('modal.updated')}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-3xl font-bold text-green-700 dark:text-green-300">{updatedCount}</span>
|
<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">
|
<div class="mb-1 flex items-center gap-2">
|
||||||
<XCircle class="h-4 w-4 text-destructive" />
|
<XCircle class="h-4 w-4 text-destructive" />
|
||||||
<span class="text-xs font-bold uppercase tracking-wider text-destructive"
|
<span class="text-xs font-bold uppercase tracking-wider text-destructive"
|
||||||
>Rechazados</span
|
>{csvMsg('modal.rejected')}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-3xl font-bold text-destructive">{totalSkipped}</span>
|
<span class="text-3xl font-bold text-destructive">{totalSkipped}</span>
|
||||||
{#if totalSkipped > 0 && commitResults.skipped_details && commitResults.skipped_details.length > 0}
|
{#if totalSkipped > 0 && commitResults.skipped_details && commitResults.skipped_details.length > 0}
|
||||||
<p class="mt-2 text-xs text-muted-foreground">
|
<p class="mt-2 text-xs text-muted-foreground">
|
||||||
Revisa el detalle por línea en la tabla inferior.
|
{csvMsg('modal.rejected_hint')}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -456,20 +456,17 @@
|
|||||||
{#if refGaps > 0}
|
{#if refGaps > 0}
|
||||||
<AlertTriangle class="mt-0.5 h-5 w-5 shrink-0 text-amber-600 dark:text-amber-500" />
|
<AlertTriangle class="mt-0.5 h-5 w-5 shrink-0 text-amber-600 dark:text-amber-500" />
|
||||||
<div>
|
<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">
|
<p class="mt-1 text-muted-foreground">
|
||||||
Hay {refGaps} brecha(s) crítica(s) de referencia. Revisa catálogos y el detalle de
|
{csvFmt('modal.ref_gaps_body', { n: refGaps })}
|
||||||
filas rechazadas antes de reintentar.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<CheckCircle2 class="mt-0.5 h-5 w-5 shrink-0 text-green-600 dark:text-green-500" />
|
<CheckCircle2 class="mt-0.5 h-5 w-5 shrink-0 text-green-600 dark:text-green-500" />
|
||||||
<div>
|
<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">
|
<p class="mt-1 text-muted-foreground">
|
||||||
{refReady
|
{refReady ? csvMsg('modal.ref_state_ok') : csvMsg('modal.ref_state_other')}
|
||||||
? 'Referencias listas para operar (sin brechas críticas reportadas).'
|
|
||||||
: 'Sin brechas numéricas; revisa el mensaje del servidor si aplica.'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -483,7 +480,7 @@
|
|||||||
{#if commitSkippedSummary.length > 0}
|
{#if commitSkippedSummary.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<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>
|
</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each commitSkippedSummary as item}
|
{#each commitSkippedSummary as item}
|
||||||
@@ -504,13 +501,13 @@
|
|||||||
<div id="detalle-errores-import" class="border rounded-lg overflow-hidden mt-2 shadow-sm">
|
<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">
|
<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">
|
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
|
||||||
Detalle de Errores
|
{csvMsg('modal.commit_errors_heading')}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
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>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -518,7 +515,7 @@
|
|||||||
class="h-7 px-2 text-xs"
|
class="h-7 px-2 text-xs"
|
||||||
>
|
>
|
||||||
<FileText class="w-3.5 h-3.5 mr-1" />
|
<FileText class="w-3.5 h-3.5 mr-1" />
|
||||||
Descargar CSV
|
{csvMsg('modal.download_csv')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2 w-20">Línea</th>
|
<th class="px-4 py-2 w-20">{csvMsg('modal.th_line')}</th>
|
||||||
<th class="px-4 py-2 w-32">Referencia</th>
|
<th class="px-4 py-2 w-32">{csvMsg('modal.th_reference')}</th>
|
||||||
<th class="px-4 py-2">Motivo</th>
|
<th class="px-4 py-2">{csvMsg('modal.th_reason')}</th>
|
||||||
<th class="px-4 py-2 min-w-[240px]">Solución</th>
|
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y">
|
<tbody class="divide-y">
|
||||||
@@ -558,7 +555,7 @@
|
|||||||
<div class="flex flex-col gap-3 border-t bg-muted/20 px-6 py-4">
|
<div class="flex flex-col gap-3 border-t bg-muted/20 px-6 py-4">
|
||||||
{#if isPending && isUploading}
|
{#if isPending && isUploading}
|
||||||
<div class="space-y-2" role="status" aria-live="polite" aria-busy="true">
|
<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="relative h-2 w-full overflow-hidden rounded-full bg-secondary">
|
||||||
<div
|
<div
|
||||||
class="csv-commit-indeterminate-bar absolute top-0 h-full w-2/5 rounded-full bg-primary"
|
class="csv-commit-indeterminate-bar absolute top-0 h-full w-2/5 rounded-full bg-primary"
|
||||||
@@ -574,7 +571,7 @@
|
|||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
class="text-muted-foreground hover:bg-muted/50"
|
class="text-muted-foreground hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
Cancelar Operación
|
{csvMsg('modal.cancel_operation')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onclick={onConfirm}
|
onclick={onConfirm}
|
||||||
@@ -583,14 +580,14 @@
|
|||||||
>
|
>
|
||||||
{#if isUploading}
|
{#if isUploading}
|
||||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||||
Procesando...
|
{csvMsg('modal.processing')}
|
||||||
{:else}
|
{:else}
|
||||||
<UploadCloud class="mr-2 h-4 w-4" />
|
<UploadCloud class="mr-2 h-4 w-4" />
|
||||||
Confirmar Carga
|
{csvMsg('modal.confirm_load')}
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{:else if isFinished}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,21 @@
|
|||||||
import { UploadCloud, Lock } from 'lucide-svelte';
|
import { UploadCloud, Lock } from 'lucide-svelte';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { api } from '$lib/api';
|
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 {
|
let {
|
||||||
items,
|
items,
|
||||||
@@ -62,7 +76,7 @@
|
|||||||
const isValidExtension = file.name.toLowerCase().endsWith('.csv');
|
const isValidExtension = file.name.toLowerCase().endsWith('.csv');
|
||||||
|
|
||||||
if (!isValidExtension) {
|
if (!isValidExtension) {
|
||||||
toast.error('Formato inválido. Solo se permiten archivos .csv');
|
toast.error(csvMsg('toast.invalid_csv'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +120,12 @@
|
|||||||
if (!item.templateId) return;
|
if (!item.templateId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast.info(`Descargando plantilla para ${item.title}...`);
|
const itemLabel = csvMsg(`items.${item.id}`);
|
||||||
const { blob, filename } = await api.getCsvTemplateDownload(item.templateId);
|
toast.info(`${csvMsg('toast.download_loading')} ${itemLabel}`);
|
||||||
|
const { blob, filename } = await api.getCsvTemplateDownload(
|
||||||
|
item.templateId,
|
||||||
|
csvTemplateLocale()
|
||||||
|
);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -116,9 +134,9 @@
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
toast.success(`Plantilla descargada: ${filename}`);
|
toast.success(`${csvMsg('toast.download_ok')} ${filename}`);
|
||||||
} catch (err) {
|
} 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>
|
</script>
|
||||||
@@ -128,6 +146,10 @@
|
|||||||
class:opacity-60={gridLocked}
|
class:opacity-60={gridLocked}
|
||||||
aria-busy={gridLocked ? true : undefined}
|
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}
|
{#if groupedItems.ungrouped.length > 0}
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
{#each groupedItems.ungrouped as item}
|
{#each groupedItems.ungrouped as item}
|
||||||
@@ -171,7 +193,7 @@
|
|||||||
<span
|
<span
|
||||||
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -183,7 +205,7 @@
|
|||||||
<div class="animate-bounce">
|
<div class="animate-bounce">
|
||||||
<UploadCloud class="h-8 w-8 text-primary" />
|
<UploadCloud class="h-8 w-8 text-primary" />
|
||||||
</div>
|
</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}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
@@ -193,7 +215,7 @@
|
|||||||
>
|
>
|
||||||
<item.icon class="h-6 w-6 text-primary" />
|
<item.icon class="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div class="font-medium text-sm text-balance">{item.title}</div>
|
{@render cardTitle(item.id)}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
@@ -205,7 +227,7 @@
|
|||||||
{#each Object.entries(groupedItems.groups) as [groupName, groupItems]}
|
{#each Object.entries(groupedItems.groups) as [groupName, groupItems]}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">
|
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">
|
||||||
{groupName}
|
{csvMsg(`groups.${groupName}`)}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
{#each groupItems as item}
|
{#each groupItems as item}
|
||||||
@@ -251,7 +273,7 @@
|
|||||||
<span
|
<span
|
||||||
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -263,7 +285,7 @@
|
|||||||
<div class="animate-bounce">
|
<div class="animate-bounce">
|
||||||
<UploadCloud class="h-8 w-8 text-primary" />
|
<UploadCloud class="h-8 w-8 text-primary" />
|
||||||
</div>
|
</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}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
@@ -273,7 +295,7 @@
|
|||||||
>
|
>
|
||||||
<item.icon class="h-6 w-6 text-primary" />
|
<item.icon class="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div class="font-medium text-sm text-balance">{item.title}</div>
|
{@render cardTitle(item.id)}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -91,6 +91,18 @@ export function getSidebarData(): SidebarData {
|
|||||||
items: [],
|
items: [],
|
||||||
permission: 'audit_logs.view',
|
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"](),
|
title: m["sidebar.reference_data.title"](),
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
<script lang="ts">
|
<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 * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
import { m } from '$lib/i18n/messages';
|
||||||
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';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
projects
|
projects
|
||||||
@@ -15,28 +8,14 @@
|
|||||||
projects: {
|
projects: {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
// This should be `Component` after @lucide/svelte updates types
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
icon: any;
|
icon: any;
|
||||||
}[];
|
}[];
|
||||||
} = $props();
|
} = $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>
|
</script>
|
||||||
|
|
||||||
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||||
<Sidebar.GroupLabel>Gestion</Sidebar.GroupLabel>
|
<Sidebar.GroupLabel>{m['sidebar.management_label']()}</Sidebar.GroupLabel>
|
||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
{#each projects as item (item.name)}
|
{#each projects as item (item.name)}
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
@@ -48,58 +27,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</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>
|
</Sidebar.MenuItem>
|
||||||
{/each}
|
{/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.Menu>
|
||||||
</Sidebar.Group>
|
</Sidebar.Group>
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ import {
|
|||||||
|
|
||||||
export interface CsvUploadItem {
|
export interface CsvUploadItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
/** Etiqueta vía i18n: csv_upload.items.<id> */
|
||||||
icon: any;
|
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
|
modelTarget?: string; // The backend model this maps to
|
||||||
description?: string;
|
description?: string;
|
||||||
/** Backend template id for CSV download (e.g. customs_brokers, part_numbers). No physical file. */
|
/** 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 {
|
export interface CsvUploadField {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
/** Ruta bajo csv_upload.* (p. ej. params.load_mode) */
|
||||||
|
labelKey: string;
|
||||||
type: 'text' | 'select' | 'boolean' | 'date' | 'radio';
|
type: 'text' | 'select' | 'boolean' | 'date' | 'radio';
|
||||||
options?: { label: string; value: string | boolean | number }[];
|
options?: { labelKey: string; value: string | boolean | number }[];
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
defaultValue?: any;
|
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). */
|
/** Global parameters shown in the CSV upload footer bar (one option or the other via select). */
|
||||||
export interface GlobalCsvParam {
|
export interface GlobalCsvParam {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
labelKey: string;
|
||||||
type: 'select';
|
type: 'select';
|
||||||
options: { label: string; value: string }[];
|
options: { labelKey: string; value: string }[];
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,62 +58,62 @@ export interface GlobalCsvParam {
|
|||||||
export const globalCsvParams: GlobalCsvParam[] = [
|
export const globalCsvParams: GlobalCsvParam[] = [
|
||||||
{
|
{
|
||||||
name: 'mode',
|
name: 'mode',
|
||||||
label: 'Modo de Carga',
|
labelKey: 'params.load_mode',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Actualizar', value: 'update' },
|
{ labelKey: 'options.update', value: 'update' },
|
||||||
{ label: 'Reemplazar', value: 'replace' }
|
{ labelKey: 'options.replace', value: 'replace' }
|
||||||
],
|
],
|
||||||
defaultValue: 'update'
|
defaultValue: 'update'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dateFormat',
|
name: 'dateFormat',
|
||||||
label: 'Formato de Fecha',
|
labelKey: 'params.date_format',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||||
],
|
],
|
||||||
defaultValue: 'dd/mm/yyyy'
|
defaultValue: 'dd/mm/yyyy'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'weight_unit',
|
name: 'weight_unit',
|
||||||
label: 'Unidad de Peso',
|
labelKey: 'params.weight_unit',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Kilos (Kgs)', value: 'kgs' },
|
{ labelKey: 'options.kgs', value: 'kgs' },
|
||||||
{ label: 'Libras (Lbs)', value: 'lbs' }
|
{ labelKey: 'options.lbs', value: 'lbs' }
|
||||||
],
|
],
|
||||||
defaultValue: 'kgs'
|
defaultValue: 'kgs'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'autonumber_series',
|
name: 'autonumber_series',
|
||||||
label: 'Autonumerar Partidas/Series',
|
labelKey: 'params.autonumber_series',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Sí', value: 'true' },
|
{ labelKey: 'options.yes', value: 'true' },
|
||||||
{ label: 'No', value: 'false' }
|
{ labelKey: 'options.no', value: 'false' }
|
||||||
],
|
],
|
||||||
defaultValue: 'false'
|
defaultValue: 'false'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'load_subpartidas',
|
name: 'load_subpartidas',
|
||||||
label: 'Levantar Subpartidas',
|
labelKey: 'params.load_subpartidas',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Sí', value: 'true' },
|
{ labelKey: 'options.yes', value: 'true' },
|
||||||
{ label: 'No', value: 'false' }
|
{ labelKey: 'options.no', value: 'false' }
|
||||||
],
|
],
|
||||||
defaultValue: 'false'
|
defaultValue: 'false'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'recalculate_pedimento_date',
|
name: 'recalculate_pedimento_date',
|
||||||
label: 'Recalcular Fecha Pedimento',
|
labelKey: 'params.recalculate_pedimento_date',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Sí', value: 'true' },
|
{ labelKey: 'options.yes', value: 'true' },
|
||||||
{ label: 'No', value: 'false' }
|
{ labelKey: 'options.no', value: 'false' }
|
||||||
],
|
],
|
||||||
defaultValue: 'false'
|
defaultValue: 'false'
|
||||||
}
|
}
|
||||||
@@ -122,11 +124,11 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
|||||||
catalogos: [
|
catalogos: [
|
||||||
{
|
{
|
||||||
name: 'mode',
|
name: 'mode',
|
||||||
label: 'Modo de Carga',
|
labelKey: 'params.load_mode',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Actualizar', value: 'update' },
|
{ labelKey: 'options.update', value: 'update' },
|
||||||
{ label: 'Reemplazar', value: 'replace' }
|
{ labelKey: 'options.replace', value: 'replace' }
|
||||||
],
|
],
|
||||||
defaultValue: 'update'
|
defaultValue: 'update'
|
||||||
}
|
}
|
||||||
@@ -134,11 +136,11 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
|||||||
transportes: [
|
transportes: [
|
||||||
{
|
{
|
||||||
name: 'mode',
|
name: 'mode',
|
||||||
label: 'Modo de Carga',
|
labelKey: 'params.load_mode',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Actualizar', value: 'update' },
|
{ labelKey: 'options.update', value: 'update' },
|
||||||
{ label: 'Reemplazar', value: 'replace' }
|
{ labelKey: 'options.replace', value: 'replace' }
|
||||||
],
|
],
|
||||||
defaultValue: 'update'
|
defaultValue: 'update'
|
||||||
}
|
}
|
||||||
@@ -146,24 +148,24 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
|||||||
importacion: [
|
importacion: [
|
||||||
{
|
{
|
||||||
name: 'autonumber_remesas',
|
name: 'autonumber_remesas',
|
||||||
label: 'Autonumerar Remesas',
|
labelKey: 'params.autonumber_remesas',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'recalculate_dates',
|
name: 'recalculate_dates',
|
||||||
label: 'Recalcular Fechas',
|
labelKey: 'params.recalculate_dates',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dateFormat',
|
name: 'dateFormat',
|
||||||
label: 'Formato de Fecha',
|
labelKey: 'params.date_format',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||||
],
|
],
|
||||||
defaultValue: 'dd/mm/yyyy'
|
defaultValue: 'dd/mm/yyyy'
|
||||||
}
|
}
|
||||||
@@ -171,28 +173,28 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
|||||||
exportacion: [
|
exportacion: [
|
||||||
{
|
{
|
||||||
name: 'invoice_type',
|
name: 'invoice_type',
|
||||||
label: 'Tipo de Factura',
|
labelKey: 'params.invoice_type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'AFIJO', value: 'AFIJO' },
|
{ labelKey: 'options.afi', value: 'AFIJO' },
|
||||||
{ label: 'NORMAL', value: 'NORMAL' },
|
{ labelKey: 'options.normal', value: 'NORMAL' },
|
||||||
],
|
],
|
||||||
defaultValue: 'AFIJO',
|
defaultValue: 'AFIJO',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'is_regime_change',
|
name: 'is_regime_change',
|
||||||
label: 'Es Cambio de Régimen',
|
labelKey: 'params.is_regime_change',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dateFormat',
|
name: 'dateFormat',
|
||||||
label: 'Formato de Fecha',
|
labelKey: 'params.date_format',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||||
],
|
],
|
||||||
defaultValue: 'dd/mm/yyyy'
|
defaultValue: 'dd/mm/yyyy'
|
||||||
}
|
}
|
||||||
@@ -204,7 +206,6 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
|||||||
export const catalogosConfig: CsvUploadItem[] = [
|
export const catalogosConfig: CsvUploadItem[] = [
|
||||||
{
|
{
|
||||||
id: 'customs_brokers',
|
id: 'customs_brokers',
|
||||||
title: 'Agentes Aduanales',
|
|
||||||
icon: User,
|
icon: User,
|
||||||
modelTarget: 'CustomsBroker',
|
modelTarget: 'CustomsBroker',
|
||||||
templateId: 'customs_brokers',
|
templateId: 'customs_brokers',
|
||||||
@@ -212,7 +213,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'clients_providers',
|
id: 'clients_providers',
|
||||||
title: 'Clientes y Proveedores',
|
|
||||||
icon: Users,
|
icon: Users,
|
||||||
modelTarget: 'ClientProvider',
|
modelTarget: 'ClientProvider',
|
||||||
templateId: 'clients_providers',
|
templateId: 'clients_providers',
|
||||||
@@ -220,7 +220,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'exchange_rates',
|
id: 'exchange_rates',
|
||||||
title: 'Tipo de Cambios',
|
|
||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
modelTarget: 'ExchangeRate',
|
modelTarget: 'ExchangeRate',
|
||||||
templateId: 'exchange_rates',
|
templateId: 'exchange_rates',
|
||||||
@@ -228,7 +227,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'material_classes',
|
id: 'material_classes',
|
||||||
title: 'Clases de Materiales',
|
|
||||||
icon: Package,
|
icon: Package,
|
||||||
modelTarget: 'MaterialClass',
|
modelTarget: 'MaterialClass',
|
||||||
templateId: 'material_classes',
|
templateId: 'material_classes',
|
||||||
@@ -236,7 +234,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'part_numbers',
|
id: 'part_numbers',
|
||||||
title: 'Números de parte',
|
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
modelTarget: 'Part',
|
modelTarget: 'Part',
|
||||||
templateId: 'part_numbers',
|
templateId: 'part_numbers',
|
||||||
@@ -244,7 +241,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'boms',
|
id: 'boms',
|
||||||
title: 'BOMs',
|
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
modelTarget: 'Bom',
|
modelTarget: 'Bom',
|
||||||
templateId: 'boms',
|
templateId: 'boms',
|
||||||
@@ -253,30 +249,26 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'items',
|
id: 'items',
|
||||||
title: 'Partidas (Permisos)',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Permisos',
|
group: 'permisos',
|
||||||
modelTarget: 'ItemPermission',
|
modelTarget: 'ItemPermission',
|
||||||
templateId: 'part_numbers'
|
templateId: 'part_numbers'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'headers',
|
id: 'headers',
|
||||||
title: 'Encabezados (Permisos)',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Permisos',
|
group: 'permisos',
|
||||||
modelTarget: 'HeaderPermission',
|
modelTarget: 'HeaderPermission',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'historical_fractions',
|
id: 'historical_fractions',
|
||||||
title: 'Fracciones Históricas',
|
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
modelTarget: 'HistoricalFraction',
|
modelTarget: 'HistoricalFraction',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pedimentos',
|
id: 'pedimentos',
|
||||||
title: 'Pedimentos',
|
|
||||||
icon: FileDigit,
|
icon: FileDigit,
|
||||||
modelTarget: 'Pedimento',
|
modelTarget: 'Pedimento',
|
||||||
templateId: 'pedimentos',
|
templateId: 'pedimentos',
|
||||||
@@ -288,7 +280,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
|||||||
export const transportesConfig: CsvUploadItem[] = [
|
export const transportesConfig: CsvUploadItem[] = [
|
||||||
{
|
{
|
||||||
id: 'transporters',
|
id: 'transporters',
|
||||||
title: 'Transportistas',
|
|
||||||
icon: Ship,
|
icon: Ship,
|
||||||
modelTarget: 'Transporter',
|
modelTarget: 'Transporter',
|
||||||
templateId: 'transporters',
|
templateId: 'transporters',
|
||||||
@@ -296,7 +287,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'transports',
|
id: 'transports',
|
||||||
title: 'Transportes',
|
|
||||||
icon: Truck,
|
icon: Truck,
|
||||||
modelTarget: 'Transport',
|
modelTarget: 'Transport',
|
||||||
templateId: 'transports',
|
templateId: 'transports',
|
||||||
@@ -304,7 +294,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'drivers',
|
id: 'drivers',
|
||||||
title: 'Conductores',
|
|
||||||
icon: User,
|
icon: User,
|
||||||
modelTarget: 'Driver',
|
modelTarget: 'Driver',
|
||||||
templateId: 'drivers',
|
templateId: 'drivers',
|
||||||
@@ -312,7 +301,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'trailers',
|
id: 'trailers',
|
||||||
title: 'Trailers y Cajas',
|
|
||||||
icon: Container,
|
icon: Container,
|
||||||
modelTarget: 'Trailer',
|
modelTarget: 'Trailer',
|
||||||
templateId: 'trailers',
|
templateId: 'trailers',
|
||||||
@@ -326,27 +314,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
|||||||
// Impo Temp
|
// Impo Temp
|
||||||
{
|
{
|
||||||
id: 'imp_temp_header',
|
id: 'imp_temp_header',
|
||||||
title: 'Encabezado',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Impo. Temp.',
|
group: 'impo_temp',
|
||||||
modelTarget: 'invoice_header',
|
modelTarget: 'invoice_header',
|
||||||
templateId: 'imp_temp_header',
|
templateId: 'imp_temp_header',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'imp_temp_details',
|
id: 'imp_temp_details',
|
||||||
title: 'Partidas',
|
|
||||||
icon: Package,
|
icon: Package,
|
||||||
group: 'Impo. Temp.',
|
group: 'impo_temp',
|
||||||
modelTarget: 'invoice_details',
|
modelTarget: 'invoice_details',
|
||||||
templateId: 'imp_temp_details',
|
templateId: 'imp_temp_details',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'imp_temp_series',
|
id: 'imp_temp_series',
|
||||||
title: 'Series',
|
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
group: 'Impo. Temp.',
|
group: 'impo_temp',
|
||||||
modelTarget: 'invoice_series',
|
modelTarget: 'invoice_series',
|
||||||
templateId: 'imp_temp_series',
|
templateId: 'imp_temp_series',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
@@ -354,27 +339,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
|||||||
// Impo Def
|
// Impo Def
|
||||||
{
|
{
|
||||||
id: 'imp_def_header',
|
id: 'imp_def_header',
|
||||||
title: 'Encabezado',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Impo. Def.',
|
group: 'impo_def',
|
||||||
modelTarget: 'invoice_header',
|
modelTarget: 'invoice_header',
|
||||||
templateId: 'imp_def_header',
|
templateId: 'imp_def_header',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'imp_def_details',
|
id: 'imp_def_details',
|
||||||
title: 'Partidas',
|
|
||||||
icon: Package,
|
icon: Package,
|
||||||
group: 'Impo. Def.',
|
group: 'impo_def',
|
||||||
modelTarget: 'invoice_details',
|
modelTarget: 'invoice_details',
|
||||||
templateId: 'imp_def_details',
|
templateId: 'imp_def_details',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'imp_def_series',
|
id: 'imp_def_series',
|
||||||
title: 'Series',
|
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
group: 'Impo. Def.',
|
group: 'impo_def',
|
||||||
modelTarget: 'invoice_series',
|
modelTarget: 'invoice_series',
|
||||||
templateId: 'imp_def_series',
|
templateId: 'imp_def_series',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
@@ -382,27 +364,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
|||||||
// Compras Mex
|
// Compras Mex
|
||||||
{
|
{
|
||||||
id: 'comp_mex_header',
|
id: 'comp_mex_header',
|
||||||
title: 'Encabezado',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Compras Mex.',
|
group: 'cmex',
|
||||||
modelTarget: 'invoice_header',
|
modelTarget: 'invoice_header',
|
||||||
templateId: 'cmex_header',
|
templateId: 'cmex_header',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'comp_mex_details',
|
id: 'comp_mex_details',
|
||||||
title: 'Partidas',
|
|
||||||
icon: Package,
|
icon: Package,
|
||||||
group: 'Compras Mex.',
|
group: 'cmex',
|
||||||
modelTarget: 'invoice_details',
|
modelTarget: 'invoice_details',
|
||||||
templateId: 'cmex_details',
|
templateId: 'cmex_details',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'comp_mex_series',
|
id: 'comp_mex_series',
|
||||||
title: 'Series',
|
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
group: 'Compras Mex.',
|
group: 'cmex',
|
||||||
modelTarget: 'invoice_series',
|
modelTarget: 'invoice_series',
|
||||||
templateId: 'cmex_series',
|
templateId: 'cmex_series',
|
||||||
layoutModule: 'layouts_csv/facturas'
|
layoutModule: 'layouts_csv/facturas'
|
||||||
@@ -415,70 +394,62 @@ export const exportacionConfig: CsvUploadItem[] = [
|
|||||||
// Expo Def / Cam. Reg.
|
// Expo Def / Cam. Reg.
|
||||||
{
|
{
|
||||||
id: 'exp_def_header',
|
id: 'exp_def_header',
|
||||||
title: 'Encabezado',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Expo. Def./Cam. Reg.',
|
group: 'expo_def',
|
||||||
modelTarget: 'invoice_header',
|
modelTarget: 'invoice_header',
|
||||||
templateId: 'exp_def_header',
|
templateId: 'exp_def_header',
|
||||||
layoutModule: 'layouts_csv/exportacion'
|
layoutModule: 'layouts_csv/exportacion'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'exp_def_details',
|
id: 'exp_def_details',
|
||||||
title: 'Partidas',
|
|
||||||
icon: Package,
|
icon: Package,
|
||||||
group: 'Expo. Def./Cam. Reg.',
|
group: 'expo_def',
|
||||||
modelTarget: 'invoice_details',
|
modelTarget: 'invoice_details',
|
||||||
templateId: 'exp_def_details',
|
templateId: 'exp_def_details',
|
||||||
layoutModule: 'layouts_csv/exportacion'
|
layoutModule: 'layouts_csv/exportacion'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'exp_def_series',
|
id: 'exp_def_series',
|
||||||
title: 'Series',
|
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
group: 'Expo. Def./Cam. Reg.',
|
group: 'expo_def',
|
||||||
modelTarget: 'invoice_series',
|
modelTarget: 'invoice_series',
|
||||||
templateId: 'exp_def_series',
|
templateId: 'exp_def_series',
|
||||||
layoutModule: 'layouts_csv/exportacion'
|
layoutModule: 'layouts_csv/exportacion'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'exp_def_nodes',
|
id: 'exp_def_nodes',
|
||||||
title: 'NODES',
|
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
group: 'Expo. Def./Cam. Reg.',
|
group: 'expo_def',
|
||||||
modelTarget: 'Nodes',
|
modelTarget: 'Nodes',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
// Expo Rep
|
// Expo Rep
|
||||||
{
|
{
|
||||||
id: 'exp_rep_header',
|
id: 'exp_rep_header',
|
||||||
title: 'Encabezado',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Expo. Rep.',
|
group: 'expo_rep',
|
||||||
modelTarget: 'InvoiceHeader',
|
modelTarget: 'InvoiceHeader',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'exp_rep_details',
|
id: 'exp_rep_details',
|
||||||
title: 'Partidas',
|
|
||||||
icon: Package,
|
icon: Package,
|
||||||
group: 'Expo. Rep.',
|
group: 'expo_rep',
|
||||||
modelTarget: 'InvoiceSalesDetails',
|
modelTarget: 'InvoiceSalesDetails',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'exp_rep_series',
|
id: 'exp_rep_series',
|
||||||
title: 'Series',
|
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
group: 'Expo. Rep.',
|
group: 'expo_rep',
|
||||||
modelTarget: 'InvoiceSeries',
|
modelTarget: 'InvoiceSeries',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
// Manifiesto
|
// Manifiesto
|
||||||
{
|
{
|
||||||
id: 'manifest_header',
|
id: 'manifest_header',
|
||||||
title: 'Encabezado',
|
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
group: 'Manifiesto',
|
group: 'manifest',
|
||||||
modelTarget: 'Manifest',
|
modelTarget: 'Manifest',
|
||||||
disabled: true,
|
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 { totalSkippedFromCommit } from '$lib/csv-import-commit-metrics';
|
||||||
import { countCsvDataRows } from '$lib/csv-upload-row-count';
|
import { countCsvDataRows } from '$lib/csv-upload-row-count';
|
||||||
import CsvPendingImportsSheet from '$lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte';
|
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. */
|
/** Intenta extraer un objeto tipo scan desde string tipo repr de Python. */
|
||||||
function parsePythonReprScan(s: string): Record<string, unknown> | null {
|
function parsePythonReprScan(s: string): Record<string, unknown> | null {
|
||||||
@@ -240,11 +241,11 @@
|
|||||||
const csvProgressStepTitle = $derived.by(() => {
|
const csvProgressStepTitle = $derived.by(() => {
|
||||||
switch (csvProgressPhase) {
|
switch (csvProgressPhase) {
|
||||||
case 'upload':
|
case 'upload':
|
||||||
return 'Subiendo archivo CSV';
|
return csvMsg('progress.upload');
|
||||||
case 'scan':
|
case 'scan':
|
||||||
return 'Validando registros en el servidor';
|
return csvMsg('progress.scan');
|
||||||
case 'commit':
|
case 'commit':
|
||||||
return 'Grabando registros en base de datos';
|
return csvMsg('progress.commit');
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -261,17 +262,26 @@
|
|||||||
switch (csvProgressPhase) {
|
switch (csvProgressPhase) {
|
||||||
case 'upload':
|
case 'upload':
|
||||||
if (csvImportRowTotal > 0) {
|
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
|
return uploadLengthComputable
|
||||||
? 'Subiendo archivo…'
|
? csvMsg('progress.upload_known')
|
||||||
: 'Subiendo archivo (tamaño desconocido en el navegador)…';
|
: csvMsg('progress.upload_unknown');
|
||||||
case 'scan':
|
case 'scan':
|
||||||
return `Registros procesados: ${Math.min(scanProgressCurrent, denom)} / ${denom}`;
|
return csvFmt('progress.rows_scan', {
|
||||||
|
current: Math.min(scanProgressCurrent, denom),
|
||||||
|
total: denom
|
||||||
|
});
|
||||||
case 'commit':
|
case 'commit':
|
||||||
return scanProgressTotal > 0
|
return scanProgressTotal > 0
|
||||||
? `Registros grabados: ${Math.min(scanProgressCurrent, denom)} / ${denom}`
|
? csvFmt('progress.rows_commit', {
|
||||||
: `Grabando en base de datos… (${Math.min(scanProgressCurrent, denom)} / ${denom} según último total conocido)`;
|
current: Math.min(scanProgressCurrent, denom),
|
||||||
|
total: denom
|
||||||
|
})
|
||||||
|
: csvFmt('progress.rows_commit_fallback', {
|
||||||
|
current: Math.min(scanProgressCurrent, denom),
|
||||||
|
total: denom
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -306,7 +316,7 @@
|
|||||||
|
|
||||||
const csvFooterAriaValueText = $derived(
|
const csvFooterAriaValueText = $derived(
|
||||||
csvFooterProgressIndeterminate
|
csvFooterProgressIndeterminate
|
||||||
? `${csvProgressStepTitle}. ${csvProgressDetailLine || 'En proceso…'}`
|
? `${csvProgressStepTitle}. ${csvProgressDetailLine || csvMsg('progress.in_progress')}`
|
||||||
: csvProgressDetailLine
|
: csvProgressDetailLine
|
||||||
? `${csvProgressStepTitle}, ${csvFooterPercentLabel}. ${csvProgressDetailLine}`
|
? `${csvProgressStepTitle}, ${csvFooterPercentLabel}. ${csvProgressDetailLine}`
|
||||||
: `${csvProgressStepTitle}, ${csvFooterPercentLabel}`
|
: `${csvProgressStepTitle}, ${csvFooterPercentLabel}`
|
||||||
@@ -591,7 +601,7 @@
|
|||||||
csvResumeOverlayHint = false;
|
csvResumeOverlayHint = false;
|
||||||
skipScanCompleteToastOnce = false;
|
skipScanCompleteToastOnce = false;
|
||||||
scanPhaseJobId = null;
|
scanPhaseJobId = null;
|
||||||
currentImportLabel = (config.title && String(config.title).trim()) || null;
|
currentImportLabel = csvMsg(`items.${config.id}`);
|
||||||
|
|
||||||
const onCsvFileUploadProgress = (e: { loaded: number; total: number }) => {
|
const onCsvFileUploadProgress = (e: { loaded: number; total: number }) => {
|
||||||
if (e.total > 0) {
|
if (e.total > 0) {
|
||||||
@@ -626,12 +636,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -646,12 +656,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -673,12 +683,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -698,12 +708,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -721,12 +731,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -744,12 +754,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -764,12 +774,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -787,12 +797,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -810,12 +820,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -834,12 +844,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -854,12 +864,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -888,12 +898,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -915,12 +925,12 @@
|
|||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
beginCsvScanAfterUpload();
|
beginCsvScanAfterUpload();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Error al subir el archivo');
|
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Upload exception', e);
|
console.error('Upload exception', e);
|
||||||
toast.error('Error inesperado al subir el archivo');
|
toast.error(csvMsg('toast.upload_err_generic'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,11 +953,9 @@
|
|||||||
const errMsg = res.error || '';
|
const errMsg = res.error || '';
|
||||||
if (isStaleImportJobError(res.status, errMsg)) {
|
if (isStaleImportJobError(res.status, errMsg)) {
|
||||||
removePendingLinkedToCurrentFlow();
|
removePendingLinkedToCurrentFlow();
|
||||||
toast.info(
|
toast.info(csvMsg('toast.stale_job'));
|
||||||
'Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.'
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(errMsg || 'Error al consultar el estado');
|
toast.error(errMsg || csvMsg('toast.poll_err'));
|
||||||
}
|
}
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
clearCsvImportSession();
|
clearCsvImportSession();
|
||||||
@@ -978,7 +986,7 @@
|
|||||||
if (skipScanCompleteToastOnce) {
|
if (skipScanCompleteToastOnce) {
|
||||||
skipScanCompleteToastOnce = false;
|
skipScanCompleteToastOnce = false;
|
||||||
} else {
|
} else {
|
||||||
toast.success('Escaneo completado. Revisa los resultados.');
|
toast.success(csvMsg('toast.scan_done'));
|
||||||
}
|
}
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
} else if (res.data?.status === 'failed' || res.data?.status === 'FAILURE') {
|
} else if (res.data?.status === 'failed' || res.data?.status === 'FAILURE') {
|
||||||
@@ -996,7 +1004,7 @@
|
|||||||
commitResults = parsedCommit;
|
commitResults = parsedCommit;
|
||||||
showResultModal = true;
|
showResultModal = true;
|
||||||
finalizeCommitAndClearPending();
|
finalizeCommitAndClearPending();
|
||||||
toast.success('Importación completada. Revisa el listado de registros.');
|
toast.success(csvMsg('toast.import_done'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
clearCsvImportSession();
|
clearCsvImportSession();
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
@@ -1026,7 +1034,7 @@
|
|||||||
if (skipScanCompleteToastOnce) {
|
if (skipScanCompleteToastOnce) {
|
||||||
skipScanCompleteToastOnce = false;
|
skipScanCompleteToastOnce = false;
|
||||||
} else {
|
} else {
|
||||||
toast.success('Escaneo completado. Revisa los resultados.');
|
toast.success(csvMsg('toast.scan_done'));
|
||||||
}
|
}
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -1036,30 +1044,32 @@
|
|||||||
(errRaw.includes('waiting_confirmation') || (errRaw.includes('total_rows') && errRaw.includes('job_id')));
|
(errRaw.includes('waiting_confirmation') || (errRaw.includes('total_rows') && errRaw.includes('job_id')));
|
||||||
if (looksLikeScanInError) {
|
if (looksLikeScanInError) {
|
||||||
removePendingLinkedToCurrentFlow();
|
removePendingLinkedToCurrentFlow();
|
||||||
toast.info('El escaneo terminó. Si no ves el modal, revisa el listado de registros.');
|
toast.info(csvMsg('toast.scan_alt'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
clearCsvImportSession();
|
clearCsvImportSession();
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let errText: string;
|
let errText: string;
|
||||||
|
const maybeDoneMsg = csvMsg('toast.import_maybe_done');
|
||||||
if (typeof errRaw === 'string') {
|
if (typeof errRaw === 'string') {
|
||||||
errText =
|
errText =
|
||||||
errRaw.includes('finished') && errRaw.includes('inserted')
|
errRaw.includes('finished') && errRaw.includes('inserted')
|
||||||
? 'La importación pudo completarse. Revisa el listado de registros.'
|
? maybeDoneMsg
|
||||||
: errRaw.includes("'status'") && errRaw.includes('waiting_confirmation')
|
: errRaw.includes("'status'") && errRaw.includes('waiting_confirmation')
|
||||||
? 'Error al obtener el resultado. Revisa el modal de resultados.'
|
? csvMsg('toast.err_fetch_scan_result')
|
||||||
: errRaw;
|
: errRaw;
|
||||||
} else if (typeof errRaw === 'object' && errRaw !== null) {
|
} 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 {
|
} else {
|
||||||
errText = 'Error desconocido';
|
errText = csvMsg('toast.err_unknown');
|
||||||
}
|
}
|
||||||
// No mostrar como error si el mensaje indica éxito
|
// 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);
|
toast.success(errText);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error en el procesamiento: ' + errText);
|
toast.error(csvFmt('toast.error_processing', { msg: errText }));
|
||||||
}
|
}
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
removePendingLinkedToCurrentFlow();
|
removePendingLinkedToCurrentFlow();
|
||||||
@@ -1082,12 +1092,16 @@
|
|||||||
if (totalOk === 0) {
|
if (totalOk === 0) {
|
||||||
toast.error(
|
toast.error(
|
||||||
backendMessage ||
|
backendMessage ||
|
||||||
`No se insertaron ni actualizaron registros. ${totalSkipped} fueron rechazados.`
|
csvFmt('toast.commit_warning_none', { skipped: totalSkipped })
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.warning(
|
toast.warning(
|
||||||
backendMessage ||
|
backendMessage ||
|
||||||
`Se aplicaron ${totalOk} registros (${inserted} insertados, ${updated} actualizados). ${totalSkipped} rechazados.`
|
csvFmt('toast.commit_warning_ok', {
|
||||||
|
inserted,
|
||||||
|
updated,
|
||||||
|
skipped: totalSkipped
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
finalizeCommitAndClearPending();
|
finalizeCommitAndClearPending();
|
||||||
@@ -1100,15 +1114,15 @@
|
|||||||
const totalSkipped = totalSkippedFromCommit(res.data as Record<string, unknown>);
|
const totalSkipped = totalSkippedFromCommit(res.data as Record<string, unknown>);
|
||||||
|
|
||||||
if (inserted > 0 || updated > 0) {
|
if (inserted > 0 || updated > 0) {
|
||||||
const parts = [];
|
const parts: string[] = [];
|
||||||
if (inserted > 0) parts.push(`${inserted} insertados`);
|
if (inserted > 0) parts.push(csvFmt('toast.n_inserted', { n: inserted }));
|
||||||
if (updated > 0) parts.push(`${updated} actualizados`);
|
if (updated > 0) parts.push(csvFmt('toast.n_updated', { n: updated }));
|
||||||
toast.success(`Importación completada: ${parts.join(', ')}`);
|
toast.success(csvFmt('toast.success_counts', { msg: parts.join(', ') }));
|
||||||
if (totalSkipped > 0) {
|
if (totalSkipped > 0) {
|
||||||
toast.warning(`${totalSkipped} registros fueron rechazados u omitidos`);
|
toast.warning(csvFmt('toast.warn_skipped', { n: totalSkipped }));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
toast.error(csvMsg('toast.finished_none'));
|
||||||
}
|
}
|
||||||
finalizeCommitAndClearPending();
|
finalizeCommitAndClearPending();
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
@@ -1136,26 +1150,26 @@
|
|||||||
<!-- Single scroll: content scrolls here; padding at bottom reserves space for fixed params bar -->
|
<!-- 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-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">
|
<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} />
|
<CsvPendingImportsSheet companyId={companyStore.activeCompany?.id} onResume={resumePendingImport} />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
Click izquierdo: cargar archivo CSV. Click derecho: descargar estructura (plantilla).
|
{csvMsg('intro_help')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
<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.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="catalogos">{csvMsg('tab_catalogos')}</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="transportes">Transportes</Tabs.Trigger>
|
<Tabs.Trigger value="transportes">{csvMsg('tab_transportes')}</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="importacion">Importación</Tabs.Trigger>
|
<Tabs.Trigger value="importacion">{csvMsg('tab_importacion')}</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="exportacion">Exportación</Tabs.Trigger>
|
<Tabs.Trigger value="exportacion">{csvMsg('tab_exportacion')}</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<Tabs.Content value="catalogos" class="space-y-4">
|
<Tabs.Content value="catalogos" class="space-y-4">
|
||||||
<!-- Catálogos: backend layouts_csv (customs_brokers, clients_and_providers, parts, boms, etc.) -->
|
<!-- Catálogos: backend layouts_csv (customs_brokers, clients_and_providers, parts, boms, etc.) -->
|
||||||
<div class="flex items-center justify-between">
|
<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>
|
</div>
|
||||||
<UploadLauncherGrid
|
<UploadLauncherGrid
|
||||||
items={catalogosConfig}
|
items={catalogosConfig}
|
||||||
@@ -1167,7 +1181,7 @@
|
|||||||
<Tabs.Content value="transportes" class="space-y-4">
|
<Tabs.Content value="transportes" class="space-y-4">
|
||||||
<!-- Logística: backend layouts_csv (vehicles, drivers, trailers); transportistas sin layouts_csv -->
|
<!-- Logística: backend layouts_csv (vehicles, drivers, trailers); transportistas sin layouts_csv -->
|
||||||
<div class="flex items-center justify-between">
|
<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>
|
</div>
|
||||||
<UploadLauncherGrid
|
<UploadLauncherGrid
|
||||||
items={transportesConfig}
|
items={transportesConfig}
|
||||||
@@ -1179,7 +1193,7 @@
|
|||||||
<Tabs.Content value="importacion" class="space-y-4">
|
<Tabs.Content value="importacion" class="space-y-4">
|
||||||
<!-- Operaciones de Importación: backend layouts_csv/facturas (api.imports) -->
|
<!-- Operaciones de Importación: backend layouts_csv/facturas (api.imports) -->
|
||||||
<div class="flex items-center justify-between">
|
<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>
|
</div>
|
||||||
<UploadLauncherGrid
|
<UploadLauncherGrid
|
||||||
items={importacionConfig}
|
items={importacionConfig}
|
||||||
@@ -1190,7 +1204,7 @@
|
|||||||
|
|
||||||
<Tabs.Content value="exportacion" class="space-y-4">
|
<Tabs.Content value="exportacion" class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<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>
|
</div>
|
||||||
<UploadLauncherGrid
|
<UploadLauncherGrid
|
||||||
items={exportacionConfig}
|
items={exportacionConfig}
|
||||||
@@ -1224,7 +1238,7 @@
|
|||||||
aria-busy="true"
|
aria-busy="true"
|
||||||
>
|
>
|
||||||
{#if csvResumeOverlayHint}
|
{#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}
|
||||||
{#if currentImportLabel}
|
{#if currentImportLabel}
|
||||||
<p class="text-xs font-medium text-foreground">{currentImportLabel}</p>
|
<p class="text-xs font-medium text-foreground">{currentImportLabel}</p>
|
||||||
@@ -1340,7 +1354,7 @@
|
|||||||
pollStatus();
|
pollStatus();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Error al iniciar la importación');
|
toast.error(csvMsg('toast.commit_err'));
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -40,16 +40,9 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173, // fija el puerto
|
port: 5173, // fija el puerto
|
||||||
host: true, // escucha en 0.0.0.0
|
host: true, // escucha en 0.0.0.0
|
||||||
allowedHosts: [
|
// Lista explícita + peticiones internas (p. ej. chunks JSON `?import`) pueden usar
|
||||||
'anexo76-dev.aduanasoft.com',
|
// Hosts distintos y recibir 403. `true` permite cualquier Host en dev.
|
||||||
// Requeridos para dev local, healthcheck del contenedor y E2E (Playwright
|
allowedHosts: true,
|
||||||
// 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
|
|
||||||
],
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/uploads': {
|
'/api/uploads': {
|
||||||
target: 'http://backend:8000',
|
target: 'http://backend:8000',
|
||||||
|
|||||||
Reference in New Issue
Block a user