v1.0.11 - Plugin BCV Exchange Rate para iDempiere v10
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
# Compiled class files
|
||||
*.class
|
||||
|
||||
# Package files
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
dist/
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
*.iml
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
@@ -0,0 +1,15 @@
|
||||
Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: BCV Exchange Rate Plugin
|
||||
Bundle-SymbolicName: com.venezuela.bcvrate;singleton:=true
|
||||
Bundle-Version: 1.0.11
|
||||
Bundle-Vendor: Ezerpa
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-11
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-Activator: com.venezuela.bcvrate.Activator
|
||||
Require-Bundle: org.adempiere.base;bundle-version="10.0.0",
|
||||
json;bundle-version="20190722.0.0"
|
||||
Import-Package: org.osgi.framework
|
||||
Export-Package: com.venezuela.bcvrate.process,
|
||||
com.venezuela.bcvrate.service,
|
||||
com.venezuela.bcvrate.factory
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
|
||||
name="com.venezuela.bcvrate.factory.BCVProcessFactory"
|
||||
immediate="true">
|
||||
<implementation class="com.venezuela.bcvrate.factory.BCVProcessFactory"/>
|
||||
<service>
|
||||
<provide interface="org.adempiere.base.IProcessFactory"/>
|
||||
</service>
|
||||
<property name="service.ranking" type="Integer" value="100"/>
|
||||
<property name="component.name" type="String" value="com.venezuela.bcvrate.factory.BCVProcessFactory"/>
|
||||
</scr:component>
|
||||
@@ -0,0 +1,322 @@
|
||||
# BCV Exchange Rate Plugin for iDempiere v10
|
||||
|
||||
## Descripción
|
||||
|
||||
Plugin OSGi que obtiene automáticamente la tasa de cambio oficial del BCV (Banco Central de Venezuela) USD/VES y la registra en la tabla C_Conversion_Rate de iDempiere.
|
||||
|
||||
## Características
|
||||
|
||||
- **Doble fuente de datos**: API bcv.today + scraping directo del sitio web del BCV
|
||||
- **Fallback automático**: Si la API no tiene datos recientes, scrapea el sitio del BCV
|
||||
- **Busca hacia atrás** cuando el BCV no publica tasa para un día hábil
|
||||
- **Rellena automáticamente** días sin publicación (fines de semana, feriados)
|
||||
- **Ejecución manual** con rango de fechas específico
|
||||
- **Ejecución automática** que detecta días pendientes y los rellena
|
||||
- **Valida duplicados** antes de insertar
|
||||
- **Modo simulación** para pruebas sin guardar
|
||||
- **Scheduler automático** diario
|
||||
|
||||
## Fuentes de Datos
|
||||
|
||||
### Fuente Principal: API bcv.today
|
||||
- **Endpoint**: `https://bcv.today/api/v1/rate.json`
|
||||
- **Endpoint por fecha**: `https://bcv.today/api/v1/history/YYYY-MM-DD.json`
|
||||
- **Ventaja**: Rápida, estructurada
|
||||
- **Desventaja**: Puede tener retraso deactualización
|
||||
|
||||
### Fuente de Respaldo: Scraping BCV
|
||||
- **URL**: `https://www.bcv.org.ve/`
|
||||
- **Método**: Extracción directa del HTML con regex
|
||||
- **Ventaja**: Siempre actualizada (datos oficiales)
|
||||
- **Desventaja**: Requiere SSL bypass y parsing de HTML
|
||||
|
||||
### Flujo de Selección
|
||||
```
|
||||
1. Intentar API bcv.today (búsqueda hacia adelante 1-10 días)
|
||||
↓ (si no encuentra)
|
||||
2. Scraping directo del sitio web del BCV
|
||||
↓ (si falla)
|
||||
3. API bcv.today getCurrentRate() (último recurso)
|
||||
```
|
||||
|
||||
## Comportamiento del Proceso
|
||||
|
||||
### Ejecución Manual (con parámetros de fecha)
|
||||
- Procesa cada día en el rango DateFrom → DateTo
|
||||
- Para cada día, busca la última tasa BCV hacia atrás (hasta 10 días)
|
||||
- Usa la tasa encontrada pero asigna ValidFrom = fecha solicitada
|
||||
|
||||
### Ejecución Automática (sin parámetros de fecha)
|
||||
1. Obtiene la última tasa BCV publicada (API + scraping)
|
||||
2. Compara con la última tasa en BD
|
||||
3. Si hay tasa nueva no registrada, procesa desde la última en BD hasta esa fecha
|
||||
4. Si ya están todas → "No hay días pendientes"
|
||||
|
||||
### Lógica de Fallback por Día
|
||||
| Situación | Acción |
|
||||
|-----------|--------|
|
||||
| BCV publica tasa para la fecha | Usa esa tasa, ValidFrom = fecha solicitada |
|
||||
| BCV no publica (feriado, fin de semana) | Busca hacia atrás, usa última tasa disponible |
|
||||
| No se encuentra tasa en 10 días | Registra error |
|
||||
|
||||
### Ejemplo
|
||||
| Fecha solicitada | BCV publica | Tasa usada | ValidFrom |
|
||||
|------------------|-------------|------------|-----------|
|
||||
| 24/06 (miércoles) | No | 617.6388 (del 23/06) | 24/06 |
|
||||
| 25/06 (jueves) | Sí (621.5299) | 621.5299 | 25/06 |
|
||||
| 27/06 (sábado) | No | tasa del 26/06 | 27/06 |
|
||||
| 28/06 (domingo) | No | tasa del 26/06 | 28/06 |
|
||||
|
||||
## Archivos del Plugin
|
||||
|
||||
```
|
||||
com.venezuela.bcvrate/
|
||||
├ META-INF/
|
||||
│ └ MANIFEST.MF # Manifest OSGi (Bundle-Activator)
|
||||
├ src/com/venezuela/bcvrate/
|
||||
│ ├ Activator.java # Registra IProcessFactory via BundleContext
|
||||
│ ├ factory/
|
||||
│ │ └ BCVProcessFactory.java # Factory del proceso
|
||||
│ ├ process/
|
||||
│ │ └ BCVExchangeRateProcess.java # Lógica principal del proceso
|
||||
│ └ service/
|
||||
│ ├ BCVApiService.java # Cliente HTTP + scraping BCV
|
||||
│ └ BCVRateResponse.java # Modelo de respuesta
|
||||
├ dist/
|
||||
│ └ com.venezuela.bcvrate.jar # JAR compilado listo para instalar
|
||||
├ lib/
|
||||
│ └ org.osgi.framework.jar # Dependencia OSGi
|
||||
├ migration/
|
||||
│ ├ install_v10.sql # SQL para registrar proceso
|
||||
│ └ BCV_ExchangeRateProcess_2Pack.xml # 2pack para despliegue
|
||||
└ build.sh # Script de build para Linux
|
||||
```
|
||||
|
||||
## Instalación
|
||||
|
||||
### Prerrequisitos
|
||||
- iDempiere v10 ejecutándose
|
||||
- Acceso a consola Felix (http://servidor:8080/system/console)
|
||||
- Acceso a la base de datos PostgreSQL
|
||||
|
||||
### Paso 1: Instalar JAR en Felix
|
||||
1. Abrir consola Felix → **Bundles**
|
||||
2. Click **Install/Update Bundle**
|
||||
3. Seleccionar `com.venezuela.bcvrate.jar`
|
||||
4. Seleccionar **Start**
|
||||
5. Verificar estado **Active**
|
||||
|
||||
**Importante**: Si actualizas el plugin, debes **uninstall** el bundle anterior primero, luego **install** el nuevo. Equinox no recarga si la versión es la misma.
|
||||
|
||||
### Paso 2: Registrar Proceso
|
||||
Opción A: Ejecutar `migration/install_v10.sql` en la BD
|
||||
Opción B: Crear manualmente via UI:
|
||||
- **Proceso**: BCV Exchange Rate Update
|
||||
- **ClassName**: com.venezuela.bcvrate.process.BCVExchangeRateProcess
|
||||
- **AccessLevel**: 3 (Client + Organization)
|
||||
- **Parámetros**: Client, Conversion Type, Date From, Date To, Simulation Mode
|
||||
|
||||
Opción C: Importar 2pack `migration/BCV_ExchangeRateProcess_2Pack.xml`
|
||||
|
||||
### Paso 3: Agregar al Menú
|
||||
Crear registro en AD_TreeBar o importar 2pack con el menú configurado.
|
||||
|
||||
### Paso 4: Configurar Scheduler (Agenda)
|
||||
1. Ir a **Agenda** (Process Scheduler)
|
||||
2. Crear nuevo registro
|
||||
3. Seleccionar proceso: **BCV Exchange Rate Update**
|
||||
4. Frecuencia: **1 dia**
|
||||
5. Hora: **17:00-18:00** (cuando BCV publica la tasa)
|
||||
6. Dejar parámetros de fecha **vacíos**
|
||||
|
||||
**Recomendación**: Configurar dos ejecuciones (17:00 y 21:00) por si la primera no captura la tasa.
|
||||
|
||||
## Parámetros del Proceso
|
||||
|
||||
| Parámetro | Tipo | Requerido | Default | Descripción |
|
||||
|-----------|------|-----------|---------|-------------|
|
||||
| Grupo Empresarial | List | Sí | @#AD_Client_ID@ | Cliente de iDempiere |
|
||||
| Tipo de Conversión | List | Sí | BCV | Tipo de conversión |
|
||||
| Fecha desde | Date | No | (vacío) | Fecha inicio (vacío = auto) |
|
||||
| Fecha hasta | Date | No | (vacío) | Fecha fin (vacío = auto) |
|
||||
| Simulación | Yes/No | No | N | Ejecutar sin guardar cambios |
|
||||
|
||||
## Datos Clave
|
||||
|
||||
| Campo | Valor | Descripción |
|
||||
|-------|-------|-------------|
|
||||
| C_Currency_ID | 100 | USD (hardcoded) |
|
||||
| C_Currency_ID_To | (dinámico) | Moneda funcional del cliente |
|
||||
| AD_Org_ID | 0 | Todas las organizaciones |
|
||||
| MultiplyRate | (API/scraping) | Tasa USD → VES (4 decimales) |
|
||||
| DivideRate | 1/MultiplyRate | Tasa VES → USD (10 decimales) |
|
||||
| ValidFrom | (fecha solicitada) | Fecha que necesita la empresa |
|
||||
| ValidTo | (fecha solicitada) | Misma que ValidFrom |
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### Error "Failed to create new process instance"
|
||||
- Verificar que el bundle está **Active** en Felix
|
||||
- Verificar logs: `grep -i bcvrate /opt/idempiere-server/logs/idempiere.log`
|
||||
|
||||
### Error "Cross tenant PO writing"
|
||||
- Se resolvió usando `set_ValueNoCheck()` en vez de `setMultiplyRate()`
|
||||
- El proceso crea el registro con el client ID correcto del contexto
|
||||
|
||||
### Error "No se pudo obtener la moneda funcional"
|
||||
- Verificar que el cliente tiene un esquema contable configurado
|
||||
- Verificar que la moneda funcional no sea USD
|
||||
|
||||
### API bcv.today no responde o tiene retraso
|
||||
- Verificar conectividad: `curl https://bcv.today/api/v1/rate.json`
|
||||
- El plugin automáticamente intenta scraping del sitio web del BCV como respaldo
|
||||
|
||||
### Error SSL en scraping del BCV
|
||||
- El plugin incluye SSL bypass para el sitio web del BCV
|
||||
- Si persiste, verificar que el bundle tiene la última versión
|
||||
|
||||
### Tasa con decimales incorrectos
|
||||
- Se resolvió usando `set_ValueNoCheck()` en vez de setters de MConversionRate
|
||||
- MultiplyRate: 4 decimales
|
||||
- DivideRate: 10 decimales
|
||||
|
||||
### Scraping captura tasa incorrecta (EUR en vez de USD)
|
||||
- El regex busca específicamente después de `USD</span>` en el HTML
|
||||
- Verificar que el HTML del BCV no ha cambiado su estructura
|
||||
|
||||
## Compilación
|
||||
|
||||
### En Windows (desarrollo)
|
||||
```powershell
|
||||
# Requiere: JDK 11, JARs de iDempiere v10
|
||||
$javac = "C:\Program Files\Eclipse Adoptium\jdk-11.0.31.11-hotspot\bin\javac.exe"
|
||||
$v10Plugins = "path/to/idempiere-v10/plugins"
|
||||
|
||||
$sources = Get-ChildItem -Path src -Recurse -Filter "*.java"
|
||||
$classpath = "$v10Plugins\org.adempiere.base_*.jar;$v10Plugins\json_*.jar;lib\org.osgi.framework.jar"
|
||||
|
||||
& $javac -cp $classpath -d dist -source 11 -target 11 @($sources)
|
||||
& jar cfm dist/com.venezuela.bcvrate.jar META-INF/MANIFEST.MF -C dist .
|
||||
```
|
||||
|
||||
### En Linux (servidor)
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **org.adempiere.base** >= 10.0.0 - Framework iDempiere
|
||||
- **json** >= 20190722.0.0 - Parsing JSON (solo para API bcv.today)
|
||||
- **org.osgi.framework** - Framework OSGi (sistema)
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Dual Source: API + Scraping
|
||||
El plugin implementa dos fuentes de datos para máxima confiabilidad:
|
||||
|
||||
1. **API bcv.today**: Consulta REST rápida, pero puede tener retraso deactualización
|
||||
2. **Scraping BCV**: Extracción directa del HTML del sitio web oficial
|
||||
|
||||
El flujo automático intenta la API primero y fallback al scraping si no hay datos recientes.
|
||||
|
||||
### SSL Bypass para Scraping
|
||||
El sitio web del BCV usa certificados SSL que pueden no estar en el cacerts de Java. El plugin implementa SSL bypass por-conexión (no global) para evitar conflictos con iDempiere:
|
||||
|
||||
```java
|
||||
TrustManager[] trustAllCerts = new TrustManager[]{...};
|
||||
SSLContext sc = SSLContext.getInstance("TLS");
|
||||
sc.init(null, trustAllCerts, new SecureRandom());
|
||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
||||
conn.setSSLSocketFactory(sc.getSocketFactory());
|
||||
conn.setHostnameVerifier((hostname, session) -> true);
|
||||
```
|
||||
|
||||
### Extracción de Tasa USD del HTML
|
||||
El regex busca específicamente la tasa del dólar después del elemento `USD</span>`:
|
||||
|
||||
```
|
||||
USD</span>.*?strong-tb">\s*([\d.,]+)\s*</strong>
|
||||
```
|
||||
|
||||
Esto evita capturar otras tasas (EUR, CNY, etc.) que aparecen en la misma página.
|
||||
|
||||
### Por qué se usa BundleActivator en vez de DS
|
||||
- `component.xml` (Declarative Services) no funcionó en iDempiere v10
|
||||
- `@Component` annotation tampoco funcionó
|
||||
- `BundleActivator` con `BundleContext.registerService()` es el más confiable
|
||||
- Requiere `Import-Package: org.osgi.framework` en MANIFEST.MF
|
||||
|
||||
### Por qué se usa regex en vez del JSON library
|
||||
- El `json_20190722.0.0.jar` bundled con iDempiere v10 tiene bugs de precisión
|
||||
- `getDouble("USD")` y `get("USD").toString()` corrompen decimales
|
||||
- Se extrae el valor directamente del string raw con regex: `"USD"\s*:\s*([0-9.]+)`
|
||||
|
||||
### Por qué se usa set_ValueNoCheck en vez de setters
|
||||
- `MConversionRate.setMultiplyRate()` modifica el valor internamente
|
||||
- `MConversionRate.setDivideRate()` redondea a 4 decimales
|
||||
- `set_ValueNoCheck()` bypassa la lógica interna y almacena el valor exacto
|
||||
|
||||
### Cross Tenant
|
||||
- `MConversionRate.setRate()` intenta guardar con AD_Client_ID=0
|
||||
- Se crea el registro directamente con `MConversionRate` y se usa `set_ValueNoCheck("AD_Client_ID", ...)`
|
||||
|
||||
### Por qué el JAR debe cambiar de versión
|
||||
- Equinox (runtime OSGi) cachea bundles por versión
|
||||
- Si la versión no cambia, el bundle anterior permanece activo
|
||||
- Siempre incrementar `Bundle-Version` en MANIFEST.MF al actualizar
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
### v1.0.11 (2026-07-03)
|
||||
- Fix regex scraping: captura tasa USD específicamente (no EUR)
|
||||
- Regex busca después de `USD</span>` en el HTML
|
||||
|
||||
### v1.0.10 (2026-07-03)
|
||||
- Logging mejorado en proceso para ver fuente de datos
|
||||
- Scraping status visible en output del proceso
|
||||
|
||||
### v1.0.9 (2026-07-03)
|
||||
- SSL bypass por-conexión (no global) para scraping BCV
|
||||
- Logging detallado en getLatestRate()
|
||||
|
||||
### v1.0.8 (2026-07-03)
|
||||
- Scraping directo del sitio web del BCV como fuente de respaldo
|
||||
- Dual source: API bcv.today + scraping bcv.org.ve
|
||||
- Método getRateFromBCVWebsite() con extracción regex del HTML
|
||||
|
||||
### v1.0.7 (2026-06-23)
|
||||
- Lógica automática: detecta días pendientes entre BD y última tasa BCV
|
||||
- Ejecución sin parámetros: busca última tasa BCV y rellena huecos
|
||||
|
||||
### v1.0.6 (2026-06-23)
|
||||
- Fix: ejecución automática ahora detecta si hay tasa para hoy
|
||||
- Evita procesar si ya existen todas las tasas
|
||||
|
||||
### v1.0.5 (2026-06-23)
|
||||
- Lógica de fallback: busca hacia atrás cuando BCV no publica tasa
|
||||
- Rellena días sin publicación con última tasa disponible
|
||||
|
||||
### v1.0.4 (2026-06-23)
|
||||
- getRateForDateWithFallback(): busca hacia atrás hasta 10 días
|
||||
- BCVRateResponse: campo requestedDate vs effectiveDate
|
||||
|
||||
### v1.0.3 (2026-06-23)
|
||||
- DivideRate: 10 decimales en vez de 4
|
||||
|
||||
### v1.0.2 (2026-06-23)
|
||||
- Todos los setters usan set_ValueNoCheck() para bypass MConversionRate
|
||||
- Log de verificación post-save
|
||||
|
||||
### v1.0.1 (2026-06-23)
|
||||
- Regex para extraer USD directo del string raw (bypass JSON library)
|
||||
|
||||
### v1.0.0 (2026-06-22)
|
||||
- Versión inicial
|
||||
- API: bcv.today
|
||||
- BundleActivator para registro de IProcessFactory
|
||||
|
||||
## Licencia
|
||||
|
||||
Plugin personalizado para uso interno de Ezerpa.
|
||||
@@ -0,0 +1,5 @@
|
||||
source.. = src/
|
||||
output.. = bin/
|
||||
bin.includes = META-INF/,\
|
||||
OSGI-INF/,\
|
||||
.
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# Build script for BCV Exchange Rate Plugin - iDempiere v10
|
||||
# Run this on the server where iDempiere v10 is installed
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
IDEMPIERE_HOME="${IDEMPIERE_HOME:-/opt/idempiere-server}"
|
||||
PLUGIN_DIR="$(dirname "$0")"
|
||||
DIST_DIR="$PLUGIN_DIR/dist"
|
||||
BUILD_DIR="$PLUGIN_DIR/build"
|
||||
|
||||
echo "=== BCV Exchange Rate Plugin Build ==="
|
||||
echo "iDempiere Home: $IDEMPIERE_HOME"
|
||||
|
||||
# Find required JARs
|
||||
BASE_JAR=$(find "$IDEMPIERE_HOME/plugins" -name "org.adempiere.base_*.jar" | head -1)
|
||||
JSON_JAR=$(find "$IDEMPIERE_HOME/plugins" -name "json_*.jar" | head -1)
|
||||
OSGI_JAR=$(find "$IDEMPIERE_HOME/plugins" -name "org.osgi.framework_*.jar" | head -1)
|
||||
COMPONENT_ANNOTATIONS_JAR=$(find "$IDEMPIERE_HOME/plugins" -name "org.osgi.service.component.annotations_*.jar" | head -1)
|
||||
|
||||
# Check required JARs
|
||||
if [ -z "$BASE_JAR" ]; then
|
||||
echo "ERROR: org.adempiere.base JAR not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JSON_JAR" ]; then
|
||||
echo "ERROR: json JAR not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If OSGi framework JAR not found, try to use the annotations from the base JAR
|
||||
if [ -z "$OSGI_JAR" ]; then
|
||||
echo "WARNING: org.osgi.framework JAR not found, checking if it's in base JAR"
|
||||
fi
|
||||
|
||||
echo "Base JAR: $BASE_JAR"
|
||||
echo "JSON JAR: $JSON_JAR"
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$DIST_DIR"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# Find Java
|
||||
JAVA_HOME="${JAVA_HOME:-$(dirname $(dirname $(readlink -f $(which java))))}"
|
||||
JAVAC="$JAVA_HOME/bin/javac"
|
||||
JAR="$JAVA_HOME/bin/jar"
|
||||
|
||||
if [ ! -f "$JAVAC" ]; then
|
||||
echo "ERROR: javac not found. Please set JAVA_HOME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using javac: $JAVAC"
|
||||
|
||||
# Build classpath
|
||||
CLASSPATH="$BASE_JAR:$JSON_JAR"
|
||||
if [ -n "$OSGI_JAR" ]; then
|
||||
CLASSPATH="$CLASSPATH:$OSGI_JAR"
|
||||
fi
|
||||
if [ -n "$COMPONENT_ANNOTATIONS_JAR" ]; then
|
||||
CLASSPATH="$CLASSPATH:$COMPONENT_ANNOTATIONS_JAR"
|
||||
fi
|
||||
|
||||
# Find all Java source files
|
||||
SOURCES=$(find "$PLUGIN_DIR/src" -name "*.java")
|
||||
SOURCE_COUNT=$(echo "$SOURCES" | wc -l)
|
||||
echo "Found $SOURCE_COUNT source files"
|
||||
|
||||
# Compile
|
||||
echo "Compiling..."
|
||||
$JAVAC -cp "$CLASSPATH" \
|
||||
-d "$BUILD_DIR" \
|
||||
-source 11 -target 11 \
|
||||
$SOURCES
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Compilation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Compilation successful"
|
||||
|
||||
# Create JAR
|
||||
echo "Creating JAR..."
|
||||
$JAR cfm "$DIST_DIR/com.venezuela.bcvrate.jar" "$PLUGIN_DIR/META-INF/MANIFEST.MF" \
|
||||
-C "$BUILD_DIR" .
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: JAR creation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_SIZE=$(du -h "$DIST_DIR/com.venezuela.bcvrate.jar" | cut -f1)
|
||||
echo "JAR created: $DIST_DIR/com.venezuela.bcvrate.jar ($JAR_SIZE)"
|
||||
|
||||
# Copy to iDempiere plugins directory
|
||||
echo "Copying to iDempiere plugins..."
|
||||
cp "$DIST_DIR/com.venezuela.bcvrate.jar" "$IDEMPIERE_HOME/plugins/"
|
||||
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo "JAR installed at: $IDEMPIERE_HOME/plugins/com.venezuela.bcvrate.jar"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Restart iDempiere server"
|
||||
echo "2. Check bundle status in Felix console: felix:ls | grep bcvrate"
|
||||
echo "3. Create AD_Process via UI (if not done)"
|
||||
echo "4. Add process to menu"
|
||||
echo "5. Test the process"
|
||||
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Migration>
|
||||
<Application>
|
||||
<AD_Migration>
|
||||
<Step seqNo="10" type="D" name="Create Process BCV_ExchangeRateUpdate">
|
||||
<Table name="AD_Process">
|
||||
<Record>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>BCV_ExchangeRateUpdate</Name>
|
||||
<Description>Actualización automática de tasa de cambio oficial BCV (USD/VES)</Description>
|
||||
<Help>Obtiene la tasa de cambio oficial del Banco Central de Venezuela y la registra en la tabla C_Conversion_Rate. La moneda funcional se obtiene del esquema contable principal del grupo empresarial.</Help>
|
||||
<Classname>com.venezuela.bcvrate.process.BCVExchangeRateProcess</Classname>
|
||||
<IsDirectPrint>N</IsDirectPrint>
|
||||
<IsReport>N</IsReport>
|
||||
<IsServerProcess>Y</IsServerProcess>
|
||||
<ShowHelp>L</ShowHelp>
|
||||
<AD_WF_Process_ID>0</AD_WF_Process_ID>
|
||||
<Value>BCV_ExchangeRateUpdate</Value>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
|
||||
<Step seqNo="20" type="D" name="Create Process Parameter AD_Client_ID">
|
||||
<Table name="AD_Process_Para">
|
||||
<Record>
|
||||
<AD_Process_Para_ID>5000000</AD_Process_Para_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>Client</Name>
|
||||
<Description>Grupo empresarial destino para la tasa de cambio</Description>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
<SeqNo>10</SeqNo>
|
||||
<AD_Reference_ID>19</AD_Reference_ID>
|
||||
<AD_Reference_Value_ID>157</AD_Reference_Value_ID>
|
||||
<DefaultValue></DefaultValue>
|
||||
<IsMandatory>Y</IsMandatory>
|
||||
<RangeHigh>0</RangeHigh>
|
||||
<RangeLow>0</RangeLow>
|
||||
<Value>AD_Client_ID</Value>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
|
||||
<Step seqNo="30" type="D" name="Create Process Parameter C_ConversionType_ID">
|
||||
<Table name="AD_Process_Para">
|
||||
<Record>
|
||||
<AD_Process_Para_ID>5000001</AD_Process_Para_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>Currency Conversion Type</Name>
|
||||
<Description>Tipo de conversión de moneda (Spot, Corporate, etc.)</Description>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
<SeqNo>20</SeqNo>
|
||||
<AD_Reference_ID>19</AD_Reference_ID>
|
||||
<AD_Reference_Value_ID>232</AD_Reference_Value_ID>
|
||||
<DefaultValue>@SQL=SELECT C_ConversionType_ID FROM C_ConversionType WHERE IsDefault='Y' AND AD_Client_ID IN (0, @AD_Client_ID@) ORDER BY AD_Client_ID DESC LIMIT 1</DefaultValue>
|
||||
<IsMandatory>N</IsMandatory>
|
||||
<RangeHigh>0</RangeHigh>
|
||||
<RangeLow>0</RangeLow>
|
||||
<Value>C_ConversionType_ID</Value>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
|
||||
<Step seqNo="40" type="D" name="Create Process Parameter DateFrom">
|
||||
<Table name="AD_Process_Para">
|
||||
<Record>
|
||||
<AD_Process_Para_ID>5000002</AD_Process_Para_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>Date From</Name>
|
||||
<Description>Fecha de inicio para buscar tasas de cambio. Si no se indica, usa la fecha actual.</Description>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
<SeqNo>30</SeqNo>
|
||||
<AD_Reference_ID>16</AD_Reference_ID>
|
||||
<DefaultValue>@#Date@</DefaultValue>
|
||||
<IsMandatory>N</IsMandatory>
|
||||
<RangeHigh>0</RangeHigh>
|
||||
<RangeLow>0</RangeLow>
|
||||
<Value>DateFrom</Value>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
|
||||
<Step seqNo="50" type="D" name="Create Process Parameter DateTo">
|
||||
<Table name="AD_Process_Para">
|
||||
<Record>
|
||||
<AD_Process_Para_ID>5000003</AD_Process_Para_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>Date To</Name>
|
||||
<Description>Fecha de fin para buscar tasas de cambio. Si no se indica, usa la fecha actual.</Description>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
<SeqNo>40</SeqNo>
|
||||
<AD_Reference_ID>16</AD_Reference_ID>
|
||||
<DefaultValue>@#Date@</DefaultValue>
|
||||
<IsMandatory>N</IsMandatory>
|
||||
<RangeHigh>0</RangeHigh>
|
||||
<RangeLow>0</RangeLow>
|
||||
<Value>DateTo</Value>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
|
||||
<Step seqNo="60" type="D" name="Create Process Parameter IsSimulation">
|
||||
<Table name="AD_Process_Para">
|
||||
<Record>
|
||||
<AD_Process_Para_ID>5000004</AD_Process_Para_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>Simulation</Name>
|
||||
<Description>Si está marcado, el proceso solo muestra qué haría sin grabar datos</Description>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
<SeqNo>50</SeqNo>
|
||||
<AD_Reference_ID>20</AD_Reference_ID>
|
||||
<DefaultValue>N</DefaultValue>
|
||||
<IsMandatory>N</IsMandatory>
|
||||
<RangeHigh>0</RangeHigh>
|
||||
<RangeLow>0</RangeLow>
|
||||
<Value>IsSimulation</Value>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
|
||||
<Step seqNo="70" type="D" name="Create Schedule for BCV Rate Update">
|
||||
<Table name="AD_Schedule">
|
||||
<Record>
|
||||
<AD_Schedule_ID>5000000</AD_Schedule_ID>
|
||||
<AD_Client_ID>0</AD_Client_ID>
|
||||
<AD_Org_ID>0</AD_Org_ID>
|
||||
<IsActive>Y</IsActive>
|
||||
<Created>2026-06-22</Created>
|
||||
<CreatedBy>100</CreatedBy>
|
||||
<Updated>2026-06-22</Updated>
|
||||
<UpdatedBy>100</UpdatedBy>
|
||||
<Name>BCV_DailyRateUpdate</Name>
|
||||
<Description>Actualización diaria de tasa BCV - Lunes a Viernes 16:00-23:00 cada 15 min</Description>
|
||||
<ScheduleType>C</ScheduleType>
|
||||
<CronPattern>0 0/15 16-23 * * 1-5</CronPattern>
|
||||
<Frequency>15</Frequency>
|
||||
<FrequencyType>M</FrequencyType>
|
||||
<IsIgnoreProcessingTime>Y</IsIgnoreProcessingTime>
|
||||
<IsSystemSchedule>Y</IsSystemSchedule>
|
||||
<AD_Process_ID>5000000</AD_Process_ID>
|
||||
</Record>
|
||||
</Table>
|
||||
</Step>
|
||||
</AD_Migration>
|
||||
</Application>
|
||||
</Migration>
|
||||
@@ -0,0 +1,94 @@
|
||||
-- ============================================
|
||||
-- BCV Exchange Rate Plugin - Registro en iDempiere
|
||||
-- ============================================
|
||||
-- Ejecutar este script en la base de datos de iDempiere
|
||||
-- ANTES de instalar el plugin OSGi
|
||||
-- ============================================
|
||||
|
||||
-- 1. Crear el Proceso
|
||||
INSERT INTO AD_Process (
|
||||
AD_Process_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Value, Description, Help, Classname,
|
||||
AccessLevel, EntityType, IsDirectPrint, IsReport, IsServerProcess, ShowHelp
|
||||
) VALUES (
|
||||
5000000, 0, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,
|
||||
'BCV_ExchangeRateUpdate', 'BCV_ExchangeRateUpdate',
|
||||
'Actualización automática de tasa de cambio oficial BCV (USD/VES)',
|
||||
'Obtiene la tasa de cambio oficial del Banco Central de Venezuela y la registra en C_Conversion_Rate.',
|
||||
'com.venezuela.bcvrate.process.BCVExchangeRateProcess',
|
||||
'4', 'U', 'N', 'N', 'Y', 'L'
|
||||
)
|
||||
ON CONFLICT (AD_Process_ID) DO NOTHING;
|
||||
|
||||
-- 2. Parámetro: AD_Client_ID (Grupo Empresarial)
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, ColumnName, Description, AD_Process_ID, SeqNo,
|
||||
AD_Reference_ID, AD_Reference_Value_ID, IsMandatory, DefaultValue,
|
||||
FieldLength, IsRange
|
||||
) VALUES (
|
||||
5000000, 0, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,
|
||||
'Client', 'AD_Client_ID', 'Grupo empresarial destino',
|
||||
5000000, 10, 19, 157, 'Y', '', 10, 'N'
|
||||
)
|
||||
ON CONFLICT (AD_Process_Para_ID) DO NOTHING;
|
||||
|
||||
-- 3. Parámetro: C_ConversionType_ID (Tipo de Conversión)
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, ColumnName, Description, AD_Process_ID, SeqNo,
|
||||
AD_Reference_ID, AD_Reference_Value_ID, IsMandatory, DefaultValue,
|
||||
FieldLength, IsRange
|
||||
) VALUES (
|
||||
5000001, 0, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,
|
||||
'Currency Conversion Type', 'C_ConversionType_ID', 'Tipo de conversión (Spot, Corporate, etc.)',
|
||||
5000000, 20, 19, 232, 'N', '', 10, 'N'
|
||||
)
|
||||
ON CONFLICT (AD_Process_Para_ID) DO NOTHING;
|
||||
|
||||
-- 4. Parámetro: DateFrom (Fecha Desde)
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, ColumnName, Description, AD_Process_ID, SeqNo,
|
||||
AD_Reference_ID, IsMandatory, DefaultValue,
|
||||
FieldLength, IsRange
|
||||
) VALUES (
|
||||
5000002, 0, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,
|
||||
'Date From', 'DateFrom', 'Fecha inicio (default: hoy)',
|
||||
5000000, 30, 16, 'N', '@#Date@', 10, 'N'
|
||||
)
|
||||
ON CONFLICT (AD_Process_Para_ID) DO NOTHING;
|
||||
|
||||
-- 5. Parámetro: DateTo (Fecha Hasta)
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, ColumnName, Description, AD_Process_ID, SeqNo,
|
||||
AD_Reference_ID, IsMandatory, DefaultValue,
|
||||
FieldLength, IsRange
|
||||
) VALUES (
|
||||
5000003, 0, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,
|
||||
'Date To', 'DateTo', 'Fecha fin (default: hoy)',
|
||||
5000000, 40, 16, 'N', '@#Date@', 10, 'N'
|
||||
)
|
||||
ON CONFLICT (AD_Process_Para_ID) DO NOTHING;
|
||||
|
||||
-- 6. Parámetro: IsSimulation (Modo Simulación)
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, ColumnName, Description, AD_Process_ID, SeqNo,
|
||||
AD_Reference_ID, IsMandatory, DefaultValue,
|
||||
FieldLength, IsRange
|
||||
) VALUES (
|
||||
5000004, 0, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,
|
||||
'Simulation', 'IsSimulation', 'Si está marcado, solo muestra qué haría sin grabar',
|
||||
5000000, 50, 20, 'N', 'N', 1, 'N'
|
||||
)
|
||||
ON CONFLICT (AD_Process_Para_ID) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- NOTA: El Scheduler se crea manualmente en iDempiere:
|
||||
-- Ir a: Procesos > Scheduler > Nuevo
|
||||
-- Nombre: BCV_DailyRateUpdate
|
||||
-- Patrón Cron: 0 0/15 16-23 * * 1-5
|
||||
-- Proceso: BCV_ExchangeRateUpdate
|
||||
-- ============================================
|
||||
@@ -0,0 +1,90 @@
|
||||
-- ============================================================
|
||||
-- BCV Exchange Rate Process Registration for iDempiere v10
|
||||
-- ============================================================
|
||||
|
||||
-- 1. Register the Process
|
||||
INSERT INTO AD_Process (
|
||||
AD_Process_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Value, Description, Help, ClassName, IsReport, IsDirectPrint,
|
||||
IsServerProcess, IsBetaFunctionality, AccessLevel
|
||||
) VALUES (
|
||||
50001, 0, 0, 'Y', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0,
|
||||
'BCV Exchange Rate Update',
|
||||
'BCVExchangeRate',
|
||||
'Fetches USD/VES exchange rate from BCV official API and registers it',
|
||||
'Connects to BCV API to get the latest USD/VES exchange rate.',
|
||||
'com.venezuela.bcvrate.process.BCVExchangeRateProcess',
|
||||
'N', 'N', 'Y', 'N', '3'
|
||||
);
|
||||
|
||||
-- 2. Register Process Parameters
|
||||
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Description, AD_Process_ID, AD_Reference_ID, AD_Val_Rule_ID,
|
||||
IsMandatory, IsRange, FieldLength, DefaultValue, SeqNo,
|
||||
IsEncrypted, IsCentrallyMaintained, EntityType
|
||||
) VALUES (
|
||||
50001, 0, 0, 'Y', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0,
|
||||
'Client', 'Client for this installation',
|
||||
50001, 18, 100,
|
||||
'Y', 'N', 0, '@#AD_Client_ID@', 10,
|
||||
'N', 'Y', 'D'
|
||||
);
|
||||
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Description, AD_Process_ID, AD_Reference_ID, AD_Val_Rule_ID,
|
||||
IsMandatory, IsRange, FieldLength, DefaultValue, SeqNo,
|
||||
IsEncrypted, IsCentrallyMaintained, EntityType
|
||||
) VALUES (
|
||||
50002, 0, 0, 'Y', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0,
|
||||
'Conversion Type', 'Currency conversion type',
|
||||
50001, 18, 153,
|
||||
'Y', 'N', 0, 'S', 20,
|
||||
'N', 'Y', 'D'
|
||||
);
|
||||
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Description, AD_Process_ID, AD_Reference_ID,
|
||||
IsMandatory, IsRange, FieldLength, DefaultValue, SeqNo,
|
||||
IsEncrypted, IsCentrallyMaintained, EntityType
|
||||
) VALUES (
|
||||
50003, 0, 0, 'Y', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0,
|
||||
'Date From', 'Start date for rate lookup',
|
||||
50001, 16,
|
||||
'N', 'N', 0, '@#Date@', 30,
|
||||
'N', 'Y', 'D'
|
||||
);
|
||||
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Description, AD_Process_ID, AD_Reference_ID,
|
||||
IsMandatory, IsRange, FieldLength, DefaultValue, SeqNo,
|
||||
IsEncrypted, IsCentrallyMaintained, EntityType
|
||||
) VALUES (
|
||||
50004, 0, 0, 'Y', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0,
|
||||
'Date To', 'End date for rate lookup',
|
||||
50001, 16,
|
||||
'N', 'N', 0, '@#Date@', 40,
|
||||
'N', 'Y', 'D'
|
||||
);
|
||||
|
||||
INSERT INTO AD_Process_Para (
|
||||
AD_Process_Para_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
|
||||
Name, Description, AD_Process_ID, AD_Reference_ID,
|
||||
IsMandatory, IsRange, FieldLength, DefaultValue, SeqNo,
|
||||
IsEncrypted, IsCentrallyMaintained, EntityType
|
||||
) VALUES (
|
||||
50005, 0, 0, 'Y', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0,
|
||||
'Simulation Mode', 'Run in simulation mode - no actual changes saved',
|
||||
50001, 28,
|
||||
'N', 'N', 0, 'N', 50,
|
||||
'N', 'Y', 'D'
|
||||
);
|
||||
|
||||
-- 3. Verify
|
||||
SELECT p.Name, p.ClassName,
|
||||
(SELECT COUNT(*) FROM AD_Process_Para pp WHERE pp.AD_Process_ID = p.AD_Process_ID) as Params
|
||||
FROM AD_Process p WHERE p.ClassName = 'com.venezuela.bcvrate.process.BCVExchangeRateProcess';
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.venezuela</groupId>
|
||||
<artifactId>com.venezuela.bcvrate</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
<name>BCV Exchange Rate Plugin for iDempiere</name>
|
||||
<description>
|
||||
Plugin para iDempiere v10 que obtiene la tasa de cambio oficial del BCV
|
||||
(Banco Central de Venezuela) y la registra automáticamente en el sistema.
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<tycho.version>4.0.4</tycho.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.eclipse.tycho</groupId>
|
||||
<artifactId>tycho-maven-plugin</artifactId>
|
||||
<version>${tycho.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.venezuela.bcvrate;
|
||||
|
||||
import org.adempiere.base.IProcessFactory;
|
||||
import org.osgi.framework.BundleActivator;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
|
||||
import com.venezuela.bcvrate.factory.BCVProcessFactory;
|
||||
|
||||
public class Activator implements BundleActivator {
|
||||
|
||||
private ServiceRegistration<?> reg;
|
||||
|
||||
@Override
|
||||
public void start(BundleContext ctx) throws Exception {
|
||||
reg = ctx.registerService(IProcessFactory.class.getName(), new BCVProcessFactory(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(BundleContext ctx) throws Exception {
|
||||
if (reg != null) reg.unregister();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.venezuela.bcvrate.factory;
|
||||
|
||||
import org.adempiere.base.IProcessFactory;
|
||||
import org.compiere.process.SvrProcess;
|
||||
|
||||
import com.venezuela.bcvrate.process.BCVExchangeRateProcess;
|
||||
|
||||
public class BCVProcessFactory implements IProcessFactory {
|
||||
|
||||
@Override
|
||||
public SvrProcess newProcessInstance(String className) {
|
||||
if ("com.venezuela.bcvrate.process.BCVExchangeRateProcess".equals(className)) {
|
||||
return new BCVExchangeRateProcess();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package com.venezuela.bcvrate.process;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.Timestamp;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.compiere.model.MAcctSchema;
|
||||
import org.compiere.model.MConversionRate;
|
||||
import org.compiere.model.MConversionType;
|
||||
import org.compiere.model.MCurrency;
|
||||
import org.compiere.model.Query;
|
||||
import org.compiere.process.ProcessInfoParameter;
|
||||
import org.compiere.process.SvrProcess;
|
||||
import org.compiere.util.Env;
|
||||
|
||||
import com.venezuela.bcvrate.service.BCVApiService;
|
||||
import com.venezuela.bcvrate.service.BCVRateResponse;
|
||||
|
||||
public class BCVExchangeRateProcess extends SvrProcess {
|
||||
|
||||
private static final Logger log = Logger.getLogger(BCVExchangeRateProcess.class.getName());
|
||||
|
||||
private static final int C_CURRENCY_USD = 100;
|
||||
|
||||
private int p_AD_Client_ID = 0;
|
||||
private int p_C_ConversionType_ID = 0;
|
||||
private Timestamp p_DateFrom = null;
|
||||
private Timestamp p_DateTo = null;
|
||||
private boolean p_IsSimulation = false;
|
||||
|
||||
private int currencyToId = 0;
|
||||
private String currencyToIso = null;
|
||||
private BCVApiService apiService;
|
||||
|
||||
private int countInserted = 0;
|
||||
private int countSkipped = 0;
|
||||
private int countErrors = 0;
|
||||
|
||||
@Override
|
||||
protected void prepare() {
|
||||
ProcessInfoParameter[] params = getParameter();
|
||||
for (ProcessInfoParameter param : params) {
|
||||
String name = param.getParameterName();
|
||||
if (param.getParameter() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("AD_Client_ID".equals(name)) {
|
||||
p_AD_Client_ID = param.getParameterAsInt();
|
||||
} else if ("C_ConversionType_ID".equals(name)) {
|
||||
p_C_ConversionType_ID = param.getParameterAsInt();
|
||||
} else if ("DateFrom".equals(name)) {
|
||||
p_DateFrom = (Timestamp) param.getParameter();
|
||||
} else if ("DateTo".equals(name)) {
|
||||
p_DateTo = (Timestamp) param.getParameter();
|
||||
} else if ("IsSimulation".equals(name)) {
|
||||
p_IsSimulation = "Y".equals(param.getParameter());
|
||||
}
|
||||
}
|
||||
|
||||
if (p_DateFrom == null) {
|
||||
p_DateFrom = truncateToDay(new Timestamp(System.currentTimeMillis()));
|
||||
}
|
||||
if (p_DateTo == null) {
|
||||
p_DateTo = truncateToDay(new Timestamp(System.currentTimeMillis()));
|
||||
}
|
||||
if (p_AD_Client_ID <= 0) {
|
||||
p_AD_Client_ID = Env.getAD_Client_ID(getCtx());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doIt() throws Exception {
|
||||
log.info("=== Inicio proceso BCV Exchange Rate Update ===");
|
||||
log.info("Cliente: " + p_AD_Client_ID);
|
||||
log.info("Fecha desde: " + p_DateFrom);
|
||||
log.info("Fecha hasta: " + p_DateTo);
|
||||
log.info("Modo simulacion: " + p_IsSimulation);
|
||||
|
||||
apiService = new BCVApiService();
|
||||
|
||||
if (!obtenerMonedaFuncional()) {
|
||||
return "@Error@ No se pudo obtener la moneda funcional del esquema contable";
|
||||
}
|
||||
|
||||
if (p_C_ConversionType_ID <= 0) {
|
||||
p_C_ConversionType_ID = MConversionType.getDefault(p_AD_Client_ID);
|
||||
log.info("Usando tipo de conversion Spot por defecto: " + p_C_ConversionType_ID);
|
||||
}
|
||||
|
||||
Env.setContext(getCtx(), "#AD_Client_ID", p_AD_Client_ID);
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
||||
boolean esEjecucionAutomatica = (p_DateFrom.equals(p_DateTo)
|
||||
&& p_DateFrom.equals(truncateToDay(new Timestamp(System.currentTimeMillis()))));
|
||||
|
||||
Timestamp fechaInicio = p_DateFrom;
|
||||
Timestamp fechaFin = p_DateTo;
|
||||
|
||||
if (esEjecucionAutomatica) {
|
||||
Timestamp ultimaFechaConTasa = getUltimaFechaConTasa();
|
||||
|
||||
BCVRateResponse ultimaTasaBCV = apiService.getLatestRate();
|
||||
if (ultimaTasaBCV != null && ultimaTasaBCV.isValid()) {
|
||||
String fechaTasaBCV = ultimaTasaBCV.getDate();
|
||||
SimpleDateFormat sdfParse = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Timestamp fechaBCV = truncateToDay(new Timestamp(sdfParse.parse(fechaTasaBCV).getTime()));
|
||||
Timestamp hoy = truncateToDay(new Timestamp(System.currentTimeMillis()));
|
||||
|
||||
if (!fechaBCV.equals(hoy) && fechaBCV.after(hoy)) {
|
||||
log.info("Tasa BCV publicada para " + fechaTasaBCV + " (dia futuro)");
|
||||
}
|
||||
|
||||
if (ultimaFechaConTasa != null) {
|
||||
if (!fechaBCV.after(ultimaFechaConTasa)) {
|
||||
log.info("Ultima tasa BCV (" + sdf.format(fechaBCV)
|
||||
+ ") ya registrada en BD (" + sdf.format(ultimaFechaConTasa) + ")");
|
||||
String resumen = "=== Resumen ===\nNo hay dias pendientes";
|
||||
log.info(resumen);
|
||||
addLog(resumen);
|
||||
return "@OK@ No hay dias pendientes";
|
||||
}
|
||||
cal.setTime(ultimaFechaConTasa);
|
||||
cal.add(Calendar.DAY_OF_MONTH, 1);
|
||||
fechaInicio = new Timestamp(cal.getTimeInMillis());
|
||||
} else {
|
||||
fechaInicio = truncateToDay(new Timestamp(System.currentTimeMillis()));
|
||||
}
|
||||
fechaFin = fechaBCV;
|
||||
} else {
|
||||
log.info("No se encontro tasa BCV futura");
|
||||
String resumen = "=== Resumen ===\nNo hay dias pendientes";
|
||||
log.info(resumen);
|
||||
addLog(resumen);
|
||||
return "@OK@ No hay dias pendientes";
|
||||
}
|
||||
log.info("Ejecucion automatica: procesando desde " + sdf.format(fechaInicio)
|
||||
+ " hasta " + sdf.format(fechaFin));
|
||||
}
|
||||
|
||||
cal.setTime(fechaInicio);
|
||||
Timestamp currentDate = fechaInicio;
|
||||
|
||||
while (!currentDate.after(fechaFin)) {
|
||||
String dateStr = sdf.format(currentDate);
|
||||
procesarDia(currentDate, dateStr);
|
||||
cal.setTime(currentDate);
|
||||
cal.add(Calendar.DAY_OF_MONTH, 1);
|
||||
currentDate = new Timestamp(cal.getTimeInMillis());
|
||||
}
|
||||
|
||||
String resumen = "=== Resumen ===" +
|
||||
"\nTasas insertadas: " + countInserted +
|
||||
"\nYa existian (omitidas): " + countSkipped +
|
||||
"\nErrores: " + countErrors;
|
||||
|
||||
log.info(resumen);
|
||||
addLog(resumen);
|
||||
|
||||
return "@OK@ Insertadas: " + countInserted +
|
||||
", Omitidas: " + countSkipped + ", Errores: " + countErrors;
|
||||
}
|
||||
|
||||
private boolean obtenerMonedaFuncional() {
|
||||
try {
|
||||
MAcctSchema[] schemas = MAcctSchema.getClientAcctSchema(
|
||||
getCtx(), p_AD_Client_ID, get_TrxName());
|
||||
|
||||
if (schemas == null || schemas.length == 0) {
|
||||
log.severe("No se encontro esquema contable para el cliente: " + p_AD_Client_ID);
|
||||
return false;
|
||||
}
|
||||
|
||||
MAcctSchema primarySchema = schemas[0];
|
||||
currencyToId = primarySchema.getC_Currency_ID();
|
||||
currencyToIso = MCurrency.getISO_Code(getCtx(), currencyToId);
|
||||
|
||||
log.info("Moneda funcional detectada: " + currencyToIso + " (ID: " + currencyToId + ")");
|
||||
|
||||
if (C_CURRENCY_USD == currencyToId) {
|
||||
log.severe("La moneda origen (USD) es igual a la moneda destino.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.log(Level.SEVERE, "Error obteniendo moneda funcional", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void procesarDia(Timestamp date, String dateStr) {
|
||||
log.info("Procesando fecha: " + dateStr);
|
||||
addLog("Buscando tasa para " + dateStr + "...");
|
||||
|
||||
if (existeTasaParaFecha(date)) {
|
||||
log.info("Ya existe tasa para " + dateStr + ", omitiendo...");
|
||||
addLog("Fecha " + dateStr + ": Ya existe tasa (omitida)");
|
||||
countSkipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
BCVRateResponse rateResponse = apiService.getLatestRate();
|
||||
|
||||
if (rateResponse == null || !rateResponse.isValid()) {
|
||||
log.warning("No se pudo obtener tasa del BCV para " + dateStr);
|
||||
addLog("Fecha " + dateStr + ": Error - No se obtuvo tasa del BCV (API + scraping fallaron)");
|
||||
countErrors++;
|
||||
return;
|
||||
}
|
||||
|
||||
String apiEffectiveDate = rateResponse.getEffectiveDate();
|
||||
log.info("Tasa BCV: " + rateResponse.getDollar()
|
||||
+ " fecha_solicitada=" + dateStr
|
||||
+ " fecha_bcv=" + apiEffectiveDate);
|
||||
addLog("Fecha " + dateStr + ": Tasa encontrada = " + rateResponse.getDollar()
|
||||
+ " (BCV fecha: " + apiEffectiveDate + ")");
|
||||
|
||||
if (!p_IsSimulation) {
|
||||
boolean created = crearRegistroTasa(date, rateResponse.getDollar());
|
||||
if (created) {
|
||||
addLog("Fecha " + dateStr + ": Tasa insertada = " + rateResponse.getDollar()
|
||||
+ " (BCV fecha: " + apiEffectiveDate + ")");
|
||||
countInserted++;
|
||||
} else {
|
||||
addLog("Fecha " + dateStr + ": Error al insertar tasa");
|
||||
countErrors++;
|
||||
}
|
||||
} else {
|
||||
addLog("Fecha " + dateStr + ": [SIMULACION] Tasa = " + rateResponse.getDollar()
|
||||
+ " (BCV fecha: " + apiEffectiveDate + ")");
|
||||
countInserted++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean existeTasaParaFecha(Timestamp date) {
|
||||
Timestamp startOfDay = truncateToDay(date);
|
||||
Timestamp endOfDay = addOneDay(startOfDay);
|
||||
|
||||
String whereClause = "C_Currency_ID=? AND C_Currency_ID_To=? " +
|
||||
"AND C_ConversionType_ID=? AND AD_Client_ID=? " +
|
||||
"AND ValidFrom>=? AND ValidFrom<?";
|
||||
|
||||
long count = new Query(getCtx(), "C_Conversion_Rate", whereClause, get_TrxName())
|
||||
.setParameters(C_CURRENCY_USD, currencyToId, p_C_ConversionType_ID,
|
||||
p_AD_Client_ID, startOfDay, endOfDay)
|
||||
.count();
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private boolean crearRegistroTasa(Timestamp date, BigDecimal rate) {
|
||||
try {
|
||||
Timestamp startOfDay = truncateToDay(date);
|
||||
BigDecimal multiplyRate = rate.setScale(4, RoundingMode.HALF_UP);
|
||||
BigDecimal divideRate = BigDecimal.ONE.divide(multiplyRate, 10, RoundingMode.HALF_UP);
|
||||
log.info("Insertando tasa: USD -> " + currencyToIso + " = " + multiplyRate + " fecha=" + startOfDay);
|
||||
|
||||
MConversionRate cr = new MConversionRate(getCtx(), 0, get_TrxName());
|
||||
cr.set_ValueNoCheck("AD_Client_ID", p_AD_Client_ID);
|
||||
cr.set_ValueNoCheck("AD_Org_ID", 0);
|
||||
cr.set_ValueNoCheck("C_Currency_ID", C_CURRENCY_USD);
|
||||
cr.set_ValueNoCheck("C_Currency_ID_To", currencyToId);
|
||||
cr.set_ValueNoCheck("C_ConversionType_ID", p_C_ConversionType_ID);
|
||||
cr.set_ValueNoCheck("ValidFrom", startOfDay);
|
||||
cr.set_ValueNoCheck("ValidTo", startOfDay);
|
||||
cr.set_ValueNoCheck("MultiplyRate", multiplyRate);
|
||||
cr.set_ValueNoCheck("DivideRate", divideRate);
|
||||
cr.saveEx();
|
||||
|
||||
log.info("Registro creado: ID=" + cr.get_ID()
|
||||
+ " MultiplyRate=" + cr.get_Value("MultiplyRate")
|
||||
+ " DivideRate=" + cr.get_Value("DivideRate"));
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
String msg = "Error creando registro: " + e.getMessage();
|
||||
log.log(Level.SEVERE, msg, e);
|
||||
addLog(" Detalle: " + e.getClass().getSimpleName() + " - " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Timestamp getUltimaFechaConTasa() {
|
||||
String whereClause = "C_Currency_ID=? AND C_Currency_ID_To=? " +
|
||||
"AND C_ConversionType_ID=? AND AD_Client_ID=?";
|
||||
|
||||
MConversionRate lastRate = new Query(getCtx(), "C_Conversion_Rate", whereClause, get_TrxName())
|
||||
.setParameters(C_CURRENCY_USD, currencyToId, p_C_ConversionType_ID, p_AD_Client_ID)
|
||||
.setOrderBy("ValidFrom DESC")
|
||||
.first();
|
||||
|
||||
if (lastRate != null) {
|
||||
Timestamp lastDate = lastRate.getValidFrom();
|
||||
log.info("Ultima fecha con tasa en BD: " + new SimpleDateFormat("yyyy-MM-dd").format(lastDate));
|
||||
return lastDate;
|
||||
}
|
||||
|
||||
log.info("No hay tasas previas en BD");
|
||||
return null;
|
||||
}
|
||||
|
||||
private Timestamp truncateToDay(Timestamp ts) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(ts);
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
return new Timestamp(cal.getTimeInMillis());
|
||||
}
|
||||
|
||||
private Timestamp addOneDay(Timestamp ts) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(ts);
|
||||
cal.add(Calendar.DAY_OF_MONTH, 1);
|
||||
return new Timestamp(cal.getTimeInMillis());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.venezuela.bcvrate.service;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class BCVApiService {
|
||||
|
||||
private static final Logger log = Logger.getLogger(BCVApiService.class.getName());
|
||||
|
||||
private static final String BASE_URL = "https://bcv.today/api/v1";
|
||||
private static final String BCV_WEBSITE = "https://www.bcv.org.ve/";
|
||||
private static final int CONNECTION_TIMEOUT = 15000;
|
||||
private static final int READ_TIMEOUT = 30000;
|
||||
private static final int MAX_DAYS_BACK = 10;
|
||||
|
||||
public BCVRateResponse getRateForDateWithFallback(String requestedDate) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
||||
try {
|
||||
cal.setTime(sdf.parse(requestedDate));
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error parseando fecha: " + requestedDate, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= MAX_DAYS_BACK; i++) {
|
||||
String dateToQuery = sdf.format(cal.getTime());
|
||||
BCVRateResponse rate = getRateFromApi(dateToQuery);
|
||||
|
||||
if (rate != null && rate.isValid()) {
|
||||
log.info("Tasa encontrada (API) consultando " + dateToQuery
|
||||
+ " para fecha solicitada " + requestedDate
|
||||
+ ": USD=" + rate.getDollar()
|
||||
+ " effective=" + rate.getDate());
|
||||
return new BCVRateResponse(rate.getDollar(), requestedDate, rate.getDate());
|
||||
}
|
||||
|
||||
cal.add(Calendar.DAY_OF_MONTH, -1);
|
||||
}
|
||||
|
||||
log.info("API no tiene tasa para " + requestedDate + ", intentando scraping BCV...");
|
||||
BCVRateResponse websiteRate = getRateFromBCVWebsite();
|
||||
if (websiteRate != null && websiteRate.isValid()) {
|
||||
log.info("Tasa obtenida del sitio web BCV: USD=" + websiteRate.getDollar()
|
||||
+ " effective=" + websiteRate.getDate());
|
||||
return new BCVRateResponse(websiteRate.getDollar(), requestedDate, websiteRate.getDate());
|
||||
}
|
||||
|
||||
log.info("No se encontro tasa en los ultimos " + MAX_DAYS_BACK + " dias para: " + requestedDate);
|
||||
return null;
|
||||
}
|
||||
|
||||
public BCVRateResponse getRateForDate(String date) {
|
||||
return getRateFromApi(date);
|
||||
}
|
||||
|
||||
public BCVRateResponse getLatestRate() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.DAY_OF_MONTH, i);
|
||||
String futureDate = sdf.format(cal.getTime());
|
||||
|
||||
BCVRateResponse rate = getRateFromApi(futureDate);
|
||||
if (rate != null && rate.isValid()) {
|
||||
log.info("Tasa encontrada (API futura) para " + futureDate
|
||||
+ ": USD=" + rate.getDollar() + " effective=" + rate.getDate());
|
||||
return rate;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("API no tiene tasa futura, intentando scraping BCV...");
|
||||
try {
|
||||
BCVRateResponse websiteRate = getRateFromBCVWebsite();
|
||||
if (websiteRate != null && websiteRate.isValid()) {
|
||||
log.info("Tasa obtenida del sitio web BCV: USD=" + websiteRate.getDollar()
|
||||
+ " effective=" + websiteRate.getDate());
|
||||
return websiteRate;
|
||||
}
|
||||
log.info("Scraping BCV no devolvio tasa valida");
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Scraping BCV fallo", e);
|
||||
}
|
||||
|
||||
log.info("Fallback a API actual...");
|
||||
BCVRateResponse apiRate = getCurrentRate();
|
||||
return apiRate;
|
||||
}
|
||||
|
||||
private BCVRateResponse getRateFromApi(String date) {
|
||||
String urlStr = BASE_URL + "/history/" + date + ".json";
|
||||
|
||||
try {
|
||||
String response = httpGet(urlStr);
|
||||
if (response == null) return null;
|
||||
|
||||
BigDecimal dollar = extractUSD(response);
|
||||
String effectiveDate = extractField(response, "effective_date");
|
||||
if (effectiveDate == null) effectiveDate = extractField(response, "date");
|
||||
|
||||
if (dollar != null && effectiveDate != null) {
|
||||
return new BCVRateResponse(dollar, effectiveDate);
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error consultando fecha: " + date, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BCVRateResponse getCurrentRate() {
|
||||
String urlStr = BASE_URL + "/rate.json";
|
||||
|
||||
try {
|
||||
String response = httpGet(urlStr);
|
||||
if (response == null) return null;
|
||||
|
||||
BigDecimal dollar = extractUSD(response);
|
||||
String effectiveDate = extractField(response, "effective_date");
|
||||
if (effectiveDate == null) effectiveDate = extractField(response, "date");
|
||||
|
||||
if (dollar != null && effectiveDate != null) {
|
||||
log.info("Tasa actual (API): USD=" + dollar + " effective=" + effectiveDate);
|
||||
return new BCVRateResponse(dollar, effectiveDate);
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error consultando tasa actual", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BCVRateResponse getRateFromBCVWebsite() {
|
||||
try {
|
||||
log.info("Iniciando scraping de BCV website...");
|
||||
String html = httpGetHtml(BCV_WEBSITE);
|
||||
if (html == null) {
|
||||
log.warning("No se pudo obtener pagina BCV (httpGetHtml returned null)");
|
||||
return null;
|
||||
}
|
||||
log.info("Pagina BCV obtenida, longitud: " + html.length());
|
||||
|
||||
Matcher rateMatcher = Pattern.compile(
|
||||
"USD</span>.*?strong-tb\">\\s*([\\d.,]+)\\s*</strong>", Pattern.DOTALL).matcher(html);
|
||||
if (!rateMatcher.find()) {
|
||||
log.warning("No se encontro tasa USD en pagina BCV (regex no match)");
|
||||
return null;
|
||||
}
|
||||
|
||||
String rateStr = rateMatcher.group(1).replace(",", ".");
|
||||
log.info("Tasa encontrada en HTML: " + rateStr);
|
||||
BigDecimal dollar = new BigDecimal(rateStr).setScale(4, RoundingMode.HALF_UP);
|
||||
|
||||
Matcher dateMatcher = Pattern.compile(
|
||||
"content=\"(\\d{4}-\\d{2}-\\d{2})T").matcher(html);
|
||||
String effectiveDate = null;
|
||||
if (dateMatcher.find()) {
|
||||
effectiveDate = dateMatcher.group(1);
|
||||
}
|
||||
|
||||
if (dollar.compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (effectiveDate == null) {
|
||||
effectiveDate = new SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date());
|
||||
}
|
||||
log.info("Scraping BCV exitoso: USD=" + dollar + " effective=" + effectiveDate);
|
||||
return new BCVRateResponse(dollar, effectiveDate);
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error scraping BCV website: " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal extractUSD(String json) {
|
||||
Matcher m = Pattern.compile("\"USD\"\\s*:\\s*([0-9.]+)").matcher(json);
|
||||
if (m.find()) {
|
||||
return new BigDecimal(m.group(1)).setScale(4, RoundingMode.HALF_UP);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractField(String json, String field) {
|
||||
Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*\"([^\"]+)\"").matcher(json);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String httpGet(String urlStr) throws Exception {
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
conn.disconnect();
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String httpGetHtml(String urlStr) throws Exception {
|
||||
URL url = new URL(urlStr);
|
||||
|
||||
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() { return null; }
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||
}
|
||||
};
|
||||
|
||||
SSLContext sc = SSLContext.getInstance("TLS");
|
||||
sc.init(null, trustAllCerts, new SecureRandom());
|
||||
|
||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
||||
conn.setSSLSocketFactory(sc.getSocketFactory());
|
||||
conn.setHostnameVerifier((hostname, session) -> true);
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("Accept", "text/html");
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
conn.disconnect();
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.venezuela.bcvrate.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Modelo para la respuesta del historial de tasas de la API BCV.
|
||||
* Endpoint: GET /rates/history?start_date={start}&end_date={end}
|
||||
* Respuesta: {"start_date": "...", "end_date": "...", "rates": [...]}
|
||||
*/
|
||||
public class BCVHistoryResponse {
|
||||
|
||||
private String start_date;
|
||||
private String end_date;
|
||||
private List<RateEntry> rates;
|
||||
|
||||
public BCVHistoryResponse() {
|
||||
this.rates = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getStart_date() {
|
||||
return start_date;
|
||||
}
|
||||
|
||||
public void setStart_date(String start_date) {
|
||||
this.start_date = start_date;
|
||||
}
|
||||
|
||||
public String getEnd_date() {
|
||||
return end_date;
|
||||
}
|
||||
|
||||
public void setEnd_date(String end_date) {
|
||||
this.end_date = end_date;
|
||||
}
|
||||
|
||||
public List<RateEntry> getRates() {
|
||||
return rates;
|
||||
}
|
||||
|
||||
public void setRates(List<RateEntry> rates) {
|
||||
this.rates = rates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la tasa más reciente del historial (primer elemento de la lista).
|
||||
* La API retorna las tasas ordenadas de más reciente a más antigua.
|
||||
* @return la tasa más reciente, o null si no hay tasas
|
||||
*/
|
||||
public RateEntry getMostRecentRate() {
|
||||
if (rates != null && !rates.isEmpty()) {
|
||||
return rates.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modelo interno para cada entrada de tasa en el historial.
|
||||
*/
|
||||
public static class RateEntry {
|
||||
private BigDecimal dollar;
|
||||
private String date;
|
||||
|
||||
public RateEntry() {
|
||||
}
|
||||
|
||||
public RateEntry(BigDecimal dollar, String date) {
|
||||
this.dollar = dollar;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public BigDecimal getDollar() {
|
||||
return dollar;
|
||||
}
|
||||
|
||||
public void setDollar(BigDecimal dollar) {
|
||||
this.dollar = dollar;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return dollar != null
|
||||
&& dollar.compareTo(BigDecimal.ZERO) > 0
|
||||
&& date != null
|
||||
&& !date.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RateEntry{dollar=" + dollar + ", date='" + date + "'}";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BCVHistoryResponse{start_date='" + start_date + "', end_date='" + end_date
|
||||
+ "', rates_count=" + (rates != null ? rates.size() : 0) + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.venezuela.bcvrate.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class BCVRateResponse {
|
||||
|
||||
private BigDecimal dollar;
|
||||
private String date;
|
||||
private String effectiveDate;
|
||||
|
||||
public BCVRateResponse() {
|
||||
}
|
||||
|
||||
public BCVRateResponse(BigDecimal dollar, String date) {
|
||||
this.dollar = dollar;
|
||||
this.date = date;
|
||||
this.effectiveDate = date;
|
||||
}
|
||||
|
||||
public BCVRateResponse(BigDecimal dollar, String requestedDate, String effectiveDate) {
|
||||
this.dollar = dollar;
|
||||
this.date = requestedDate;
|
||||
this.effectiveDate = effectiveDate;
|
||||
}
|
||||
|
||||
public BigDecimal getDollar() {
|
||||
return dollar;
|
||||
}
|
||||
|
||||
public void setDollar(BigDecimal dollar) {
|
||||
this.dollar = dollar;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getEffectiveDate() {
|
||||
return effectiveDate;
|
||||
}
|
||||
|
||||
public void setEffectiveDate(String effectiveDate) {
|
||||
this.effectiveDate = effectiveDate;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return dollar != null
|
||||
&& dollar.compareTo(BigDecimal.ZERO) > 0
|
||||
&& date != null
|
||||
&& !date.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BCVRateResponse{dollar=" + dollar + ", date='" + date
|
||||
+ "', effectiveDate='" + effectiveDate + "'}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user