diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py index 524a7e9e..dd7f048e 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py @@ -8,7 +8,7 @@ class PedimentoDatesBase(BaseModel): """Base schema for Pedimento Dates""" entry_date: Optional[datetime] = Field(None, description="Entry date") - payment_date: datetime = Field(None, description="Payment date") + payment_date: datetime = Field(..., description="Payment date") rectification_payment_date: Optional[datetime] = Field( None, description="Rectification payment date" ) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py index f5639249..da6fb6aa 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py @@ -11,8 +11,8 @@ class PedimentoTransportMeansBase(BaseModel): tenant_id: int = Field(..., description="Tenant ID") destination: Optional[int] = Field(None, description="Destination") entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") - arrival: Optional[str] = Field(None, max_length=2, description="Arrival") - departure: Optional[str] = Field(None, max_length=2, description="Departure") + arrival: str = Field(..., max_length=2, description="Arrival") + departure: str = Field(..., max_length=2, description="Departure") class PedimentoTransportMeansCreate(BaseModel): @@ -20,8 +20,8 @@ class PedimentoTransportMeansCreate(BaseModel): destination: Optional[int] = Field(None, description="Destination") entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") - arrival: Optional[str] = Field(None, max_length=2, description="Arrival") - departure: Optional[str] = Field(None, max_length=2, description="Departure") + arrival: str = Field(..., max_length=2, description="Arrival") + departure: str = Field(..., max_length=2, description="Departure") class PedimentoTransportMeansUpdate(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py index c34dc9ff..32598d75 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py @@ -33,7 +33,7 @@ class PedimentosBase(BaseModel): year: Optional[str] = Field(None, max_length=2, description="Year") customs_office: Optional[str] = Field( - None, max_length=2, description="Customs office" + None, max_length=3, description="Customs office" ) license: Optional[str] = Field(None, max_length=4, description="License") pedimento_number: Optional[str] = Field( @@ -41,11 +41,11 @@ class PedimentosBase(BaseModel): ) client_id: Optional[int] = Field(None, description="Client ID") operation_type: Optional[int] = Field(None, description="Operation type") - pedimento_type: Optional[int] = Field(None, description="Pedimento type") - pedimento_code: Optional[str] = Field( - None, max_length=2, description="Pedimento key" + pedimento_type: Optional[str] = Field(None, max_length=20, description="Pedimento type") + pedimento_code: str = Field( + ..., max_length=2, description="Pedimento key" ) - regime: Optional[str] = Field(None, max_length=3, description="Regime") + regime: str = Field(..., max_length=3, description="Regime") status: Optional[str] = Field(None, max_length=30, description="Status") usd_value: Optional[Decimal] = Field(None, description="USD value") paid_price: Optional[Decimal] = Field(None, description="Paid price") @@ -57,14 +57,17 @@ class PedimentosCreate(PedimentosBase): # Override to make required fields non-optional year: str = Field(..., max_length=2, description="Year") - customs_office: str = Field(..., max_length=2, description="Customs office") + customs_office: str = Field(..., max_length=3, description="Customs office") license: str = Field(..., max_length=4, description="License") pedimento_number: str = Field(..., max_length=7, description="Pedimento number") client_id: int = Field(..., description="Client ID") operation_type: int = Field(..., description="Operation type") - pedimento_type: int = Field(..., description="Pedimento type") + pedimento_type: str = Field(..., max_length=20, description="Pedimento type") + pedimento_code: str = Field( + ..., max_length=2, description="Pedimento key" + ) regime: str = Field(..., max_length=3, description="Regime") - status: str = Field(..., max_length=30, description="Status") + status: str = Field(..., max_length=30, description="Status") pedimento_dates: Optional[PedimentoDatesCreate] = None pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None @@ -87,12 +90,12 @@ class PedimentosUpdate(BaseModel): """Schema for updating a Pedimento""" year: Optional[str] = Field(None, max_length=2) - customs_office: Optional[str] = Field(None, max_length=2) + customs_office: Optional[str] = Field(None, max_length=3) license: Optional[str] = Field(None, max_length=4) pedimento_number: Optional[str] = Field(None, max_length=7) client_id: Optional[int] = None operation_type: Optional[int] = None - pedimento_type: Optional[int] = None + pedimento_type: Optional[str] = Field(None, max_length=20) pedimento_code: Optional[str] = Field(None, max_length=2) regime: Optional[str] = Field(None, max_length=3) status: Optional[str] = Field(None, max_length=30) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py index ad4f9a6e..3f414f1c 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -94,12 +94,12 @@ class Pedimentos(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(Integer) year: Mapped[str] = mapped_column(String(2)) - customs_office: Mapped[str] = mapped_column(String(2)) + customs_office: Mapped[str] = mapped_column(String(3)) license: Mapped[str] = mapped_column(String(4)) pedimento_number: Mapped[str] = mapped_column(String(7)) client_id: Mapped[int] = mapped_column(Integer) operation_type: Mapped[int] = mapped_column(Integer) - pedimento_type: Mapped[int] = mapped_column(Integer) + pedimento_type: Mapped[str] = mapped_column(String(20)) pedimento_code: Mapped[str] = mapped_column(String(2)) regime: Mapped[str] = mapped_column(String(3)) status: Mapped[str] = mapped_column(String(30)) diff --git a/frontend/src/lib/api/dashboard/a76/exchange-rate.ts b/frontend/src/lib/api/dashboard/a76/exchange-rate.ts new file mode 100644 index 00000000..d7843807 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/exchange-rate.ts @@ -0,0 +1,44 @@ +/** + * API Client para Exchange Rate + */ +import { api } from '$lib/api'; + +export interface ExchangeRate { + id: number; + date: string; + value: number; + local_currency: string | null; + foreign_currency: string | null; + company_id: number; + tenant_id: number; +} + +export interface ExchangeRateListResponse { + items: ExchangeRate[]; + total: number; + page: number; + size: number; + pages: number; +} + +/** + * Get exchange rate by date + */ +export async function getExchangeRateByDate(date: string): Promise { + try { + // Convert date to ISO format with time for backend datetime field + const dateWithTime = `${date}T00:00:00`; + const response = await api.get(`/v1/a76/exchange-rate?date=${dateWithTime}`); + + if (response.data && response.data.items && response.data.items.length > 0) { + // Find USD exchange rate + const usdRate = response.data.items.find(rate => rate.foreign_currency === 'USD'); + return usdRate || null; + } + + return null; + } catch (error) { + console.error('Error fetching exchange rate:', error); + return null; + } +} diff --git a/frontend/src/lib/api/dashboard/a76/pedimentos.ts b/frontend/src/lib/api/dashboard/a76/pedimentos.ts index 3e93e440..175c9a79 100644 --- a/frontend/src/lib/api/dashboard/a76/pedimentos.ts +++ b/frontend/src/lib/api/dashboard/a76/pedimentos.ts @@ -59,7 +59,7 @@ export interface Pedimento { pedimento_number?: string | null; client_id?: number | null; operation_type?: number | null; - pedimento_type?: number | null; + pedimento_type?: string | null; pedimento_code?: string | null; regime?: string | null; status?: string | null; @@ -89,7 +89,7 @@ export interface CreatePedimentoData { pedimento_number?: string | null; client_id?: number | null; operation_type?: number | null; - pedimento_type?: number | null; + pedimento_type?: string | null; pedimento_code?: string | null; regime?: string | null; status?: string | null; @@ -111,7 +111,7 @@ export interface UpdatePedimentoData { pedimento_number?: string | null; client_id?: number | null; operation_type?: number | null; - pedimento_type?: number | null; + pedimento_type?: string | null; pedimento_code?: string | null; regime?: string | null; status?: string | null; 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 facb4ad0..2d72efb2 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 @@ -3,6 +3,7 @@ import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; import * as Select from '$lib/components/ui/select'; + import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate'; import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos'; import type { PedimentoCode } from '$lib/api/dashboard/refrence_data/pedimento_codes'; import type { CustomsSection } from '$lib/api/dashboard/refrence_data/customs_sections'; @@ -52,77 +53,96 @@ return null; } + // Opciones constantes + const operationOptions = [ + { value: 1, label: 'Exportación' }, + { value: 2, label: 'Importación' }, + ]; + // Opciones filtradas para Régimen y Tipo de Operación basadas en las selecciones actuales // NOTA: La Clave NO se filtra, siempre muestra todas las opciones - const filteredRegimens = $derived(() => { + const filteredRegimens = $derived.by(() => { + if (!formData) return uniqueRegimens; + // Si hay clave o tipo de operación seleccionado, filtrar - if (formData.pedimento_code || formData.operation_type !== null) { + if (formData.pedimento_code || formData.operation_type !== null && formData.operation_type !== undefined) { + const expectedTypeCode = operationTypeToTypeCode(formData.operation_type); + const matches = codePedimentoRegimens.filter(r => { const matchesCode = !formData.pedimento_code || r.pedimento_code === formData.pedimento_code; - const matchesType = formData.operation_type === null || r.type_code === operationTypeToTypeCode(formData.operation_type); + const matchesType = !expectedTypeCode || r.type_code === expectedTypeCode; return matchesCode && matchesType; }); + const validRegimens = new Set(matches.map(m => m.regimen_code).filter((code): code is string => code !== null)); return Array.from(validRegimens).sort().map(code => ({ code, label: code })); } return uniqueRegimens; }); - const filteredOperationTypes = $derived(() => { - // Si hay clave o régimen seleccionado, filtrar - if (formData.pedimento_code || formData.regime) { - const matches = codePedimentoRegimens.filter(r => { - const matchesCode = !formData.pedimento_code || r.pedimento_code === formData.pedimento_code; - const matchesRegime = !formData.regime || r.regimen_code === formData.regime; - return matchesCode && matchesRegime; - }); + // Determinar si el régimen debe ser seleccionable o readonly + const hasMultipleRegimens = $derived.by(() => { + if (!formData) return false; + return filteredRegimens.length > 1; + }); + + const filteredOperationTypes = $derived.by(() => { + if (!formData) return operationOptions; + // Solo filtrar por la clave del pedimento (no por régimen) + // Esto permite cambiar entre Exportación e Importación libremente + if (formData.pedimento_code) { + const matches = codePedimentoRegimens.filter(r => r.pedimento_code === formData.pedimento_code); const validTypes = new Set(matches.map(m => typeCodeToOperationType(m.type_code)).filter(t => t !== null)); return operationOptions.filter(opt => validTypes.has(opt.value)); } return operationOptions; }); + // Track previous values to detect changes + let previousPedimentoCode = $state(formData?.pedimento_code || ''); + let previousOperationType = $state(formData?.operation_type ?? null); + // Reactive synchronization between Clave, Régimen, and Tipo de Operación // REGLA: La Clave es el campo principal y NUNCA se modifica automáticamente // Solo se auto-llenan Régimen y Tipo de Operación basándose en la Clave // Cuando cambia la Clave del Pedimento $effect(() => { + if (!formData) return; + const currentCode = formData.pedimento_code; + + // Detectar si la clave cambió + const codeChanged = currentCode !== previousPedimentoCode; + + // Solo procesar si la clave cambió + if (!codeChanged) return; + + // Actualizar el tracking + previousPedimentoCode = currentCode; + if (!currentCode) return; const matches = codePedimentoRegimens.filter(r => r.pedimento_code === currentCode); if (matches.length === 0) return; - // Verificar si los valores actuales de régimen y tipo son válidos para esta clave - const currentIsValid = matches.some(m => { - const matchesRegime = !formData.regime || m.regimen_code === formData.regime; - const matchesType = formData.operation_type === null || m.type_code === operationTypeToTypeCode(formData.operation_type); - return matchesRegime && matchesType; - }); - - // Si los valores actuales son válidos, NO auto-llenar - if (currentIsValid && (formData.regime || formData.operation_type !== null)) { - return; + // Auto-llenar con el primer match + const firstMatch = matches[0]; + if (firstMatch.regimen_code) { + formData.regime = firstMatch.regimen_code; } - - // Si solo hay un match y no hay valores válidos, auto-llenar - if (matches.length === 1) { - const match = matches[0]; - if (match.regimen_code && formData.regime !== match.regimen_code) { - formData.regime = match.regimen_code; - } - const expectedOpType = typeCodeToOperationType(match.type_code); - if (expectedOpType !== null && formData.operation_type !== expectedOpType) { - formData.operation_type = expectedOpType; - } + const expectedOpType = typeCodeToOperationType(firstMatch.type_code); + if (expectedOpType !== null) { + formData.operation_type = expectedOpType; } }); // Cuando cambia el Régimen $effect(() => { + if (!formData) return; + const currentRegime = formData.regime; if (!currentRegime) return; @@ -140,31 +160,40 @@ // Cuando cambia el Tipo de Operación $effect(() => { + if (!formData) return; + const currentType = formData.operation_type; + + // Detectar si el tipo de operación cambió + const typeChanged = currentType !== previousOperationType; + previousOperationType = currentType; + + // Si no hay clave seleccionada, no hacer nada + if (!formData.pedimento_code) return; + + // Si el tipo es null/undefined, no hacer nada if (currentType === null || currentType === undefined) return; const expectedTypeCode = operationTypeToTypeCode(currentType); - const matches = codePedimentoRegimens.filter(r => r.type_code === expectedTypeCode); + + // Buscar matches para esta combinación de clave + tipo de operación + const matches = codePedimentoRegimens.filter(r => + r.pedimento_code === formData.pedimento_code && + r.type_code === expectedTypeCode + ); if (matches.length === 0) return; - // Si hay clave seleccionada, actualizar régimen (forzar si no hay match exacto) - if (formData.pedimento_code) { - const exactMatch = matches.find(m => m.pedimento_code === formData.pedimento_code); - if (exactMatch?.regimen_code && formData.regime !== exactMatch.regimen_code) { - formData.regime = exactMatch.regimen_code; - } else if (!exactMatch) { - // No hay match exacto - buscar cualquier match con la clave actual - const allMatchesForClave = codePedimentoRegimens.filter(r => r.pedimento_code === formData.pedimento_code); - if (allMatchesForClave.length > 0) { - // Forzar el régimen al primer match disponible para esta clave - const firstMatch = allMatchesForClave[0]; - if (firstMatch.regimen_code) formData.regime = firstMatch.regimen_code; - } + // Obtener regímenes válidos para esta combinación + const validRegimens = new Set(matches.map(m => m.regimen_code).filter((code): code is string => code !== null)); + + // Si el tipo de operación cambió O el régimen actual no es válido, actualizar el régimen + if (typeChanged || !formData.regime || !validRegimens.has(formData.regime)) { + const firstMatch = matches[0]; + if (firstMatch?.regimen_code) { + formData.regime = firstMatch.regimen_code; } } - // Si hay régimen pero no clave, no hacer nada - // (el usuario debe seleccionar la clave primero) }); // Obtener el año actual (últimos 2 dígitos) @@ -180,7 +209,7 @@ pedimento_number: pedimento?.pedimento_number || '', client_id: pedimento?.client_id ?? null, operation_type: pedimento?.operation_type ?? null, - pedimento_type: pedimento?.pedimento_type ?? null, + pedimento_type: pedimento?.pedimento_type || '', pedimento_code: pedimento?.pedimento_code || '', regime: pedimento?.regime || '', status: pedimento?.status || '', @@ -205,10 +234,24 @@ } }); - const operationOptions = [ - { value: 1, label: 'Exportación' }, - { value: 2, label: 'Importación' }, - ]; + // Obtener automáticamente el tipo de cambio cuando cambie la fecha de entrada + $effect(() => { + if (formData && formData.entry_date) { + console.log('Buscando tipo de cambio para fecha:', formData.entry_date); + + getExchangeRateByDate(formData.entry_date) + .then(usdRate => { + console.log('Tipo de cambio USD encontrado:', usdRate); + if (usdRate && formData) { + formData.exchange_rate = usdRate.value; + console.log('Tipo de cambio actualizado a:', usdRate.value); + } else { + console.warn('No se encontró tipo de cambio USD para la fecha:', formData.entry_date); + } + }) + .catch(err => console.error('Error al obtener tipo de cambio:', err)); + } + }); const statusOptions = [ { value: 'MODIFICABLE', label: 'Modificable' }, @@ -227,6 +270,13 @@ { value: 'CARTA CUPO', label: 'Carta cupo' }, { value: 'ESPERA CANCELAR CARTA CUPO', label: 'Espera cancelar carta cupo' } ]; + + const pedimentoTypeOptions = [ + { value: 'automovil', label: 'Automóvil' }, + { value: 'complementario', label: 'Complementario' }, + { value: 'consolidado', label: 'Consolidado' }, + { value: 'normal', label: 'Normal' } + ]; @@ -241,7 +291,7 @@
- +
- + - {formData.customs_office || 'Sel...'} + {formData.customs_office ? formData.customs_office.substring(0, 2) : 'Sel...'} - {#each customsSections as section} - - - {section.customs_code} - {section.section_name} - - - {/each} + {#if customsSections.length === 0} +
+ No hay aduanas disponibles +
+ {:else} + {#each customsSections as section} + + + {section.customs_code} - {section.section_name} + + + {/each} + {/if}
@@ -286,7 +342,7 @@
- + - {#each customsBrokers as broker} - - - {broker.broker_key} - {broker.name || ''} - - - {/each} + {#if customsBrokers.length === 0} +
+ No hay patentes disponibles +
+ {:else} + {#each customsBrokers as broker} + {@const displayName = broker.name || broker.broker_key || ''} + + + {displayName ? `${displayName} - ${broker.license}` : broker.license} + + + {/each} + {/if}
@@ -314,7 +377,7 @@
- +
- +
@@ -340,7 +406,7 @@
- + o.code === formData.pedimento_code)?.code || 'Sel...'} - + {#each pedimentoCodes as code} - - {code.code} - {code.description} - - - {/each} - - -
- - -
- - formData.regime = v ?? ''} - > - - - {formData.regime || 'Sel...'} - - - - {#each filteredRegimens() as regimen} - - - {regimen.code} - +
+ {code.code} + - + {code.description} +
{/each}
@@ -400,18 +443,54 @@ {operationOptions.find(o => o.value === formData.operation_type)?.label || 'Seleccionar...'} - {#each filteredOperationTypes() as option} + {#each filteredOperationTypes as option} {/each}
+ + +
+ + {#if hasMultipleRegimens} + formData.regime = v ?? ''} + > + + + {formData.regime || 'Sel...'} + + + + {#each filteredRegimens as regimen} + + + {regimen.code} + + + {/each} + + + {:else} + + {/if} +
- + - {#each clients as client} - - - {client.name} - - - {/each} + {#if clients.length === 0} +
+ No hay clientes disponibles +
+ {:else} + {#each clients as client} + + + {client.name} + + + {/each} + {/if}
- - + + formData.pedimento_type = v ?? ''} + > + + {pedimentoTypeOptions.find(o => o.value === formData.pedimento_type)?.label || 'Seleccionar...'} + + + {#each pedimentoTypeOptions as option} + + {/each} + +
@@ -451,7 +544,7 @@
- +