From bf26268f23e71e73ef02525e5c3184b763e845fc Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Thu, 22 Jan 2026 17:30:04 -0600 Subject: [PATCH 1/5] Se integro la ventana de tipo de dato en pedimentos y facturas --- .../exchange_rate/create-edit-dialog.svelte | 201 +++++++++++++----- .../exchange_rate/exchange-rate-guard.svelte | 60 ++++++ .../invoices/create-edit-dialog.svelte | 74 +++++++ .../pedimentos/edit/general-tab-form.svelte | 38 ++++ frontend/src/routes/dashboard/+layout.svelte | 3 + .../dashboard/invoices/edit/[id]/+page.svelte | 81 +++++++ .../pedimentos/edit/[id]/+page.svelte | 102 ++++++++- scripts/cleanup_exchange_rate.py | 30 +++ 8 files changed, 525 insertions(+), 64 deletions(-) create mode 100644 frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte create mode 100644 scripts/cleanup_exchange_rate.py diff --git a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte index 34c61657..47a47ca8 100644 --- a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte @@ -1,27 +1,40 @@ + + diff --git a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte index 4e0eef19..f9c82247 100644 --- a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte @@ -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(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 { + 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 @@ + + {/* Optional: maybe refresh something or just let user continue */}} +/> 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 99f50cce..774be895 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 @@ -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 { + 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; + } @@ -1159,3 +1190,10 @@ + + {/* Optional: maybe refresh something or just let user continue */}} +/> diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 60b5d95c..ce289bdc 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -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 @@ + + diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index 62065835..f00643cd 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -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(data.invoice?.financials?.exchange_rate ?? null); + + let showExchangeRateDialog = $state(false); + let missingExchangeRateDate = $state(""); + + async function checkExchangeRate(date: string): Promise { + 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 @@ + { + // 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; + }); + } + }} +/>
diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte index 1138594e..5c18aa1a 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte @@ -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(null); // Todavía útil para otras cosas let saving = $state(false); let error = $state(null); let success = $state(false); + + // Dialog state lifted up + let showExchangeRateDialog = $state(false); + let missingExchangeRateDate = $state(""); + + 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; + } + } // ID del pedimento let pedimentoId = $state(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 @@
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() From b8045d73e6557088067a93a7c3ce7b465c1024e2 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 23 Jan 2026 10:42:21 -0600 Subject: [PATCH 2/5] Se integro el sistema de extraer el tipo de cambio --- .../general_catalogs/exchange_rate/routes.py | 53 +++++++++- .../exchange_rate/services.py | 96 +++++++++++++++++++ backend/core/config.py | 7 +- backend/requirements.txt | 1 + docker-compose.yml | 3 + .../a76/general_catalogs/exchange-rate.ts | 22 ++++- .../exchange_rate/create-edit-dialog.svelte | 55 ++++++++++- .../exchange_rate/exchange-rate-guard.svelte | 5 +- .../invoices/create-edit-dialog.svelte | 4 +- 9 files changed, 228 insertions(+), 18 deletions(-) diff --git a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py index 00870adf..22ab8ec9 100644 --- a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py @@ -27,10 +27,25 @@ route_handler = TenantCRUDRoutes( max_page_size=100, ) -router = route_handler.router +crud_router = route_handler.router + +# Create a custom router for specific endpoints that must be matched BEFORE generic CRUD routes +# We use the same prefix so they are grouped together +from fastapi import APIRouter +custom_router = APIRouter(prefix="/exchange-rate", tags=[]) + +@custom_router.get("/test-ping") +async def test_ping(): + return {"message": "pong"} + +# Master router to export +router = APIRouter() +# Include custom routes FIRST to avoid shadowing by /{id} +router.include_router(custom_router) +# router.include_router(crud_router) -@router.get( +@custom_router.get( "/", response_model=Dict[str, Any], summary="List Exchange Rates", @@ -66,3 +81,37 @@ async def list_exchange_rates( "page": page, "page_size": page_size, } + +@custom_router.get( + "/dof-search", + response_model=Dict[str, Any], + summary="Fetch Exchange Rate from DOF", + description="Fetches the exchange rate from the Official Journal of the Federation (DOF) for a specific date.", +) +async def fetch_exchange_rate_dof( + date: str = Query(..., description="Date in YYYY-MM-DD format"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + # This endpoint can be public or protected. Assuming protected for now. + # No specific tenant validation needed since it's an external query, + # but good to ensure user is authenticated. + + try: + print(f"DEBUG: Route called with date={date}") + rate = ExchangeRateService.fetch_from_dof(date) + + if rate is None: + return {"success": False, "message": "No se encontró el tipo de cambio en el DOF para la fecha especificada o el servicio no está disponible.", "value": None} + + return {"success": True, "value": rate} + except Exception as e: + print(f"DEBUG: Error in route: {e}") + import traceback + traceback.print_exc() + return {"success": False, "message": f"Error interno: {str(e)}", "value": None} + +# Include routers at the end to ensure all routes are registered +# Include custom routes FIRST to avoid shadowing by /{id} of crud_router +router.include_router(custom_router) +router.include_router(crud_router) diff --git a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py index f7749b43..56e550fc 100644 --- a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py +++ b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py @@ -1,10 +1,18 @@ from typing import Optional, Tuple, List, Dict, Any from datetime import datetime, time +import requests +import re from sqlalchemy.orm import Session from sqlalchemy import cast, Date from . import dto, models +import urllib3 +from core.config import settings + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + class ExchangeRateService: @@ -135,3 +143,91 @@ class ExchangeRateService: db.delete(exchange_rate) db.commit() return True + + @staticmethod + def _get_external_api_token(base_url, username, password) -> Optional[str]: + """Helper to get authentication token from external API""" + try: + login_url = f"{base_url}/auth/login" + payload = {"username": username, "password": password} + headers = {"Content-Type": "application/json"} + + response = requests.post(login_url, json=payload, headers=headers, timeout=5) + if response.status_code not in [200, 201]: + print(f"External API Login Failed: {response.status_code} - {response.text}") + return None + + data = response.json() + return data.get("token") or data.get("access_token") + except Exception as e: + print(f"External API Login Error: {e}") + return None + + @staticmethod + def fetch_from_dof(date_str: str) -> Optional[float]: + """ + Fetches the exchange rate from an external API (replacing direct DOF scraping). + The API handles date logic (holidays, weekends) automatically. + + Args: + date_str (str): Date in 'YYYY-MM-DD' format. + + Returns: + Optional[float]: The exchange rate value if found, None otherwise. + """ + # API Credentials + API_BASE_URL = settings.EXTERNAL_API_URL + API_USER = settings.EXTERNAL_API_USER + API_PASS = settings.EXTERNAL_API_PASSWORD + + if not API_USER or not API_PASS: + print("ERROR: External API credentials not properly configured in settings") + return None + + try: + print(f"DEBUG: Fetching External API for date: {date_str}") + + # 1. Get Token + token = ExchangeRateService._get_external_api_token(API_BASE_URL, API_USER, API_PASS) + if not token: + print("Failed to obtain external API token") + return None + + # 2. Fetch Exchange Rate + # The API endpoint is /tipoCambio/{YYYY-MM-DD} + tc_endpoint = f"{API_BASE_URL}/tipoCambio/{date_str}" + + # Auth header: The API expects just the token string in common usage, but we try standard first + # based on user feedback/code: 'Authorization:' . $token + headers = { + "Authorization": token, + "Content-Type": "application/json" + } + + response = requests.get(tc_endpoint, headers=headers, timeout=5) + + # Retry logic as per PHP reference (if 401, maybe formatting issue, but requests handles headers well) + if response.status_code == 401: + # Try with Bearer prefix just in case, though PHP code suggested raw token + print("DEBUG: 401 received, retrying with Bearer prefix...") + headers["Authorization"] = f"Bearer {token}" + response = requests.get(tc_endpoint, headers=headers, timeout=5) + + if response.status_code != 200: + print(f"External API TC Error: {response.status_code} - {response.text}") + return None + + data = response.json() + # Expected response: {"Id":..., "Fecha":"...", "TipoCambio":17.452, "Mov":"..."} + + if "TipoCambio" in data: + val = float(data["TipoCambio"]) + print(f"DEBUG: External API returned value: {val}") + return val + + print(f"DEBUG: 'TipoCambio' key not found in response: {data}") + return None + + except Exception as e: + print(f"Error fetching from External API: {e}") + return None diff --git a/backend/core/config.py b/backend/core/config.py index c27954eb..54301641 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -42,8 +42,13 @@ class Settings(BaseSettings): # License LICENSE_CHECK_ENABLED: bool = True + # External APIs + EXTERNAL_API_URL: str = "http://74.208.80.245:3000" + EXTERNAL_API_USER: str = "" + EXTERNAL_API_PASSWORD: str = "" + model_config = SettingsConfigDict( - env_file=".env", case_sensitive=True, extra="ignore", env_file_encoding="utf-8" + env_file=[".env", "../.env"], case_sensitive=True, extra="ignore", env_file_encoding="utf-8" ) @property diff --git a/backend/requirements.txt b/backend/requirements.txt index ff7a7cfa..00ebff8e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,6 +21,7 @@ passlib[bcrypt]==1.7.4 httpx==0.28.1 requests==2.32.5 + # Utilities python-multipart==0.0.20 python-dotenv==1.1.1 diff --git a/docker-compose.yml b/docker-compose.yml index c26575a5..ae4cb27c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -175,6 +175,9 @@ services: - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000} + - EXTERNAL_API_URL=${EXTERNAL_API_URL} + - EXTERNAL_API_USER=${EXTERNAL_API_USER} + - EXTERNAL_API_PASSWORD=${EXTERNAL_API_PASSWORD} ports: - "8000:8000" depends_on: diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/exchange-rate.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/exchange-rate.ts index b8b94a40..25a24b2f 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/exchange-rate.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/exchange-rate.ts @@ -37,10 +37,12 @@ export interface ExchangeRateFilters { page_size?: number; } +import type { ApiResponse } from '$lib/api'; + export async function getExchangeRates( companyId: number, filters?: ExchangeRateFilters -): Promise { +): Promise> { const params = new URLSearchParams({ company_id: companyId.toString() }); if (filters) { @@ -57,7 +59,7 @@ export async function getExchangeRates( export async function getExchangeRate( exchangeRateId: number, companyId: number -): Promise { +): Promise> { const params = new URLSearchParams({ company_id: companyId.toString() }); return api.get(`/v1/a76/exchange-rate/${exchangeRateId}?${params.toString()}`); } @@ -65,7 +67,7 @@ export async function getExchangeRate( export async function createExchangeRate( data: ExchangeRateCreate, companyId: number -): Promise { +): Promise> { const params = new URLSearchParams({ company_id: companyId.toString() }); return api.post(`/v1/a76/exchange-rate/?${params.toString()}`, data); } @@ -74,7 +76,7 @@ export async function updateExchangeRate( exchangeRateId: number, data: ExchangeRateUpdate, companyId: number -): Promise { +): Promise> { const params = new URLSearchParams({ company_id: companyId.toString() }); return api.put( `/v1/a76/exchange-rate/${exchangeRateId}?${params.toString()}`, @@ -85,7 +87,17 @@ export async function updateExchangeRate( export async function deleteExchangeRate( exchangeRateId: number, companyId: number -): Promise { +): Promise> { const params = new URLSearchParams({ company_id: companyId.toString() }); return api.delete(`/v1/a76/exchange-rate/${exchangeRateId}?${params.toString()}`); } + +export interface DofResponse { + success: boolean; + message?: string; + value?: number | null; +} + +export async function getDofExchangeRate(date: string): Promise> { + return api.get(`/v1/a76/exchange-rate/dof-search?date=${date}`); +} diff --git a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte index 47a47ca8..2b32120d 100644 --- a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte @@ -8,10 +8,11 @@ import { createExchangeRate, updateExchangeRate, - type ExchangeRate + type ExchangeRate, + getDofExchangeRate } 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 { Scale, BadgeDollarSign, Info, AlertCircle, ArrowRight, CloudDownload } from "lucide-svelte"; import { fly, scale } from 'svelte/transition'; import { cubicOut } from 'svelte/easing'; @@ -44,6 +45,7 @@ }); let loading = $state(false); + let scraping = $state(false); let error = $state(null); let showConfirmation = $state(false); @@ -65,11 +67,40 @@ local_currency: 'MXN', foreign_currency: 'USD' }; + + // Si es modo contexto (falta dato) y tenemos fecha, intentar cargar automáticamente del DOF + if (initialDate && !item) { + // Opcional: Auto-consultar + // fetchFromDof(initialDate); + } } error = null; } }); + async function fetchFromDof(date: string) { + if (!date) return; + scraping = true; + error = null; + try { + const response = await getDofExchangeRate(date); + // The API returns an ApiResponse object, so we need to access response.data + // response.data contains { success: boolean, value: number, message: string } + if (response.data?.success && response.data?.value) { + formData.value = response.data.value; + toast.success(`Tipo de cambio obtenido del DOF: ${response.data.value}`); + } else { + toast.error(response.data?.message || response.error || 'No se pudo obtener el dato del DOF'); + // No bloquear, permitir manual + } + } catch (e) { + console.error(e); + toast.error('Error al consultar el servicio del DOF'); + } finally { + scraping = false; + } + } + function handleSubmit() { error = null; try { @@ -124,8 +155,6 @@
@@ -181,7 +210,23 @@
- +
+ + +
$
Date: Fri, 23 Jan 2026 10:48:45 -0600 Subject: [PATCH 3/5] Se habilito buscar por fecha del usuario --- .../dashboard/exchange_rate/create-edit-dialog.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte index 2b32120d..2e23c655 100644 --- a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte @@ -197,7 +197,7 @@ id="date" type="date" bind:value={formData.date} - disabled={loading || (isMissingRateContext && !!initialDate)} + disabled={loading} required class="pl-3 h-11 text-base bg-muted/30 focus:bg-background transition-colors" /> From 83f2a0fa2ec0669677b0a16e7124a6ab171c9e50 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 23 Jan 2026 11:16:29 -0600 Subject: [PATCH 4/5] Se agregaron botones similar a SCAII --- .../exchange_rate/create-edit-dialog.svelte | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte index 2e23c655..f85eec38 100644 --- a/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/exchange_rate/create-edit-dialog.svelte @@ -247,19 +247,21 @@
- - + + - From f38d9a3fbc65c9fd42ee2a138f9cdd51a9fbbd6c Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 23 Jan 2026 11:34:20 -0600 Subject: [PATCH 5/5] Eliminacion de script de pruebas --- scripts/cleanup_exchange_rate.py | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 scripts/cleanup_exchange_rate.py diff --git a/scripts/cleanup_exchange_rate.py b/scripts/cleanup_exchange_rate.py deleted file mode 100644 index 276d364e..00000000 --- a/scripts/cleanup_exchange_rate.py +++ /dev/null @@ -1,30 +0,0 @@ -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()