Merge branch 'featuere/pedimento-logica-clarion' into feature/focus-error-facturas
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<boolean> {
|
||||
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 @@
|
||||
>
|
||||
<!-- Año -->
|
||||
<div class="space-y-2">
|
||||
<Label for="year">Año <span class="text-red-500">*</span></Label>
|
||||
<Label for="year">Año</Label>
|
||||
<Input
|
||||
id="year"
|
||||
bind:value={formData.year}
|
||||
@@ -612,12 +620,7 @@
|
||||
|
||||
<!-- Tipo de Cambio -->
|
||||
<div class="space-y-2">
|
||||
<Label for="exchange_rate">
|
||||
Tipo de Cambio
|
||||
{#if !(formData.pedimento_type === 'consolidated' && getEffectiveExchangeDate() > getCurrentLocalDate())}
|
||||
<span class="text-red-500">*</span>
|
||||
{/if}
|
||||
</Label>
|
||||
<Label for="exchange_rate">Tipo de Cambio</Label>
|
||||
<Input
|
||||
id="exchange_rate"
|
||||
type="text"
|
||||
@@ -843,7 +846,7 @@
|
||||
|
||||
<!-- Destino -->
|
||||
<div class="space-y-2">
|
||||
<Label for="destino">Destino <span class="text-red-500">*</span></Label>
|
||||
<Label for="destino">Destino</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.pedimento_transport_means.destination
|
||||
@@ -1025,13 +1028,11 @@
|
||||
<Input id="entry_date" type="date" bind:value={formData.entry_date} />
|
||||
</div>
|
||||
|
||||
<!-- Fecha Fin (Solo Consolidado) -->
|
||||
{#if formData.pedimento_type === 'consolidated'}
|
||||
<div class="space-y-2">
|
||||
<Label for="end_date">Fecha Fin <span class="text-red-500">*</span></Label>
|
||||
<Input id="end_date" type="date" bind:value={formData.end_date} />
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Fecha Fin -->
|
||||
<div class="space-y-2">
|
||||
<Label for="end_date">Fecha Final <span class="text-red-500">*</span></Label>
|
||||
<Input id="end_date" type="date" bind:value={formData.end_date} />
|
||||
</div>
|
||||
|
||||
<!-- Fecha de Pago -->
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<string, string> = {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user