From eb5df06d24ee75c025fe6a8523ebb9851129fef6 Mon Sep 17 00:00:00 2001 From: hreyes Date: Mon, 27 Apr 2026 12:01:55 -0600 Subject: [PATCH] feature/logica-pedimento-clarion --- .../layouts_csv/pedmientos/common/mappers.py | 21 +++--- .../layouts_csv/pedmientos/template_config.py | 12 ++- .../pedmientos/validators/common.py | 73 ++++++++++++++++++- .../pedimentos/edit/general-tab-form.svelte | 14 +++- .../pedimentos/edit/[id]/+page.svelte | 4 +- 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/mappers.py b/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/mappers.py index 3975f19a..e83ebb44 100644 --- a/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/mappers.py +++ b/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/mappers.py @@ -2,7 +2,6 @@ Mapeo fila CSV → datos para PedimentosCreate. Soporta layout Clarion (PEDIMENTO, TIPO_OPERACION, CLAVE_PEDIMENTO, etc.) y legacy (AÑO, ADUANA, PATENTE, NUMERO). """ -from datetime import datetime from decimal import Decimal, InvalidOperation from typing import Dict, Any, Optional @@ -71,8 +70,11 @@ def _row_to_pedimento_data_clarion( elif tipo == "E": data["operation_type"] = "exp" - ind_con = (row_norm.get("INDIVIDUAL_CONSOLIDADO") or "").strip().upper() or "CON" - data["pedimento_type"] = "normal" if ind_con == "IND" else "consolidated" + ind_con = (row_norm.get("INDIVIDUAL_CONSOLIDADO") or "").strip().upper() + if ind_con == "IND": + data["pedimento_type"] = "normal" + elif ind_con == "CON": + data["pedimento_type"] = "consolidated" status = (row_norm.get("ESTATUS") or "").strip() if status: @@ -87,19 +89,20 @@ def _row_to_pedimento_data_clarion( data["observations"] = obs # Fechas E, F, G → pedimento_dates + # Paridad legacy: Col.G es la fecha de referencia operativa (pago/entrada). start_str = (row_norm.get("FECHA_INICIO") or "").strip() end_str = (row_norm.get("FECHA_FINAL") or "").strip() payment_str = (row_norm.get("FECHA_PAGO") or "").strip() - base = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - start_dt = parse_date(start_str, date_format_preference) if start_str else base - end_dt = parse_date(end_str, date_format_preference) if end_str else base - payment_dt = parse_date(payment_str, date_format_preference) if payment_str else base + start_dt = parse_date(start_str, date_format_preference) if start_str else None + end_dt = parse_date(end_str, date_format_preference) if end_str else None + payment_dt = parse_date(payment_str, date_format_preference) if payment_str else None if start_dt and end_dt: + entry_dt = payment_dt or start_dt data["pedimento_dates"] = { - "entry_date": start_dt, + "entry_date": entry_dt, "end_date": end_dt, "start_date": start_dt, - "payment_date": payment_dt, + "payment_date": payment_dt or entry_dt, } return data diff --git a/backend/api/v1/modules/a76/layouts_csv/pedmientos/template_config.py b/backend/api/v1/modules/a76/layouts_csv/pedmientos/template_config.py index b855d390..e18541ef 100644 --- a/backend/api/v1/modules/a76/layouts_csv/pedmientos/template_config.py +++ b/backend/api/v1/modules/a76/layouts_csv/pedmientos/template_config.py @@ -34,7 +34,17 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = { # Col E, F, G {"canonical": "FECHA_INICIO", "aliases": ["FECHA INICIO", "FECHA INICIAL"]}, {"canonical": "FECHA_FINAL", "aliases": ["FECHA FINAL", "FECHA FIN"]}, - {"canonical": "FECHA_PAGO", "aliases": ["FECHA DE PAGO", "FECHA PAGO"]}, + { + "canonical": "FECHA_PAGO", + "aliases": [ + "FECHA DE PAGO", + "FECHA PAGO", + "FECHA_ENTRADA", + "FECHA ENTRADA", + "ENTRY_DATE", + "ENTRY DATE", + ], + }, # Col H {"canonical": "ADUANA_SECCION_CRUCE", "aliases": ["ADUANA Y SECCION DE CRUCE", "ADUANA Y SECCION CRUCE", "ADUANA", "CUSTOMS_OFFICE", "CUSTOMS OFFICE"]}, # Col I diff --git a/backend/api/v1/modules/a76/layouts_csv/pedmientos/validators/common.py b/backend/api/v1/modules/a76/layouts_csv/pedmientos/validators/common.py index ce840f19..9df12c92 100644 --- a/backend/api/v1/modules/a76/layouts_csv/pedmientos/validators/common.py +++ b/backend/api/v1/modules/a76/layouts_csv/pedmientos/validators/common.py @@ -180,7 +180,7 @@ def validate_row_patente( def validate_row_pedimento_required_full( row: Dict[str, Any], line_num: int ) -> Optional[Dict[str, Any]]: - """Obligatorios cuando no es actualizar: TIPO (B), CLAVE_PEDIMENTO (C), REGIMEN (D), FECHA_INICIO (E), FECHA_FINAL (F), FECHA_PAGO (G), ADUANA_SECCION_CRUCE (H).""" + """Obligatorios cuando no es actualizar: TIPO (B), CLAVE_PEDIMENTO (C), REGIMEN (D), FECHA_INICIO (E), FECHA_FINAL (F), FECHA_PAGO/ENTRADA (G), ADUANA_SECCION_CRUCE (H), IND/CON (J).""" cols_missing = [] col_labels = [ ("TIPO_OPERACION", "Col.B) Tipo Operación"), @@ -188,8 +188,9 @@ def validate_row_pedimento_required_full( ("REGIMEN", "Col.D) Clave Régimen"), ("FECHA_INICIO", "Col.E) Fecha Inicio"), ("FECHA_FINAL", "Col.F) Fecha Final"), - ("FECHA_PAGO", "Col.G) Fecha de Pago"), + ("FECHA_PAGO", "Col.G) Fecha de Entrada/Referencia"), ("ADUANA_SECCION_CRUCE", "Col.H) Aduana y Sección de Cruce"), + ("INDIVIDUAL_CONSOLIDADO", "Col.J) Tipo de Pedimento (IND/CON)"), ] for key, label in col_labels: if not (row.get(key) or "").strip(): @@ -375,9 +376,77 @@ def validaciones_pedimento( if err: errors.append(err) + err = validate_row_fechas_coherencia_clarion(row, line_num, date_format_preference) + if err: + errors.append(err) + return errors +def validate_row_fechas_coherencia_clarion( + row: Dict[str, Any], + line_num: int, + date_format_preference: Optional[str] = None, +) -> Optional[Dict[str, Any]]: + """ + Reglas legacy Clarion: + - CON: inicio <= final <= fecha de referencia (pago/entrada). + - IND: si las fechas difieren, advertencia no bloqueante. + En este layout CSV la referencia operativa está en FECHA_PAGO (Col G). + """ + start_raw = (row.get("FECHA_INICIO") or "").strip() + end_raw = (row.get("FECHA_FINAL") or "").strip() + ref_raw = (row.get("FECHA_PAGO") or "").strip() + if not start_raw or not end_raw or not ref_raw: + return None + + start_dt = parse_date(start_raw, date_format_preference) + end_dt = parse_date(end_raw, date_format_preference) + ref_dt = parse_date(ref_raw, date_format_preference) + if not start_dt or not end_dt or not ref_dt: + return None + + start_date = start_dt.date() + end_date = end_dt.date() + ref_date = ref_dt.date() + + ind_con = (row.get("INDIVIDUAL_CONSOLIDADO") or "").strip().upper() + # Compatibilidad legacy: vacío se trata como consolidado. + if not ind_con: + ind_con = "CON" + + if ind_con == "CON": + if start_date > end_date: + return { + "line": line_num, + "col": "FECHA_FINAL", + "msg": "Error: La fecha inicio no puede ser mayor que la fecha final.", + } + if start_date > ref_date: + return { + "line": line_num, + "col": "FECHA_PAGO", + "msg": "Error: La fecha inicio no puede ser mayor que la fecha de referencia (Col.G).", + } + if end_date > ref_date: + return { + "line": line_num, + "col": "FECHA_PAGO", + "msg": "Error: La fecha final no puede ser mayor que la fecha de referencia (Col.G).", + } + return None + + if ind_con == "IND": + if not (start_date == end_date == ref_date): + return { + "line": line_num, + "col": "FECHA_INICIO", + "msg": "Advertencia: En pedimento individual las fechas inicio/final/referencia son distintas.", + "warning": True, + } + return None + + # --- Legacy (layout sin PEDIMENTO único): mantener para compatibilidad --- def validate_row_pedimento_required_legacy( row: Dict[str, Any], line_num: int diff --git a/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte index 052effca..49061420 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte @@ -382,6 +382,11 @@ return 'Inicio'; } + function isFutureEffectiveDate(effectiveDate: string | null): boolean { + if (!effectiveDate) return false; + return effectiveDate > getCurrentLocalDate(); + } + let lastTrigger = 0; // Obtener automáticamente el tipo de cambio cuando cambie la fecha efectiva @@ -414,9 +419,11 @@ console.warn('⚠️ [TIPO CAMBIO] No encontrado para fecha:', effectiveDate); if (formData) { formData.exchange_rate = null; - // Mostrar el diálogo automáticamente si no existe el tipo de cambio - missingExchangeRateDate = effectiveDate; - showExchangeRateDialog = true; + // Para fecha futura no forzamos captura de TC/DOF. + if (!isFutureEffectiveDate(effectiveDate)) { + missingExchangeRateDate = effectiveDate; + showExchangeRateDialog = true; + } } } }) @@ -447,6 +454,7 @@ export async function checkPaymentDateRate(date: string): Promise { const effectiveDate = date || getEffectiveExchangeDate(); if (!effectiveDate || !companyStore.activeCompany?.id) return true; + if (isFutureEffectiveDate(effectiveDate)) return true; try { const rate = await getExchangeRateByDate(effectiveDate, companyStore.activeCompany.id); diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte index 0ca75b93..61921c6c 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte @@ -1035,10 +1035,8 @@ errorStr.includes('No existe un Tipo de Cambio registrado') || errorStr.includes('financials.exchange_rate') ) { - // Evitamos bloquear por validaciones locales de tipo de cambio, pero - // si backend lo rechaza informamos el motivo y mantenemos al usuario en General. toast.error( - 'El backend rechazó el guardado por tipo de cambio faltante o inválido. Revisa la fecha de inicio/pago y registra el tipo de cambio requerido.' + 'No hay tipo de cambio para la fecha seleccionada. Si es una fecha futura, puedes guardar y capturarlo después; si no es futura, registra el tipo de cambio.' ); activeTab = 'general'; return;