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 7ae354d7..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); @@ -503,7 +511,7 @@ >
- +
- +
- +
- - {#if formData.pedimento_type === 'consolidated'} -
- - -
- {/if} + +
+ + +
diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte index 21eb5399..61921c6c 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte @@ -83,11 +83,8 @@ authenticated?: boolean; } - import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate'; - import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte'; import { companyStore } from '$lib/stores/company.svelte'; import PrerequisitesModal from '$lib/components/dashboard/PrerequisitesModal.svelte'; - import { getCurrentLocalDate } from '$lib/date-utils'; let { data }: { data: ExtendedPageData } = $props(); @@ -123,10 +120,6 @@ }) ); - // Dialog state lifted up - let showExchangeRateDialog = $state(false); - let missingExchangeRateDate = $state(''); - // Prerrequisitos: modal solo en creación cuando no hay agentes o clientes const agentsCount = $derived(data.customsBrokers?.length ?? 0); const clientsCount = $derived(data.clients?.length ?? 0); @@ -141,31 +134,58 @@ } }); - async function checkPaymentDateRate(date: string): Promise { - if (!date || !companyStore.activeCompany?.id) return true; - - try { - const rate = await getExchangeRateByDate(date, companyStore.activeCompany.id); - if (!rate) { - missingExchangeRateDate = date; - showExchangeRateDialog = true; - return false; - } - return true; - } catch (error) { - console.error('Error checking payment date rate:', error); - // Si falla, forzamos diálogo para seguridad - missingExchangeRateDate = date; - showExchangeRateDialog = true; - return false; - } + function normalizeDateValue(value: string | null | undefined): string | null { + if (!value) return null; + return value.includes('T') ? value.slice(0, 10) : value; } - function getExchangeDateForPedimento(formData: any): { date: string | null; label: string } { - return { - date: formData?.start_date || null, - label: 'fecha de inicio' - }; + function validateLegacyDateConsistency(formData: any): boolean { + const startDate = normalizeDateValue(formData?.start_date); + const endDate = normalizeDateValue(formData?.end_date); + const entryDate = normalizeDateValue(formData?.entry_date); + const paymentDate = normalizeDateValue(formData?.payment_date); + const referenceDate = paymentDate || entryDate; + const pedimentoType = formData?.pedimento_type; + const isConsolidated = pedimentoType === 'consolidated'; + const isIndividual = pedimentoType === 'normal'; + + if (!startDate || !endDate || !referenceDate) return true; + + // Legacy Clarion (CON): inicio <= fin <= fecha de referencia (pago / entrada) + if (isConsolidated) { + if (startDate > endDate) { + toast.error( + `La fecha inicio (${startDate}) no puede ser mayor que la fecha final (${endDate}).` + ); + activeTab = 'general'; + return false; + } + + if (startDate > referenceDate) { + toast.error( + `La fecha inicio (${startDate}) no puede ser mayor que la fecha de referencia (${referenceDate}).` + ); + activeTab = 'general'; + return false; + } + + if (endDate > referenceDate) { + toast.error( + `La fecha final (${endDate}) no puede ser mayor que la fecha de referencia (${referenceDate}).` + ); + activeTab = 'general'; + return false; + } + } + + // Legacy Clarion (IND): advertencia si las fechas no coinciden, sin bloqueo. + if (isIndividual && (startDate !== endDate || startDate !== referenceDate)) { + toast.warning( + 'Advertencia: en pedimento individual las fechas inicio/final/referencia son distintas.' + ); + } + + return true; } // ID del pedimento @@ -410,35 +430,19 @@ saving = true; try { - // Verificar tipo de cambio según catalogo de transporte (E/P) - if (generalTabInstance && generalFormData) { - const exchangeRef = getExchangeDateForPedimento(generalFormData); - const isConsolidated = generalFormData.pedimento_type === 'consolidated'; - const todayStr = getCurrentLocalDate(); - const isFutureDate = exchangeRef.date && exchangeRef.date > todayStr; - - // Solo verificar si NO es un consolidado con fecha futura - if (!(isConsolidated && isFutureDate)) { - const rateExists = await generalTabInstance.checkPaymentDateRate(exchangeRef.date || ''); - if (!rateExists) { - saving = false; - // Asegurar que se muestre el tab general - activeTab = 'general'; - return; - } - } - } - - // Validar campos requeridos para creación (client_id es opcional) - if (data.isCreate && generalFormData) { + // Validar campos requeridos para guardar (client_id es opcional) + if (generalFormData) { const requiredFields: Record = { - year: 'Año', customs_office: 'Aduana', license: 'Patente', pedimento_number: 'Número de Pedimento', - start_date: 'Fecha de Inicio', + operation_type: 'Tipo de Operación', + pedimento_type: 'Tipo de Pedimento', pedimento_code: 'Clave', - regime: 'Régimen' + regime: 'Régimen', + start_date: 'Fecha de Inicio', + end_date: 'Fecha Final', + entry_date: 'Fecha de Entrada' }; const missingFields: string[] = []; @@ -454,41 +458,9 @@ } } - // Validar tipo de cambio en create y update segun fecha efectiva del metodo de transporte - if (generalFormData) { - const exchangeRef = getExchangeDateForPedimento(generalFormData); - const rate = generalFormData.exchange_rate; - const isConsolidated = generalFormData.pedimento_type === 'consolidated'; - const todayStr = getCurrentLocalDate(); - const isFutureDate = exchangeRef.date && exchangeRef.date > todayStr; - - if ( - rate === null || - rate === undefined || - String(rate).trim() === '' || - Number(rate) <= 0 - ) { - // Si es consolidado y fecha futura, permitimos continuar con un warning - if (isConsolidated && isFutureDate) { - toast.warning( - `Aviso: No hay tipo de cambio para la ${exchangeRef.label} (${exchangeRef.date}), pero se permite continuar por ser pedimento consolidado.` - ); - } else { - saving = false; - activeTab = 'general'; - const date = exchangeRef.date || ''; - toast.error( - Number(rate) <= 0 && rate !== null && rate !== undefined - ? `El tipo de cambio debe ser mayor a 0. Registra el tipo de cambio para la ${exchangeRef.label}.` - : `No hay tipo de cambio registrado para la ${exchangeRef.label}. Por favor, regístralo antes de guardar.` - ); - if (date) { - missingExchangeRateDate = date; - showExchangeRateDialog = true; - } - return; - } - } + if (generalFormData && !validateLegacyDateConsistency(generalFormData)) { + saving = false; + return; } // Construir el payload unificado @@ -1063,13 +1035,9 @@ errorStr.includes('No existe un Tipo de Cambio registrado') || errorStr.includes('financials.exchange_rate') ) { - // Interceptar error de tipo de cambio - console.log('Interceptor: Exchange rate missing error caught (Pedimento).'); - - const missingDate = dateMatch ? dateMatch[0] : (generalFormData?.start_date || ''); - - missingExchangeRateDate = missingDate; - showExchangeRateDialog = true; + toast.error( + '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; }