Se integro la ventana de tipo de dato en pedimentos y facturas
This commit is contained in:
@@ -1,27 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import * as AlertDialog from "$lib/components/ui/alert-dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { toast } from "svelte-sonner";
|
||||
import {
|
||||
createExchangeRate,
|
||||
updateExchangeRate,
|
||||
type ExchangeRate
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/exchange-rate";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { Scale, BadgeDollarSign, Info, AlertCircle, ArrowRight } from "lucide-svelte";
|
||||
import { fly, scale } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
onSuccess,
|
||||
overlayClass = "bg-black/80 backdrop-blur-sm",
|
||||
initialDate = ""
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ExchangeRate | null;
|
||||
onSuccess?: () => void;
|
||||
overlayClass?: string;
|
||||
initialDate?: string;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Tipo de Cambio" : "Nuevo Tipo de Cambio");
|
||||
const isMissingRateContext = $derived(!isEdit && initialDate);
|
||||
const title = $derived(
|
||||
isEdit ? "Editar Tipo de Cambio" :
|
||||
isMissingRateContext ? "Tipo de Cambio Requerido" : "Nuevo Tipo de Cambio"
|
||||
);
|
||||
|
||||
let formData = $state({
|
||||
date: '',
|
||||
@@ -32,6 +45,7 @@
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let showConfirmation = $state(false);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
@@ -46,7 +60,7 @@
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
date: initialDate || new Date().toISOString().split('T')[0],
|
||||
value: null,
|
||||
local_currency: 'MXN',
|
||||
foreign_currency: 'USD'
|
||||
@@ -56,16 +70,27 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
function handleSubmit() {
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (formData.value === null) throw new Error('El valor es requerido');
|
||||
|
||||
showConfirmation = true;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al validar';
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (formData.value === null) throw new Error('El valor es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
date: formData.date,
|
||||
value: Number(formData.value),
|
||||
@@ -75,16 +100,18 @@
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateExchangeRate(item.id, dataToSend, companyId);
|
||||
alert(`✅ Tipo de cambio actualizado correctamente`);
|
||||
toast.success('Tipo de cambio actualizado correctamente');
|
||||
} else {
|
||||
await createExchangeRate(dataToSend, companyId);
|
||||
alert(`✅ Tipo de cambio creado correctamente`);
|
||||
toast.success('Tipo de cambio creado correctamente');
|
||||
}
|
||||
|
||||
showConfirmation = false;
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
showConfirmation = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -93,59 +120,121 @@
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] {overlayClass}" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[500px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
<Dialog.Content
|
||||
class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[480px] translate-x-[-50%] translate-y-[-50%] border-0 bg-transparent shadow-2xl p-0 sm:rounded-xl overflow-hidden"
|
||||
transition={scale}
|
||||
params={{ duration: 300, easing: cubicOut, start: 0.95 }}
|
||||
>
|
||||
<div class="bg-background flex flex-col h-full rounded-xl overflow-hidden border border-border">
|
||||
|
||||
{#if isMissingRateContext}
|
||||
<div class="bg-amber-50 dark:bg-amber-950/40 p-5 flex gap-4 border-b border-amber-100 dark:border-amber-900/50">
|
||||
<div class="bg-amber-100 dark:bg-amber-900/60 p-2.5 rounded-full h-fit shadow-sm shrink-0">
|
||||
<AlertCircle class="text-amber-600 dark:text-amber-400" size={24} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-semibold text-amber-900 dark:text-amber-100 text-lg leading-tight">
|
||||
{title}
|
||||
</h3>
|
||||
<p class="text-sm text-amber-800/80 dark:text-amber-200/80 leading-relaxed">
|
||||
Para continuar con el guardado, es necesario registrar el tipo de cambio oficial para esta fecha.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Dialog.Header class="p-6 pb-2">
|
||||
<Dialog.Title class="flex items-center gap-2 text-xl">
|
||||
<BadgeDollarSign class="text-primary" />
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="date" class="text-right">Fecha *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="date" type="date" bind:value={formData.date} disabled={loading} required />
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="p-6 pt-4 space-y-6">
|
||||
{#if error}
|
||||
<div class="rounded-lg bg-destructive/10 border border-destructive/20 p-3 flex gap-3 text-sm text-destructive" transition:fly={{ y: -10 }}>
|
||||
<AlertCircle size={16} class="mt-0.5 shrink-0" />
|
||||
<span class="font-medium">{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-5">
|
||||
<div class="grid gap-2">
|
||||
<Label for="date" class="text-sm font-medium text-muted-foreground ml-1">Fecha Aplicable</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
bind:value={formData.date}
|
||||
disabled={loading || (isMissingRateContext && !!initialDate)}
|
||||
required
|
||||
class="pl-3 h-11 text-base bg-muted/30 focus:bg-background transition-colors"
|
||||
/>
|
||||
{#if isMissingRateContext}
|
||||
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-amber-600 font-medium bg-amber-100 px-2 py-0.5 rounded-full">
|
||||
Requerida
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="value" class="text-sm font-medium text-muted-foreground ml-1">Tipo de Cambio (MXN/USD)</Label>
|
||||
<div class="relative group">
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground font-semibold">$</div>
|
||||
<Input
|
||||
id="value"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
bind:value={formData.value}
|
||||
disabled={loading}
|
||||
required
|
||||
placeholder="0.0000"
|
||||
class="pl-7 h-11 text-lg font-mono tracking-wide focus:ring-2 ring-primary/20 transition-all group-hover:border-primary/50"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<p class="text-[11px] text-muted-foreground text-right px-1">
|
||||
Ej. 24.1234
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="value" type="number" step="0.000001" bind:value={formData.value} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="local_currency" class="text-right">Local</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="local_currency" bind:value={formData.local_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="foreign_currency" class="text-right">Extranjera</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="foreign_currency" bind:value={formData.foreign_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
<Dialog.Footer class="gap-2 sm:gap-0 pt-2">
|
||||
<Button type="button" variant="ghost" onclick={() => open = false} disabled={loading} class="hover:bg-muted/50">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading} class="min-w-[140px] shadow-sm font-medium" size="lg">
|
||||
{#if loading}
|
||||
<span class="animate-spin mr-2">⟳</span> Guardando...
|
||||
{:else}
|
||||
{#if isMissingRateContext}
|
||||
Guardar y Continuar
|
||||
{:else}
|
||||
{isEdit ? 'Actualizar' : 'Crear Registro'}
|
||||
{/if}
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Dialog.Root>
|
||||
|
||||
<AlertDialog.Root bind:open={showConfirmation}>
|
||||
<AlertDialog.Content class="z-[10002]">
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Se {isEdit ? 'actualizará' : 'creará'} el tipo de cambio con valor {formData.value} para el día {formData.date}.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel onclick={() => showConfirmation = false}>Cancelar</AlertDialog.Cancel>
|
||||
<AlertDialog.Action onclick={confirmSubmit}>Confirmar</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let open = $state(false);
|
||||
let checked = $state(false);
|
||||
|
||||
async function checkExchangeRate() {
|
||||
console.log('[ExchangeRateGuard] Checking...', companyStore.activeCompany);
|
||||
if (!companyStore.activeCompany?.id) {
|
||||
console.log('[ExchangeRateGuard] No active company');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use local date instead of UTC
|
||||
const today = new Date().toLocaleDateString('fr-CA'); // YYYY-MM-DD
|
||||
console.log('[ExchangeRateGuard] Date:', today);
|
||||
|
||||
try {
|
||||
const response = await getExchangeRates(companyStore.activeCompany.id, {
|
||||
date: today,
|
||||
page_size: 1
|
||||
});
|
||||
console.log('[ExchangeRateGuard] Response (stringified):', JSON.stringify(response, null, 2));
|
||||
|
||||
// api.get returns { data: ..., status: ... } but types say otherwise
|
||||
const actualResponse = response as any;
|
||||
const items = actualResponse.data?.items || [];
|
||||
|
||||
if (items.length === 0) {
|
||||
console.log('[ExchangeRateGuard] No rate found, opening modal');
|
||||
open = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ExchangeRateGuard] Error checking exchange rate:', error);
|
||||
} finally {
|
||||
checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (companyStore.activeCompany?.id && !checked) {
|
||||
checkExchangeRate();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSuccess() {
|
||||
|
||||
console.log('Exchange rate created successfully via guard');
|
||||
checked = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open
|
||||
onSuccess={handleSuccess}
|
||||
overlayClass="bg-black/20"
|
||||
/>
|
||||
@@ -5,9 +5,11 @@
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { invoicesApi, type Invoice, type CreateInvoiceData, type UpdateInvoiceData } from "$lib/api/dashboard/a76/invoices";
|
||||
import { getExchangeRates } from "$lib/api/dashboard/a76/general_catalogs/exchange-rate";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { LoaderCircle } from 'lucide-svelte';
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
import ExchangeRateDialog from "$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -72,6 +74,9 @@
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let showExchangeRateDialog = $state(false);
|
||||
let missingExchangeRateDate = $state("");
|
||||
|
||||
// Actualizar formData cuando item cambia
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
@@ -181,6 +186,15 @@
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Verificar tipo de cambio antes de guardar
|
||||
if (formData.invoice_date) {
|
||||
const rateExists = await checkExchangeRate(formData.invoice_date);
|
||||
if (!rateExists) {
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let response;
|
||||
if (isEditing && item) {
|
||||
const payload: UpdateInvoiceData = {
|
||||
@@ -287,11 +301,37 @@
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
// Convertir el error a string para buscar mensajes específicos (maneja objetos/arrays de DRF)
|
||||
const errorStr = typeof response.error === 'string'
|
||||
? response.error
|
||||
: JSON.stringify(response.error);
|
||||
|
||||
if (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 (Invoice).");
|
||||
error = null;
|
||||
|
||||
const dateMatch = errorStr.match(/(\d{4}-\d{2}-\d{2})/);
|
||||
missingExchangeRateDate = dateMatch ? dateMatch[0] : (formData.invoice_date || "");
|
||||
|
||||
showExchangeRateDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check exchange rate BEFORE calling API to avoid 400 error
|
||||
if (formData.invoice_date) {
|
||||
const rateExists = await checkExchangeRate(formData.invoice_date);
|
||||
if (!rateExists) {
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Éxito
|
||||
open = false;
|
||||
if (onSuccess) {
|
||||
@@ -305,6 +345,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function checkExchangeRate(date: string): Promise<boolean> {
|
||||
if (!date || !companyStore.activeCompany?.id) return true;
|
||||
|
||||
try {
|
||||
const response = await getExchangeRates(companyStore.activeCompany.id, {
|
||||
date: date,
|
||||
page_size: 1
|
||||
});
|
||||
|
||||
const actualResponse = response as any;
|
||||
const items = actualResponse.data?.items || [];
|
||||
|
||||
// Verificar estrictamente que haya items
|
||||
if (items.length === 0) {
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking exchange rate:', error);
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
resetForm();
|
||||
@@ -679,3 +746,10 @@
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<ExchangeRateDialog
|
||||
bind:open={showExchangeRateDialog}
|
||||
initialDate={missingExchangeRateDate}
|
||||
overlayClass="bg-black/20"
|
||||
onSuccess={() => {/* Optional: maybe refresh something or just let user continue */}}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import type { CodePedimentoRegimen } from '$lib/api/dashboard/refrence_data/code_pedimento_regimens';
|
||||
import IdentificadoresTabForm from './identifiers-tab-form.svelte';
|
||||
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
|
||||
import { Calendar, Clock } from 'lucide-svelte';
|
||||
import {
|
||||
loadServerDate,
|
||||
@@ -45,6 +46,10 @@
|
||||
// Estado para controlar la sección activa de la navegación
|
||||
let activeSection = $state('fechas');
|
||||
|
||||
// Exchange Rate Check State
|
||||
let showExchangeRateDialog = $state(false);
|
||||
let missingExchangeRateDate = $state("");
|
||||
|
||||
// Extraer regímenes únicos de codePedimentoRegimens
|
||||
const uniqueRegimens = $derived(
|
||||
Array.from(new Set(codePedimentoRegimens.map(r => r.regimen_code).filter((code): code is string => code !== null)))
|
||||
@@ -314,6 +319,32 @@
|
||||
{ value: 'adicional', label: 'Adicional' },
|
||||
{ value: 'decrementable', label: 'Decrementable' }
|
||||
];
|
||||
|
||||
export 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) {
|
||||
// Abrir modal preventivamente
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking payment date rate:', error);
|
||||
// Si hay error de red, asumimos que falta para forzar reintento/captura segura
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function openExchangeRateDialog(date: string) {
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
@@ -1159,3 +1190,10 @@
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<ExchangeRateDialog
|
||||
bind:open={showExchangeRateDialog}
|
||||
initialDate={missingExchangeRateDate}
|
||||
overlayClass="bg-black/20"
|
||||
onSuccess={() => {/* Optional: maybe refresh something or just let user continue */}}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import ExchangeRateGuard from "$lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte";
|
||||
|
||||
let { data, children }: { data: LayoutData; children: any } = $props();
|
||||
|
||||
@@ -61,3 +62,5 @@
|
||||
</div>
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
|
||||
<ExchangeRateGuard />
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import { saveInvoice } from '$lib/components/dashboard/invoices/edit/save-invoice';
|
||||
import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate';
|
||||
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
|
||||
|
||||
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
|
||||
let companyStore: any = $state(undefined);
|
||||
@@ -101,6 +103,44 @@
|
||||
let continuationExists = $state(false);
|
||||
|
||||
let calculatedExchangeRate = $state<number | null>(data.invoice?.financials?.exchange_rate ?? null);
|
||||
|
||||
let showExchangeRateDialog = $state(false);
|
||||
let missingExchangeRateDate = $state("");
|
||||
|
||||
async function checkExchangeRate(date: string): Promise<boolean> {
|
||||
if (!date || !companyStore?.activeCompany?.id) return true;
|
||||
|
||||
// Si ya tenemos un tipo de cambio calculado y es válido, asumimos que existe
|
||||
if (calculatedExchangeRate && calculatedExchangeRate > 0) return true;
|
||||
|
||||
try {
|
||||
// Verificar explícitamente si existe
|
||||
const response = await getExchangeRates(companyStore.activeCompany.id, {
|
||||
date: date,
|
||||
page_size: 1
|
||||
});
|
||||
|
||||
const actualResponse = response as any;
|
||||
const items = actualResponse.data?.items || [];
|
||||
|
||||
if (items.length === 0) {
|
||||
console.log('No exchange rate found for', date);
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking exchange rate:', error);
|
||||
// Ante la duda, si es un error de red, quizás deberíamos dejar pasar o bloquear.
|
||||
// Si asumimos que falló la petición, mejor pedir al usuario que verifique.
|
||||
// O podríamos asumir que falta si es 404/Empty.
|
||||
// Por seguridad, abrimos modal.
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Efecto para actualizar el tipo de cambio cuando cambia la fecha de factura
|
||||
$effect(() => {
|
||||
@@ -130,6 +170,22 @@
|
||||
saving = true;
|
||||
|
||||
try {
|
||||
// Validar ID si es edición
|
||||
if (!data.isCreate && !invoiceId) {
|
||||
toast.error("Error interrrno: No se encuentra el ID de la factura para actualizar.");
|
||||
saving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar tipo de cambio antes de guardar
|
||||
if (InvoiceTopFieldsFormData?.invoice_date) {
|
||||
const rateExists = await checkExchangeRate(InvoiceTopFieldsFormData.invoice_date);
|
||||
if (!rateExists) {
|
||||
saving = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await saveInvoice({
|
||||
invoiceId,
|
||||
isCreate: data.isCreate || false,
|
||||
@@ -165,6 +221,18 @@
|
||||
} else {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Error al guardar los cambios';
|
||||
|
||||
// Intercept exchange rate error
|
||||
if (errorMessage.includes('No existe un Tipo de Cambio registrado') || errorMessage.includes('financials.exchange_rate')) {
|
||||
console.log("Interceptor: Exchange rate missing error caught (Invoice Page).");
|
||||
|
||||
const dateMatch = errorMessage.match(/(\d{4}-\d{2}-\d{2})/);
|
||||
const missingDate = dateMatch ? dateMatch[0] : (InvoiceTopFieldsFormData?.invoice_date || "");
|
||||
|
||||
missingExchangeRateDate = missingDate;
|
||||
showExchangeRateDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Si hay errores de validación, mostrarlos en detalle
|
||||
if (e && typeof e === 'object' && 'validationErrors' in e && Array.isArray((e as any).validationErrors)) {
|
||||
const validationErrors = (e as any).validationErrors;
|
||||
@@ -305,6 +373,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Footer fijo en la parte inferior -->
|
||||
<ExchangeRateDialog
|
||||
bind:open={showExchangeRateDialog}
|
||||
initialDate={missingExchangeRateDate}
|
||||
onSuccess={() => {
|
||||
// Actualizar el tipo de cambio mostrado
|
||||
if (InvoiceTopFieldsFormData.invoice_date && companyStore?.activeCompany?.id) {
|
||||
getExchangeRateByDate(InvoiceTopFieldsFormData.invoice_date, companyStore.activeCompany.id)
|
||||
.then(rate => {
|
||||
if (rate) calculatedExchangeRate = rate.value;
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
|
||||
>
|
||||
|
||||
@@ -64,12 +64,41 @@
|
||||
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';
|
||||
|
||||
let { data }: { data: ExtendedPageData } = $props();
|
||||
|
||||
let activeTab = $state('general');
|
||||
let generalTabInstance = $state<any>(null); // Todavía útil para otras cosas
|
||||
let saving = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let success = $state(false);
|
||||
|
||||
// Dialog state lifted up
|
||||
let showExchangeRateDialog = $state(false);
|
||||
let missingExchangeRateDate = $state("");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ID del pedimento
|
||||
let pedimentoId = $state<number | null>(data.pedimentoId ?? null);
|
||||
@@ -274,6 +303,16 @@
|
||||
success = false;
|
||||
|
||||
try {
|
||||
// Verificar tipo de cambio antes de guardar si hay instancia del tab general y hay fecha de pago
|
||||
if (generalTabInstance && generalFormData?.payment_date) {
|
||||
const rateExists = await generalTabInstance.checkPaymentDateRate(generalFormData.payment_date);
|
||||
if (!rateExists) {
|
||||
saving = false;
|
||||
// Asegurar que se muestre el tab general
|
||||
activeTab = 'general';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validar campos requeridos para creación
|
||||
if (data.isCreate && generalFormData) {
|
||||
@@ -718,14 +757,60 @@
|
||||
success = false;
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes('401')) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar los cambios';
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
if (e.message.includes('401')) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
// Extracción segura del error
|
||||
let errorStr = "";
|
||||
try {
|
||||
if (typeof e.message === 'object') {
|
||||
errorStr = JSON.stringify(e.message);
|
||||
} else if (e.message === '[object Object]') {
|
||||
// Si el mensaje es literalmente [object Object], intentamos ver si e tiene otras props
|
||||
errorStr = JSON.stringify(e);
|
||||
} else {
|
||||
errorStr = e.message || String(e);
|
||||
}
|
||||
} catch (jsonErr) {
|
||||
errorStr = String(e);
|
||||
}
|
||||
|
||||
if (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).");
|
||||
error = null;
|
||||
|
||||
const dateMatch = errorStr.match(/(\d{4}-\d{2}-\d{2})/);
|
||||
const missingDate = dateMatch ? dateMatch[0] : (generalFormData?.payment_date || "");
|
||||
|
||||
missingExchangeRateDate = missingDate;
|
||||
showExchangeRateDialog = true;
|
||||
activeTab = 'general';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback normal: intentar mostrar algo legible
|
||||
if (errorStr.includes('{')) {
|
||||
// Si parece JSON, intentar formatearlo un poco o mostrar mensaje genérico
|
||||
try {
|
||||
const errObj = JSON.parse(errorStr);
|
||||
// Si es del formato {"field": ["msg"]}
|
||||
const values = Object.values(errObj).flat();
|
||||
error = values.join(', ');
|
||||
} catch {
|
||||
error = "Error al guardar (ver consola)";
|
||||
}
|
||||
} else {
|
||||
error = errorStr;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error = 'Error al guardar los cambios';
|
||||
}
|
||||
console.error('Error saving all:', e);
|
||||
} finally {
|
||||
saving = false;
|
||||
@@ -794,6 +879,7 @@
|
||||
<div class="pb-56">
|
||||
<Tabs.Content value="general">
|
||||
<GeneralTabForm
|
||||
bind:this={generalTabInstance}
|
||||
pedimento={data.pedimento}
|
||||
bind:formData={generalFormData}
|
||||
bind:identificadoresFormData={identificadoresFormData}
|
||||
|
||||
30
scripts/cleanup_exchange_rate.py
Normal file
30
scripts/cleanup_exchange_rate.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Add backend directory to path
|
||||
sys.path.append('/home/josmar/dev/anexo76/backend')
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.exchange_rate.services import ExchangeRateService
|
||||
from core.database import get_core_db
|
||||
# from api.v1.common.tenant_crud_routes import get_core_db # This might be needing another import path
|
||||
|
||||
db = next(get_core_db())
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
tenant_id = 1
|
||||
company_id = 1
|
||||
|
||||
print(f"Checking for exchange rate on {today}...")
|
||||
filters = {"date": today}
|
||||
rates, total = ExchangeRateService.get_all(db, tenant_id, company_id, filters=filters)
|
||||
|
||||
if total > 0:
|
||||
print(f"Found {total} exchange rate(s) for today. Deleting...")
|
||||
for rate in rates:
|
||||
ExchangeRateService.delete(db, rate.id, tenant_id, company_id)
|
||||
print("Deleted.")
|
||||
else:
|
||||
print("No exchange rate found for today.")
|
||||
|
||||
db.close()
|
||||
Reference in New Issue
Block a user