feature/csv-clarion-exchange-type
This commit is contained in:
@@ -6,13 +6,34 @@ from decimal import Decimal
|
|||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
DATE_FORMATS: List[str] = ["%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y", "%d-%m-%Y", "%Y/%m/%d"]
|
DATE_FORMATS: List[str] = ["%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y", "%d-%m-%Y", "%Y/%m/%d"]
|
||||||
|
|
||||||
|
# Valores que envía el frontend (globalCsvParams dateFormat) -> formato strptime
|
||||||
|
# Cuando el usuario elige un formato en "Parámetros globales", solo se aceptan fechas en ese formato.
|
||||||
|
DATE_FORMAT_PREFERENCE_MAP: Dict[str, str] = {
|
||||||
|
"dd/mm/yyyy": "%d/%m/%Y",
|
||||||
|
"mm/dd/yyyy": "%m/%d/%Y",
|
||||||
|
"yyyy-mm-dd": "%Y-%m-%d",
|
||||||
|
}
|
||||||
|
|
||||||
CURRENCY_MAX = 7
|
CURRENCY_MAX = 7
|
||||||
|
|
||||||
|
|
||||||
def parse_date(val: Optional[str]) -> Optional[datetime]:
|
def parse_date(val: Optional[str], date_format_preference: Optional[str] = None) -> Optional[datetime]:
|
||||||
|
"""
|
||||||
|
Parsea fecha. Si date_format_preference está definido (formato elegido en el frontend),
|
||||||
|
solo se acepta ese formato; si la cadena no coincide, se rechaza.
|
||||||
|
Si no hay preferencia, se intentan todos los formatos (retrocompatibilidad).
|
||||||
|
"""
|
||||||
if not val or not str(val).strip():
|
if not val or not str(val).strip():
|
||||||
return None
|
return None
|
||||||
raw = str(val).strip()
|
raw = str(val).strip()
|
||||||
|
if date_format_preference and date_format_preference in DATE_FORMAT_PREFERENCE_MAP:
|
||||||
|
fmt = DATE_FORMAT_PREFERENCE_MAP[date_format_preference]
|
||||||
|
try:
|
||||||
|
parsed = datetime.strptime(raw, fmt)
|
||||||
|
return datetime.combine(parsed.date(), time.min)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
for fmt in DATE_FORMATS:
|
for fmt in DATE_FORMATS:
|
||||||
try:
|
try:
|
||||||
parsed = datetime.strptime(raw, fmt)
|
parsed = datetime.strptime(raw, fmt)
|
||||||
@@ -63,3 +84,57 @@ def check_optional_max_length(row: Dict[str, Any], col: str, max_len: int, line_
|
|||||||
if len(val) > max_len:
|
if len(val) > max_len:
|
||||||
return {"line": line_num, "col": col, "msg": f"Máximo {max_len} caracteres"}
|
return {"line": line_num, "col": col, "msg": f"Máximo {max_len} caracteres"}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
FECHA_MAX_LEN = 10
|
||||||
|
MSG_FECHA_LONGITUD = "Error: (Col. A) La Fecha: {fecha} supera la longitud de caracteres."
|
||||||
|
MSG_FECHA_LONGITUD_SOLUCION = "Capturar en la columna A el campo Fecha con este formato ##/##/####."
|
||||||
|
MSG_FECHA_DIA_INVALIDO = "Error: (Col. A) El día {dia} de la Fecha: {fecha} no es válido para el mes."
|
||||||
|
MSG_FECHA_DIA_SOLUCION = "Capturar correctamente en la columna A el día del campo Fecha, con este formato ##/##/####. (Día/Mes/Año)"
|
||||||
|
MSG_FECHA_MES_INVALIDO = "Error: (Col. A) El mes {mes} de la Fecha: {fecha} es mayor a 12 esto no es valido."
|
||||||
|
MSG_FECHA_MES_SOLUCION = "Capturar correctamente en la columna A el mes del campo Fecha, con este formato ##/##/####. (Día/Mes/Año)"
|
||||||
|
|
||||||
|
# Etiquetas para mensaje cuando no coincide con el formato elegido
|
||||||
|
DATE_FORMAT_LABELS: Dict[str, str] = {
|
||||||
|
"dd/mm/yyyy": "DD/MM/YYYY (Día/Mes/Año)",
|
||||||
|
"mm/dd/yyyy": "MM/DD/YYYY (Mes/Día/Año)",
|
||||||
|
"yyyy-mm-dd": "YYYY-MM-DD (Año-Mes-Día)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_fecha_clarion(
|
||||||
|
raw_fecha: str, line_num: int, date_format_preference: Optional[str] = None
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Valida la fecha según reglas Clarion: longitud ≤ 10, día válido para el mes, mes ≤ 12.
|
||||||
|
Sin límite de año. Si date_format_preference está definido (ej. dd/mm/yyyy), se prioriza ese formato.
|
||||||
|
"""
|
||||||
|
if not raw_fecha or not str(raw_fecha).strip():
|
||||||
|
return None
|
||||||
|
raw = str(raw_fecha).strip()
|
||||||
|
if len(raw) > FECHA_MAX_LEN:
|
||||||
|
return {
|
||||||
|
"line": line_num,
|
||||||
|
"col": "FECHA",
|
||||||
|
"msg": f"{MSG_FECHA_LONGITUD.format(fecha=raw)} {MSG_FECHA_LONGITUD_SOLUCION}",
|
||||||
|
}
|
||||||
|
parsed = parse_date(raw, date_format_preference)
|
||||||
|
if parsed is None:
|
||||||
|
format_label = (
|
||||||
|
DATE_FORMAT_LABELS.get(date_format_preference, "##/##/####")
|
||||||
|
if date_format_preference
|
||||||
|
else "##/##/####"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"line": line_num,
|
||||||
|
"col": "FECHA",
|
||||||
|
"msg": f"Error: (Col. A) La Fecha: {raw} no coincide con el formato elegido ({format_label}). {MSG_FECHA_LONGITUD_SOLUCION}",
|
||||||
|
}
|
||||||
|
d = parsed.date()
|
||||||
|
if d.month > 12:
|
||||||
|
return {
|
||||||
|
"line": line_num,
|
||||||
|
"col": "FECHA",
|
||||||
|
"msg": f"{MSG_FECHA_MES_INVALIDO.format(mes=d.month, fecha=raw)} {MSG_FECHA_MES_SOLUCION}",
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ def _str_or_none(val: Any, max_len: Optional[int] = None) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def row_to_exchange_rate_data(
|
def row_to_exchange_rate_data(
|
||||||
row_norm: Dict[str, Any], tenant_id: int, company_id: int
|
row_norm: Dict[str, Any], tenant_id: int, company_id: int, date_format_preference: Optional[str] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Build dict for ExchangeRate model. Returns {} if FECHA or VALOR invalid."""
|
"""Build dict for ExchangeRate model. Returns {} if FECHA or VALOR invalid."""
|
||||||
parsed_date = parse_date(row_norm.get("FECHA"))
|
parsed_date = parse_date(row_norm.get("FECHA"), date_format_preference)
|
||||||
value_decimal = parse_decimal_positive(row_norm.get("VALOR"))
|
value_decimal = parse_decimal_positive(row_norm.get("VALOR"))
|
||||||
if not parsed_date or value_decimal is None:
|
if not parsed_date or value_decimal is None:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, Depends
|
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from core.celery_app import celery_app
|
from core.celery_app import celery_app
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
@@ -40,11 +40,20 @@ def _get_redis():
|
|||||||
async def upload_import_file(
|
async def upload_import_file(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
company_id: int = Query(..., description="Company ID"),
|
company_id: int = Query(..., description="Company ID"),
|
||||||
|
reemplazar_sin_preguntar: bool = Query(
|
||||||
|
True,
|
||||||
|
description="Si True, reemplaza tipos de cambio existentes para la misma fecha; si False, solo agrega nuevos (omite fechas ya existentes)",
|
||||||
|
),
|
||||||
|
date_format: Optional[str] = Query(
|
||||||
|
None,
|
||||||
|
description="Formato de fecha del CSV: dd/mm/yyyy, mm/dd/yyyy o yyyy-mm-dd. Si no se envía, se intentan todos.",
|
||||||
|
),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fase 1: Subir CSV, guardar en Redis, encolar tarea de escaneo.
|
Fase 1: Subir CSV, guardar en Redis, encolar tarea de escaneo.
|
||||||
|
Parámetros globales de carga: reemplazar_sin_preguntar (Modo Reemplazar vs Actualizar), date_format (Formato de Fecha).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
@@ -63,6 +72,8 @@ async def upload_import_file(
|
|||||||
"company_id": company_id,
|
"company_id": company_id,
|
||||||
"user_id": current_user.get("id"),
|
"user_id": current_user.get("id"),
|
||||||
"template_id": "exchange_rates",
|
"template_id": "exchange_rates",
|
||||||
|
"reemplazar_sin_preguntar": reemplazar_sin_preguntar,
|
||||||
|
"date_format": date_format,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ def _do_scan(job_id: str, progress_callback: Optional[Any] = None) -> Dict[str,
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return {"status": "failed", "error": str(e)}
|
return {"status": "failed", "error": str(e)}
|
||||||
|
|
||||||
|
meta = common_meta.load_meta(file_path)
|
||||||
|
# Formato activo del selector del frontend; si no viene, mismo default que el front (dd/mm/yyyy)
|
||||||
|
date_format_preference = meta.get("date_format") or meta.get("dateFormat") or "dd/mm/yyyy"
|
||||||
|
|
||||||
error_count = 0
|
error_count = 0
|
||||||
processed_rows = 0
|
processed_rows = 0
|
||||||
errors_detail: List[Dict[str, Any]] = []
|
errors_detail: List[Dict[str, Any]] = []
|
||||||
@@ -66,7 +70,7 @@ def _do_scan(job_id: str, progress_callback: Optional[Any] = None) -> Dict[str,
|
|||||||
progress_callback(i, total_rows, error_count)
|
progress_callback(i, total_rows, error_count)
|
||||||
|
|
||||||
row_norm = _norm_row(row)
|
row_norm = _norm_row(row)
|
||||||
err = validate_row_exchange_rate(row_norm, i)
|
err = validate_row_exchange_rate(row_norm, i, raw_row=row, date_format_preference=date_format_preference)
|
||||||
if err:
|
if err:
|
||||||
error_count += 1
|
error_count += 1
|
||||||
error_lines_list.append(err["line"])
|
error_lines_list.append(err["line"])
|
||||||
@@ -116,9 +120,15 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return {"status": "failed", "error": str(e)}
|
return {"status": "failed", "error": str(e)}
|
||||||
|
|
||||||
|
meta = common_meta.load_meta(file_path)
|
||||||
|
reemplazar_sin_preguntar = meta.get("reemplazar_sin_preguntar", True)
|
||||||
|
date_format_preference = meta.get("date_format") or meta.get("dateFormat")
|
||||||
|
|
||||||
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
|
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
|
||||||
|
|
||||||
inserted_count = 0
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
skipped_duplicate = 0
|
||||||
skipped_invalid = 0
|
skipped_invalid = 0
|
||||||
skipped_details: List[Dict[str, Any]] = []
|
skipped_details: List[Dict[str, Any]] = []
|
||||||
meta_path = common_meta.get_meta_path(file_path)
|
meta_path = common_meta.get_meta_path(file_path)
|
||||||
@@ -142,7 +152,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
row_norm = _norm_row(row)
|
row_norm = _norm_row(row)
|
||||||
err = validate_row_exchange_rate(row_norm, i)
|
err = validate_row_exchange_rate(row_norm, i, raw_row=row, date_format_preference=date_format_preference)
|
||||||
if err:
|
if err:
|
||||||
skipped_invalid += 1
|
skipped_invalid += 1
|
||||||
skipped_details.append({
|
skipped_details.append({
|
||||||
@@ -151,7 +161,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data = row_to_exchange_rate_data(row_norm, tenant_id, company_id)
|
data = row_to_exchange_rate_data(row_norm, tenant_id, company_id, date_format_preference)
|
||||||
if not data or not data.get("date"):
|
if not data or not data.get("date"):
|
||||||
skipped_invalid += 1
|
skipped_invalid += 1
|
||||||
skipped_details.append({"line": i, "reason": "FECHA o VALOR no válidos"})
|
skipped_details.append({"line": i, "reason": "FECHA o VALOR no válidos"})
|
||||||
@@ -160,15 +170,19 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
key_date = data["date"].date() if hasattr(data["date"], "date") else data["date"]
|
key_date = data["date"].date() if hasattr(data["date"], "date") else data["date"]
|
||||||
existing = existing_by_date.get((tenant_id, company_id, key_date))
|
existing = existing_by_date.get((tenant_id, company_id, key_date))
|
||||||
if existing:
|
if existing:
|
||||||
|
if not reemplazar_sin_preguntar:
|
||||||
|
skipped_duplicate += 1
|
||||||
|
continue
|
||||||
existing.value = data["value"]
|
existing.value = data["value"]
|
||||||
existing.local_currency = data.get("local_currency")
|
existing.local_currency = data.get("local_currency")
|
||||||
existing.foreign_currency = data.get("foreign_currency")
|
existing.foreign_currency = data.get("foreign_currency")
|
||||||
session.add(existing)
|
session.add(existing)
|
||||||
|
updated_count += 1
|
||||||
else:
|
else:
|
||||||
new_er = ExchangeRate(**data)
|
new_er = ExchangeRate(**data)
|
||||||
session.add(new_er)
|
session.add(new_er)
|
||||||
existing_by_date[(tenant_id, company_id, key_date)] = new_er
|
existing_by_date[(tenant_id, company_id, key_date)] = new_er
|
||||||
inserted_count += 1
|
inserted_count += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -188,34 +202,34 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
|||||||
meta_path=meta_path,
|
meta_path=meta_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if inserted_count == 0 and skipped_invalid > 0:
|
if inserted_count == 0 and updated_count == 0 and (skipped_invalid > 0 or skipped_duplicate > 0):
|
||||||
return {
|
return {
|
||||||
"status": "warning",
|
"status": "warning",
|
||||||
"inserted": 0,
|
"inserted": 0,
|
||||||
"updated": 0,
|
"updated": 0,
|
||||||
"skipped_invalid": skipped_invalid,
|
"skipped_invalid": skipped_invalid,
|
||||||
"skipped_duplicate": 0,
|
"skipped_duplicate": skipped_duplicate,
|
||||||
"skipped_missing_fk": 0,
|
"skipped_missing_fk": 0,
|
||||||
"skipped_details": skipped_details,
|
"skipped_details": skipped_details,
|
||||||
"message": f"No se insertaron registros. {skipped_invalid} rechazados.",
|
"message": f"No se insertaron registros. {skipped_invalid} rechazados, {skipped_duplicate} omitidos por fecha existente.",
|
||||||
}
|
}
|
||||||
if inserted_count == 0:
|
if inserted_count == 0 and updated_count == 0:
|
||||||
return {
|
return {
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"error": "No hay registros válidos en el archivo CSV",
|
"error": "No hay registros válidos en el archivo CSV",
|
||||||
"inserted": 0,
|
"inserted": 0,
|
||||||
"updated": 0,
|
"updated": 0,
|
||||||
"skipped_invalid": skipped_invalid,
|
"skipped_invalid": skipped_invalid,
|
||||||
"skipped_duplicate": 0,
|
"skipped_duplicate": skipped_duplicate,
|
||||||
"skipped_missing_fk": 0,
|
"skipped_missing_fk": 0,
|
||||||
"skipped_details": skipped_details,
|
"skipped_details": skipped_details,
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"status": "finished",
|
"status": "finished",
|
||||||
"inserted": inserted_count,
|
"inserted": inserted_count,
|
||||||
"updated": 0,
|
"updated": updated_count,
|
||||||
"skipped_invalid": skipped_invalid,
|
"skipped_invalid": skipped_invalid,
|
||||||
"skipped_duplicate": 0,
|
"skipped_duplicate": skipped_duplicate,
|
||||||
"skipped_missing_fk": 0,
|
"skipped_missing_fk": 0,
|
||||||
"skipped_details": skipped_details,
|
"skipped_details": skipped_details,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,81 @@
|
|||||||
"""
|
"""
|
||||||
Validaciones comunes de fila para import CSV de tipos de cambio.
|
Validaciones comunes de fila para import CSV de tipos de cambio.
|
||||||
|
Paridad Clarion: desfase (Col C vacía), obligatorios (Col A Fecha, Col B Tipo de Cambio),
|
||||||
|
validación fecha (longitud, día acorde al mes, mes ≤ 12; sin límite de año).
|
||||||
"""
|
"""
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from ..common.common_validators import (
|
from ..common.common_validators import (
|
||||||
check_required_date,
|
|
||||||
check_required_value_positive,
|
check_required_value_positive,
|
||||||
check_optional_max_length,
|
check_optional_max_length,
|
||||||
|
validate_fecha_clarion,
|
||||||
CURRENCY_MAX,
|
CURRENCY_MAX,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_row_exchange_rate(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
|
MSG_DESFASE = "Error: Existe un desfase en esta línea."
|
||||||
|
MSG_DESFASE_SOLUCION = "Revisar esta línea del archivo CSV y verificar cada campo este en la posicion correcta."
|
||||||
|
|
||||||
|
MSG_OBLIGATORIOS = "Existen campos vacios que son obligatorios, es la (Col.A) Fecha , (Col.B) Tipo de Cambio."
|
||||||
|
MSG_OBLIGATORIOS_SOLUCION = "Revisar la línea del archivo y capturar los campos con la información correcta."
|
||||||
|
|
||||||
|
|
||||||
|
def validate_row_desfase(raw_row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Si la fila tiene 3 o más columnas y la 3ª tiene valor, error de desfase (Clarion ColumnaC <> '').
|
||||||
|
"""
|
||||||
|
values_ordered = list(raw_row.values()) if raw_row else []
|
||||||
|
if len(values_ordered) >= 3 and (values_ordered[2] or "").strip():
|
||||||
|
return {
|
||||||
|
"line": line_num,
|
||||||
|
"col": "",
|
||||||
|
"msg": f"{MSG_DESFASE} {MSG_DESFASE_SOLUCION}",
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_row_required_exchange_rate(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""FECHA y VALOR obligatorios con mensaje Clarion."""
|
||||||
|
fecha = (row.get("FECHA") or "").strip()
|
||||||
|
valor = (row.get("VALOR") or "").strip()
|
||||||
|
if not fecha or not valor:
|
||||||
|
return {
|
||||||
|
"line": line_num,
|
||||||
|
"col": "FECHA" if not fecha else "VALOR",
|
||||||
|
"msg": f"{MSG_OBLIGATORIOS} {MSG_OBLIGATORIOS_SOLUCION}",
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_row_fecha_clarion(
|
||||||
|
row: Dict[str, Any], line_num: int, date_format_preference: Optional[str] = None
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Longitud ≤ 10 y fecha válida (día acorde al mes, mes ≤ 12; sin límite de año)."""
|
||||||
|
raw_fecha = (row.get("FECHA") or "").strip()
|
||||||
|
if not raw_fecha:
|
||||||
|
return None
|
||||||
|
return validate_fecha_clarion(raw_fecha, line_num, date_format_preference)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_row_exchange_rate(
|
||||||
|
row: Dict[str, Any],
|
||||||
|
line_num: int,
|
||||||
|
raw_row: Optional[Dict[str, Any]] = None,
|
||||||
|
date_format_preference: Optional[str] = None,
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Valida una fila de CSV de tipos de cambio.
|
Valida una fila de CSV de tipos de cambio.
|
||||||
FECHA y VALOR requeridos; MONEDA_LOCAL y MONEDA_EXTRANJERA opcionales (max 7).
|
Orden: desfase (si raw_row) → obligatorios → fecha Clarion → VALOR > 0 → MONEDA opc (max 7).
|
||||||
|
date_format_preference: valor del parámetro global (ej. dd/mm/yyyy, mm/dd/yyyy, yyyy-mm-dd).
|
||||||
"""
|
"""
|
||||||
err = check_required_date(row, line_num)
|
if raw_row is not None:
|
||||||
|
err = validate_row_desfase(raw_row, line_num)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
err = validate_row_required_exchange_rate(row, line_num)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
err = validate_row_fecha_clarion(row, line_num, date_format_preference)
|
||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
err = check_required_value_positive(row, line_num)
|
err = check_required_value_positive(row, line_num)
|
||||||
|
|||||||
@@ -437,11 +437,20 @@ export const api = {
|
|||||||
|
|
||||||
// CSV import for Tipos de Cambio (flujo en general_catalogs/exchange_rate/imports)
|
// CSV import for Tipos de Cambio (flujo en general_catalogs/exchange_rate/imports)
|
||||||
exchangeRateImports: {
|
exchangeRateImports: {
|
||||||
upload: (file: File, companyId: number) => {
|
upload: (
|
||||||
|
file: File,
|
||||||
|
companyId: number,
|
||||||
|
params?: { reemplazar_sin_preguntar?: boolean; date_format?: string }
|
||||||
|
) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
const search = new URLSearchParams({ company_id: String(companyId) });
|
||||||
|
if (params?.reemplazar_sin_preguntar !== undefined)
|
||||||
|
search.set('reemplazar_sin_preguntar', String(!!params.reemplazar_sin_preguntar));
|
||||||
|
if (params?.date_format != null && params.date_format !== '')
|
||||||
|
search.set('date_format', params.date_format);
|
||||||
return fetchApi(
|
return fetchApi(
|
||||||
`/v1/a76/exchange-rate/imports/upload?company_id=${companyId}`,
|
`/v1/a76/exchange-rate/imports/upload?${search.toString()}`,
|
||||||
{ method: 'POST', body: formData }
|
{ method: 'POST', body: formData }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
let totalSkipped = $derived(
|
let totalSkipped = $derived(
|
||||||
(commitResults?.skipped_invalid || 0) +
|
(commitResults?.skipped_invalid || 0) +
|
||||||
(commitResults?.skipped_missing_fk || 0) +
|
(commitResults?.skipped_missing_fk || 0) +
|
||||||
(commitResults?.skipped_missing_invoice || 0)
|
(commitResults?.skipped_missing_invoice || 0) +
|
||||||
|
(commitResults?.skipped_duplicate || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleOpenChange(newOpen: boolean) {
|
function handleOpenChange(newOpen: boolean) {
|
||||||
|
|||||||
@@ -176,7 +176,16 @@
|
|||||||
|
|
||||||
if (useExchangeRateImport) {
|
if (useExchangeRateImport) {
|
||||||
try {
|
try {
|
||||||
const res = await api.exchangeRateImports.upload(file, companyId);
|
const catalogosSettings = allSettings['catalogos'] || {};
|
||||||
|
const globalMode = globalSettings['mode'] ?? catalogosSettings['mode'] ?? 'update';
|
||||||
|
const reemplazar_sin_preguntar = globalMode === 'replace';
|
||||||
|
// Formato activo del selector global (Formato de Fecha); mismo default que la barra
|
||||||
|
const dateFormat =
|
||||||
|
globalSettings['dateFormat'] ?? catalogosSettings['dateFormat'] ?? globalCsvParams.find((p) => p.name === 'dateFormat')?.defaultValue ?? 'dd/mm/yyyy';
|
||||||
|
const res = await api.exchangeRateImports.upload(file, companyId, {
|
||||||
|
reemplazar_sin_preguntar,
|
||||||
|
date_format: dateFormat
|
||||||
|
});
|
||||||
if (res.data?.job_id) {
|
if (res.data?.job_id) {
|
||||||
currentJobId = res.data.job_id;
|
currentJobId = res.data.job_id;
|
||||||
pollStatus();
|
pollStatus();
|
||||||
@@ -518,16 +527,20 @@
|
|||||||
commitResults = res.data;
|
commitResults = res.data;
|
||||||
showResultModal = true;
|
showResultModal = true;
|
||||||
const inserted = res.data?.inserted || 0;
|
const inserted = res.data?.inserted || 0;
|
||||||
|
const updated = res.data?.updated || 0;
|
||||||
const skippedInvalid = res.data?.skipped_invalid || 0;
|
const skippedInvalid = res.data?.skipped_invalid || 0;
|
||||||
const skippedFk = res.data?.skipped_missing_fk || 0;
|
const skippedFk = res.data?.skipped_missing_fk || 0;
|
||||||
const skippedDup = res.data?.skipped_duplicate || 0;
|
const skippedDup = res.data?.skipped_duplicate || 0;
|
||||||
const skippedDetails = res.data?.skipped_details || [];
|
const skippedDetails = res.data?.skipped_details || [];
|
||||||
|
|
||||||
if (inserted > 0) {
|
if (inserted > 0 || updated > 0) {
|
||||||
toast.success(`Importación completada: ${inserted} registros insertados`);
|
const parts = [];
|
||||||
|
if (inserted > 0) parts.push(`${inserted} insertados`);
|
||||||
|
if (updated > 0) parts.push(`${updated} actualizados`);
|
||||||
|
toast.success(`Importación completada: ${parts.join(', ')}`);
|
||||||
if (skippedInvalid > 0 || skippedFk > 0 || skippedDup > 0) {
|
if (skippedInvalid > 0 || skippedFk > 0 || skippedDup > 0) {
|
||||||
const totalSkipped = skippedInvalid + skippedFk + skippedDup;
|
const totalSkipped = skippedInvalid + skippedFk + skippedDup;
|
||||||
toast.warning(`${totalSkipped} registros fueron rechazados`);
|
toast.warning(`${totalSkipped} registros fueron rechazados u omitidos`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
||||||
|
|||||||
Reference in New Issue
Block a user