.
This commit is contained in:
parent
cfa08ea61d
commit
7551f9d5ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -72,5 +72,6 @@ FACTUGES.FDB
|
||||
# ===========================
|
||||
# Otros
|
||||
# ===========================
|
||||
out
|
||||
last_execution*.txt
|
||||
*.json
|
||||
|
||||
47
Dockerfile
47
Dockerfile
@ -1,38 +1,31 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# Usa una imagen base de Python
|
||||
FROM python:3.12.6-slim-bookworm AS python_script
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
APP_HOME="/app"
|
||||
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
|
||||
# Instalar librerías cliente Firebird
|
||||
RUN apt-get update
|
||||
RUN apt-get install libfbclient2 -y
|
||||
|
||||
# Establece el directorio de trabajo dentro del contenedor
|
||||
WORKDIR /opt/uecko_sync_app
|
||||
# Copiamos solo lo necesario para instalar el paquete
|
||||
COPY pyproject.toml setup.cfg README.md ./
|
||||
COPY app ./app
|
||||
|
||||
# Copia los archivos del proyecto al contenedor
|
||||
COPY . .
|
||||
COPY ./.env.production ./.env
|
||||
RUN pip install --no-cache-dir .
|
||||
|
||||
# Instala las dependencias de Python
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Copiar enviroment (se sobreescribe en compose)
|
||||
#COPY enviroment/ ./enviroment
|
||||
|
||||
# Instala cron en el contenedor
|
||||
RUN apt-get update && apt-get install -y cron nano
|
||||
# Volumen para logs persistentes
|
||||
#VOLUME ["/app/logs"]
|
||||
|
||||
# Copia el archivo de cron dentro del contenedor
|
||||
COPY cronjob /etc/cron.d/cronjob
|
||||
|
||||
# Da permisos de ejecución al archivo cronjob
|
||||
RUN chmod 0644 /etc/cron.d/cronjob
|
||||
|
||||
# Aplica la configuración de cronjob
|
||||
RUN crontab /etc/cron.d/cronjob
|
||||
|
||||
# Crea un archivo log para cron
|
||||
RUN touch /var/log/cron.log
|
||||
|
||||
#RUN mkdir -p /var/log/uecko_sync_app
|
||||
#RUN touch /var/log/uecko_sync_app/uecko_sync_app.log
|
||||
|
||||
# Comando para iniciar cron y mantener el contenedor en ejecución
|
||||
CMD cron && tail -f /var/log/cron.log
|
||||
# Entrypoint genérico
|
||||
#CMD ["python", "-m", "sync_factuges_main"]
|
||||
CMD ["factuges-sync"]
|
||||
|
||||
126
README.md
Normal file
126
README.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Factuges Sync
|
||||
Proceso ETL para sincronizar datos desde una base de datos Legacy hacia MariaDB.
|
||||
Incluye dos modos de sincronización:
|
||||
|
||||
- `factuges`
|
||||
- `verifactu`
|
||||
|
||||
En desarrollo **no se utiliza Docker**.
|
||||
En producción **sí se utiliza Docker + cron**.
|
||||
|
||||
---
|
||||
|
||||
# 1. Requisitos previos
|
||||
|
||||
- Python 3.11 o superior
|
||||
- Git instalado
|
||||
- Acceso a las bases de datos necesarias para las pruebas
|
||||
- (Opcional) Librerías/cliente de Firebird si la BD legacy las requiere
|
||||
|
||||
---
|
||||
|
||||
# 2. Clonar el repositorio
|
||||
|
||||
```bash
|
||||
git clone <URL_DEL_REPO> factuges-sync
|
||||
cd factuges-sync
|
||||
```
|
||||
|
||||
# 3. Crear y activar entorno virtual
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
En Windows:
|
||||
|
||||
```powershell
|
||||
venv\Scripts\activate
|
||||
```
|
||||
|
||||
# 4. Instalar dependencias del proyecto
|
||||
|
||||
Este proyecto se instala mediante setup.cfg + pyproject.toml.
|
||||
|
||||
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Esto crea el comando factuges-sync disponible en tu entorno virtual.
|
||||
|
||||
# 5. Configurar entorno de desarrollo
|
||||
|
||||
El código carga automáticamente:
|
||||
|
||||
* environment/dev.env cuando ENV=dev
|
||||
* Variables del contenedor en producción (ENV=prod)
|
||||
|
||||
Por tanto, **en desarrollo**:
|
||||
|
||||
```bash
|
||||
export ENV=dev
|
||||
```
|
||||
|
||||
(En Windows: set ENV=dev)
|
||||
|
||||
Revisa/edita:
|
||||
|
||||
```bash
|
||||
environment/dev.env
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```ini
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASS=123
|
||||
SYNC_INTERVAL_MINUTES=5
|
||||
```
|
||||
|
||||
# 6. Ejecutar el job en desarrollo
|
||||
|
||||
Puedes ejecutar cualquiera de los dos modos.
|
||||
|
||||
**Opción A: usando el CLI**
|
||||
|
||||
```bash
|
||||
# Modo FACTUGES
|
||||
factuges-sync factuges
|
||||
|
||||
# Modo VERIFACTU
|
||||
factuges-sync verifactu
|
||||
```
|
||||
|
||||
**Opción B: llamando directamente a los módulos Python**
|
||||
|
||||
```bash
|
||||
python -m app.sync_factuges_main
|
||||
python -m app.sync_verifactu_main
|
||||
```
|
||||
|
||||
# 7. Logs en desarrollo
|
||||
|
||||
Si el proyecto escribe logs en logs/:
|
||||
|
||||
```bash
|
||||
tail -f logs/job.log
|
||||
```
|
||||
|
||||
# 8. Producción (resumen)
|
||||
|
||||
En producción se usa Docker y cron.
|
||||
Build:
|
||||
|
||||
```bash
|
||||
./scripts/build.sh <company>
|
||||
```
|
||||
|
||||
Cron job:
|
||||
|
||||
```bash
|
||||
*/5 * * * * docker run --rm -e ENV=prod -e SYNC_MODE=factuges myimage:latest
|
||||
```
|
||||
@ -1 +1 @@
|
||||
2025-11-28 10:37:49
|
||||
2025-11-30 09:07:15
|
||||
45
app/cli.py
Normal file
45
app/cli.py
Normal file
@ -0,0 +1,45 @@
|
||||
# app/cli.py
|
||||
# CLI simple para seleccionar el entrypoint correcto.
|
||||
# Permite:
|
||||
# factuges-sync factuges
|
||||
# factuges-sync verifactu
|
||||
# SYNC_MODE=factuges factuges-sync
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Selector de modos de sincronización."""
|
||||
parser = argparse.ArgumentParser(description="Factuges Sync Dispatcher")
|
||||
parser.add_argument(
|
||||
"mode",
|
||||
nargs="?",
|
||||
choices=["factuges", "verifactu"],
|
||||
help="Modo de sincronización",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
mode = args.mode or os.getenv("SYNC_MODE")
|
||||
|
||||
if mode not in ("factuges", "verifactu"):
|
||||
print("Error: debes indicar modo: 'factuges' o 'verifactu'")
|
||||
sys.exit(1)
|
||||
|
||||
if os.getenv("ENV") == "dev":
|
||||
print("Running in development mode (no docker)")
|
||||
|
||||
module = (
|
||||
"app.sync_factuges_main"
|
||||
if mode == "factuges"
|
||||
else "app.sync_verifactu_main"
|
||||
)
|
||||
|
||||
# Ejecuta el módulo Python correspondiente
|
||||
subprocess.run([sys.executable, "-m", module], check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,48 +1,81 @@
|
||||
import logging
|
||||
import os
|
||||
from os.path import join, dirname
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def load_config():
|
||||
dotenv_path = join(dirname(__file__), '../../.env')
|
||||
load_dotenv(dotenv_path)
|
||||
def _required(name: str) -> str:
|
||||
"""
|
||||
Devuelve el valor de la variable de entorno `name`.
|
||||
Lanza RuntimeError si no existe o está vacía.
|
||||
"""
|
||||
value = os.getenv(name)
|
||||
if value is None or value == "":
|
||||
# Comentario: fail fast con mensaje claro
|
||||
raise RuntimeError(f"Missing required environment variable: {name}")
|
||||
return value
|
||||
|
||||
return {
|
||||
'ENVIRONMENT': os.getenv('ENVIRONMENT'),
|
||||
'LOCAL_TZ': os.getenv('LOCAL_TZ', 'Europe/Madrid'),
|
||||
'LAST_RUN_PATH': os.getenv('LAST_RUN_PATH'),
|
||||
# 'LOG_PATH': os.getenv('LOG_PATH', 'app.log'),
|
||||
|
||||
'FACTUGES_HOST': os.getenv('FACTUGES_HOST'),
|
||||
'FACTUGES_PORT': os.getenv('FACTUGES_PORT'),
|
||||
'FACTUGES_DATABASE': os.getenv('FACTUGES_DATABASE'),
|
||||
'FACTUGES_USER': os.getenv('FACTUGES_USER'),
|
||||
'FACTUGES_PASSWORD': os.getenv('FACTUGES_PASSWORD'),
|
||||
def load_config() -> Dict[str, Any]:
|
||||
"""
|
||||
Carga la configuración desde variables de entorno.
|
||||
|
||||
'UECKO_MYSQL_HOST': os.getenv('UECKO_MYSQL_HOST'),
|
||||
'UECKO_MYSQL_PORT': os.getenv('UECKO_MYSQL_PORT', 3306),
|
||||
'UECKO_MYSQL_DATABASE': os.getenv('UECKO_MYSQL_DATABASE'),
|
||||
'UECKO_MYSQL_USER': os.getenv('UECKO_MYSQL_USER'),
|
||||
'UECKO_MYSQL_PASSWORD': os.getenv('UECKO_MYSQL_PASSWORD'),
|
||||
- En dev: carga dev.env y luego valida.
|
||||
- En prod: NO carga ningún .env, solo usa entorno del sistema/contendor.
|
||||
- Si falta alguna variable requerida -> RuntimeError.
|
||||
"""
|
||||
|
||||
'CTE_COMPANY_ID': os.getenv('CTE_COMPANY_ID'),
|
||||
'CTE_SERIE': os.getenv('CTE_SERIE'),
|
||||
'CTE_STATUS_INVOICE': os.getenv('CTE_STATUS_INVOICE'),
|
||||
'CTE_IS_PROFORMA': os.getenv('CTE_IS_PROFORMA'),
|
||||
'CTE_STATUS_VERIFACTU': os.getenv('CTE_STATUS_VERIFACTU'),
|
||||
'CTE_LANGUAGE_CODE': os.getenv('CTE_LANGUAGE_CODE'),
|
||||
'CTE_COUNTRY_CODE': os.getenv('CTE_COUNTRY_CODE'),
|
||||
'CTE_IS_COMPANY': os.getenv('CTE_IS_COMPANY'),
|
||||
'CTE_SYNC_RESULT_OK': os.getenv('CTE_SYNC_RESULT_OK'),
|
||||
'CTE_SYNC_RESULT_FAIL': os.getenv('CTE_SYNC_RESULT_FAIL'),
|
||||
env = os.getenv("ENV", "dev")
|
||||
|
||||
'VERIFACTU_BASE_URL': os.getenv('VERIFACTU_BASE_URL'),
|
||||
'VERIFACTU_API_KEY': os.getenv('VERIFACTU_API_KEY'),
|
||||
'VERIFACTU_NIFS_API_KEY': os.getenv('VERIFACTU_NIFS_API_KEY'),
|
||||
if env == "dev":
|
||||
dotenv_path = join(dirname(__file__), "../../enviroment/dev.env")
|
||||
load_dotenv(dotenv_path)
|
||||
elif env == "prod":
|
||||
# En producción NO se carga archivo .env
|
||||
# Las variables vienen del contenedor (docker run -e VAR=...)
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError(f"Unknown ENV: {env}")
|
||||
|
||||
# 'BREVO_API_KEY': os.getenv('BREVO_API_KEY'),
|
||||
# 'BREVO_EMAIL_TEMPLATE': os.getenv("BREVO_EMAIL_TEMPLATE"),
|
||||
# 'MAIL_FROM': os.getenv('MAIL_FROM'),
|
||||
# 'MAIL_TO': os.getenv('MAIL_TO'),
|
||||
config: Dict[str, Any] = {
|
||||
# Opcionales (con valor por defecto)
|
||||
"ENV": env,
|
||||
"ENVIRONMENT": os.getenv("ENVIRONMENT", env),
|
||||
"LOCAL_TZ": os.getenv("LOCAL_TZ", "Europe/Madrid"),
|
||||
"LAST_RUN_PATH": _required("LAST_RUN_PATH"),
|
||||
|
||||
# FACTUGES (requeridas)
|
||||
"FACTUGES_HOST": _required("FACTUGES_HOST"),
|
||||
"FACTUGES_PORT": _required("FACTUGES_PORT"),
|
||||
"FACTUGES_DATABASE": _required("FACTUGES_DATABASE"),
|
||||
"FACTUGES_USER": _required("FACTUGES_USER"),
|
||||
"FACTUGES_PASSWORD": _required("FACTUGES_PASSWORD"),
|
||||
|
||||
# UECKO MySQL (requeridas salvo puerto)
|
||||
"UECKO_MYSQL_HOST": _required("UECKO_MYSQL_HOST"),
|
||||
"UECKO_MYSQL_PORT": os.getenv("UECKO_MYSQL_PORT", "3306"),
|
||||
"UECKO_MYSQL_DATABASE": _required("UECKO_MYSQL_DATABASE"),
|
||||
"UECKO_MYSQL_USER": _required("UECKO_MYSQL_USER"),
|
||||
"UECKO_MYSQL_PASSWORD": _required("UECKO_MYSQL_PASSWORD"),
|
||||
|
||||
# Constantes/CTE (requeridas)
|
||||
"CTE_COMPANY_ID": _required("CTE_COMPANY_ID"),
|
||||
"CTE_SERIE": _required("CTE_SERIE"),
|
||||
"CTE_STATUS_INVOICE": _required("CTE_STATUS_INVOICE"),
|
||||
"CTE_IS_PROFORMA": _required("CTE_IS_PROFORMA"),
|
||||
"CTE_STATUS_VERIFACTU": _required("CTE_STATUS_VERIFACTU"),
|
||||
"CTE_LANGUAGE_CODE": _required("CTE_LANGUAGE_CODE"),
|
||||
"CTE_COUNTRY_CODE": _required("CTE_COUNTRY_CODE"),
|
||||
"CTE_IS_COMPANY": _required("CTE_IS_COMPANY"),
|
||||
"CTE_SYNC_RESULT_OK": _required("CTE_SYNC_RESULT_OK"),
|
||||
"CTE_SYNC_RESULT_FAIL": _required("CTE_SYNC_RESULT_FAIL"),
|
||||
|
||||
# Verifactu (requeridas)
|
||||
"VERIFACTU_BASE_URL": _required("VERIFACTU_BASE_URL"),
|
||||
"VERIFACTU_API_KEY": _required("VERIFACTU_API_KEY"),
|
||||
"VERIFACTU_NIFS_API_KEY": _required("VERIFACTU_NIFS_API_KEY"),
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import logging
|
||||
from datetime import date
|
||||
from config import load_config
|
||||
from app.config import load_config
|
||||
import textwrap
|
||||
from typing import Dict, Any, Optional
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert, map_tax_code, cents, cents4, money_round, tax_fraction_from_code
|
||||
from app.utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert, map_tax_code, cents, cents4, money_round, tax_fraction_from_code
|
||||
from striprtf.striprtf import rtf_to_text
|
||||
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from uuid6 import uuid7
|
||||
from config import load_config
|
||||
from app.config import load_config
|
||||
from . import sql_sentences as SQL
|
||||
from . import normalizations as NORMALIZA
|
||||
from utils import validar_nif
|
||||
from app.utils import validar_nif
|
||||
|
||||
|
||||
def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from typing import Dict, Any, Tuple, Optional, List, Iterable
|
||||
from config import load_config
|
||||
from app.config import load_config
|
||||
from decimal import Decimal
|
||||
from utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str
|
||||
from app.utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str
|
||||
from . import sql_sentences as SQL
|
||||
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from __version_sync_factuges__ import __version__
|
||||
from app.__version_sync_factuges__ import __version__
|
||||
from datetime import datetime
|
||||
from dateutil import tz
|
||||
from config import setup_logging, load_config
|
||||
from db import get_mysql_connection, get_factuges_connection, sync_invoices_factuges
|
||||
from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail
|
||||
from app.config import setup_logging, load_config
|
||||
from app.db import get_mysql_connection, get_factuges_connection, sync_invoices_factuges
|
||||
from app.utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from __version_sync_factuges__ import __version__
|
||||
from app.__version_sync_factuges__ import __version__
|
||||
from datetime import datetime
|
||||
from dateutil import tz
|
||||
from config import setup_logging, load_config
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
DEFAULT_PATH = "./last_execution.txt"
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
DEFAULT_PATH = Path("last_execution.txt")
|
||||
DEFAULT_FALLBACK = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
FMT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
@ -14,22 +18,33 @@ def obtener_fecha_ultima_ejecucion(
|
||||
) -> datetime:
|
||||
"""
|
||||
Lee la última fecha de ejecución desde `path` y la devuelve como aware (UTC).
|
||||
Si no existe o hay error de parseo, devuelve `fallback` (por defecto 2024-01-01 UTC).
|
||||
|
||||
- Si el fichero no existe o el contenido es inválido, devuelve `fallback`.
|
||||
- Si `fallback` es None, usa DEFAULT_FALLBACK (2024-01-01T00:00:00Z).
|
||||
"""
|
||||
if fallback is None:
|
||||
fallback = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
# Comentario: fallback explícito para evitar lógica duplicada en llamadas
|
||||
effective_fallback = fallback or DEFAULT_FALLBACK
|
||||
|
||||
if path is None:
|
||||
return effective_fallback
|
||||
|
||||
# 2. Convertimos str -> Path
|
||||
path = Path(path)
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf8") as f:
|
||||
fecha_str = f.read().strip()
|
||||
# Se guarda como texto sin tz; interpretamos como UTC
|
||||
dt_naive = datetime.strptime(fecha_str, FMT)
|
||||
text = path.read_text(encoding="utf-8").strip()
|
||||
if not text:
|
||||
# Comentario: fichero vacío -> usamos fallback
|
||||
return effective_fallback
|
||||
|
||||
dt_naive = datetime.strptime(text, FMT)
|
||||
return dt_naive.replace(tzinfo=timezone.utc)
|
||||
except FileNotFoundError:
|
||||
return fallback
|
||||
return effective_fallback
|
||||
except ValueError:
|
||||
# Formato inválido en el archivo -> usar fallback
|
||||
return fallback
|
||||
# Comentario: formato inválido en el archivo -> fallback
|
||||
return effective_fallback
|
||||
|
||||
|
||||
def actualizar_fecha_ultima_ejecucion(
|
||||
|
||||
@ -2,28 +2,29 @@ import logging
|
||||
import brevo_python
|
||||
|
||||
from brevo_python.rest import ApiException
|
||||
from config import setup_brevo
|
||||
from app.config import setup_brevo
|
||||
from brevo_python.rest import ApiException
|
||||
from config import load_config
|
||||
from app.config import load_config
|
||||
|
||||
|
||||
def send_orders_mail(inserted_orders):
|
||||
config = load_config()
|
||||
|
||||
|
||||
try:
|
||||
configuration = setup_brevo(config)
|
||||
api_instance = brevo_python.TransactionalEmailsApi(brevo_python.ApiClient(configuration))
|
||||
api_instance = brevo_python.TransactionalEmailsApi(brevo_python.ApiClient(configuration))
|
||||
|
||||
for order in inserted_orders:
|
||||
for order in inserted_orders:
|
||||
send_smtp_email = brevo_python.SendSmtpEmail(
|
||||
to=[{'email':config['MAIL_TO']}],
|
||||
to=[{'email': config['MAIL_TO']}],
|
||||
subject=f"Nuevo pedido del distribuidor {order["dealer_name"]}",
|
||||
template_id=int(config["BREVO_EMAIL_TEMPLATE"]),
|
||||
params={
|
||||
"customer_reference": order["customer_reference"],
|
||||
"customer_reference": order["customer_reference"],
|
||||
"dealer_name": order["dealer_name"]
|
||||
},
|
||||
)
|
||||
api_response = api_instance.send_transac_email(send_smtp_email)
|
||||
)
|
||||
api_response = api_instance.send_transac_email(send_smtp_email)
|
||||
logging.info(msg=api_response)
|
||||
except ApiException as e:
|
||||
logging.error(msg=e)
|
||||
|
||||
5
cronjob
5
cronjob
@ -1 +1,6 @@
|
||||
*/5 * * * * /usr/local/bin/python /opt/uecko_sync_app/app/main.py >> /var/log/cron.log 2>&1
|
||||
|
||||
*/5 * * * * docker run --rm \
|
||||
--env-file /opt/my_project/config/prod.env \
|
||||
-v /opt/my_project/logs:/app/logs \
|
||||
my_project:latest
|
||||
|
||||
50
enviroment/dev.env
Normal file
50
enviroment/dev.env
Normal file
@ -0,0 +1,50 @@
|
||||
ENVIRONMENT = development
|
||||
LOCAL_TZ = Europe/Madrid
|
||||
LAST_RUN_PATH = ./app.last_run.txt
|
||||
#LOG_PATH = ./app.log
|
||||
|
||||
#DESARROLLO ACANA
|
||||
#FACTUGES_HOST = 192.168.0.105
|
||||
#FACTUGES_PORT = 3050
|
||||
#FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB
|
||||
#FACTUGES_USER = sysdba
|
||||
#FACTUGES_PASSWORD = masterkey
|
||||
#CONFIGURACION ACANA
|
||||
#CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
|
||||
#VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
|
||||
|
||||
#DESARROLLO ALONSO Y SAL
|
||||
FACTUGES_HOST = 192.168.0.146
|
||||
FACTUGES_PORT = 3050
|
||||
FACTUGES_DATABASE = C:\Codigo Arribas2\Output\Debug\Database\FACTUGES.FDB
|
||||
FACTUGES_USER = sysdba
|
||||
FACTUGES_PASSWORD = masterkey
|
||||
#CONFIGURACION ACANA
|
||||
CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
|
||||
#CTE_COMPANY_ID_PRODUCCION = '019ac4f2-9502-731a-a6db-525475e85bc7'
|
||||
VERIFACTU_API_KEY = vf_test_C03HL2F0X5OXSDRunjNFoMxD4IrRfK3kCC8PfcvCENI=
|
||||
|
||||
#DESARROLLO
|
||||
UECKO_MYSQL_HOST = localhost
|
||||
UECKO_MYSQL_PORT = 3306
|
||||
UECKO_MYSQL_DATABASE = uecko_erp_sync
|
||||
UECKO_MYSQL_USER = rodax
|
||||
UECKO_MYSQL_PASSWORD = rodax
|
||||
|
||||
CTE_SERIE = 'F25/'
|
||||
CTE_STATUS_INVOICE = 'issued'
|
||||
CTE_IS_PROFORMA = 0
|
||||
CTE_STATUS_VERIFACTU = 'Pendiente'
|
||||
CTE_LANGUAGE_CODE = 'es' #En uecko vendrá de su ficha
|
||||
CTE_COUNTRY_CODE = 'es' #En uecko vendrá de su ficha
|
||||
CTE_IS_COMPANY = 1
|
||||
CTE_SYNC_RESULT_OK = 1
|
||||
CTE_SYNC_RESULT_FAIL = 2
|
||||
|
||||
VERIFACTU_BASE_URL = https://api.verifacti.com/
|
||||
VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno=
|
||||
|
||||
#BREVO_API_KEY = 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
|
||||
#BREVO_EMAIL_TEMPLATE = 1
|
||||
#MAIL_FROM = 'no-reply@presupuestos.uecko.com'
|
||||
#MAIL_TO = 'soporte@rodax-software.com'
|
||||
4
enviroment/prod.env
Normal file
4
enviroment/prod.env
Normal file
@ -0,0 +1,4 @@
|
||||
DB_HOST=prod-db
|
||||
DB_USER=prod_user
|
||||
DB_PASS=supersecret
|
||||
RUN_INTERVAL_MINUTES=5
|
||||
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
83
readme.md
83
readme.md
@ -1,83 +0,0 @@
|
||||
Instalar Python en Ubuntu (alias de Python3)
|
||||
--------------------------------------------
|
||||
sudo apt-get install python-is-python3
|
||||
sudo apt install python3.12-venv
|
||||
|
||||
Instalar el cliente de FirebirdSQL 2.0/2.1 en Ubuntu
|
||||
----------------------------------------------------
|
||||
- Opción 1. Revisar si el cliente Firebird 2.1 (libfbclient2) está en repositorios. Si aparece, se puede instalar directamente:
|
||||
sudo apt-get update
|
||||
apt-cache search firebird | grep client
|
||||
sudo apt-get install libfbclient2
|
||||
|
||||
- Opción 2. No aparece en el repositorio de Ubuntu. Instalación manual -> investigar
|
||||
|
||||
|
||||
Crear el entorno por primera vez:
|
||||
---------------------------------
|
||||
python -m venv venv
|
||||
source venv/bin/activate <-- en linux
|
||||
.\venv\Scripts\activat <-- en Windows
|
||||
|
||||
Meter librerias requeridas al entorno creado
|
||||
--------------------------------------------
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
|
||||
Lanzar el entorno para hacer pruebas del script:
|
||||
-----------------------------------------------
|
||||
Linux:
|
||||
source venv/bin/activate
|
||||
python app/main.py
|
||||
|
||||
Windows:
|
||||
.\venv\Scripts\activat
|
||||
python app\main.py
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
git clone ssh://git@wopr.rodax-software.com:30001/uecko/presupuestador-web---scripts-sync.git uecko-sync-scripts
|
||||
cd uecko-sync-scripts/
|
||||
cp .env-sample .env
|
||||
pip install -r requirements.txt
|
||||
python3 -m venv env
|
||||
sudo apt install python3.11-venv
|
||||
python3 -m venv env
|
||||
source venv/bin/activate
|
||||
pip3 install -r requirements.txt
|
||||
python3 factuges_catalog_to_json_file.py
|
||||
|
||||
> Reconstruir imagen docker
|
||||
docker compose up --build -d
|
||||
|
||||
|
||||
> Instalar Firebird 2.1
|
||||
1. Descargar paquete: https://master.dl.sourceforge.net/project/firebird/firebird-linux-amd64/2.1.7-Release/FirebirdSS-2.1.7.18553-0.amd64.tar.gz?viasf=1
|
||||
2. Descomprimir: tar -xvf <paquete>
|
||||
3. Lanzar instalación: sudo ./install.sh
|
||||
4. Si da error, da igual. El caso es que en /opt/firebird estén los ficheros y en <lib> las librerías.
|
||||
5. Crear enlaces simbólicos:
|
||||
Busque la librería libfbclient.so.2.m.n (m.n es el nro. menor de versión más el nro. de actualización) en /opt/firebird/lib del equipo donde está instalado el servidor Firebird. Cópiela a /usr/lib en el cliente.
|
||||
|
||||
Cree enlaces simbólicos usando los siguientes comandos:
|
||||
|
||||
ln -s /usr/lib/libfbclient.so.2.m.n /usr/lib/libfbclient.so.2
|
||||
|
||||
ln -s /usr/lib/libfbclient.so.2 /usr/lib/libfbclient.so
|
||||
|
||||
reemplazando 2.m.n con su número de versión, por ejemplo 2.1.7
|
||||
|
||||
Si Ud. está ejecutando aplicaciones que esperan que las librerías antiguas estén presentes, cree también los siguientes enlaces simbólicos:
|
||||
|
||||
ln -s /usr/lib/libfbclient.so /usr/lib/libgds.so.0
|
||||
|
||||
ln -s /usr/lib/libfbclient.so /usr/lib/libgds.so
|
||||
|
||||
Copie el archivo firebird.msg a /opt/firebird
|
||||
|
||||
En el perfil por defecto del sistema, o usando setenv() desde una consola, cree la variable de entorno FIREBIRD y apúntela al directorio /opt/firebird, para permitir a las rutinas de la API localizar los mensajes.
|
||||
Para ello, editar con sudo nano /etc/profile y añadir FIREBIRD=/opt/firebird
|
||||
|
||||
6. sudo apt-get install libncurses5
|
||||
@ -1,27 +0,0 @@
|
||||
about-time==4.2.1
|
||||
alive-progress==3.1.5
|
||||
bcrypt==4.1.3
|
||||
black==24.8.0
|
||||
brevo-python==1.1.2
|
||||
certifi==2024.8.30
|
||||
cffi==1.16.0
|
||||
click==8.1.7
|
||||
colorama==0.4.6
|
||||
cryptography==42.0.8
|
||||
fdb==2.0.2
|
||||
future==0.18.3
|
||||
grapheme==0.6.0
|
||||
mypy-extensions==1.0.0
|
||||
mysql-connector-python==8.4.0
|
||||
packaging==24.1
|
||||
paramiko==3.4.0
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
psutil==6.0.0
|
||||
pycparser==2.22
|
||||
PyNaCl==1.5.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.0
|
||||
six==1.16.0
|
||||
sshtunnel==0.4.0
|
||||
urllib3==2.2.3
|
||||
113
scripts/build.sh
Executable file
113
scripts/build.sh
Executable file
@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_VERSION="1.0.0"
|
||||
|
||||
# ================================================
|
||||
# FACTUGES SYNC - Docker Build Script
|
||||
# -----------------------------------------------
|
||||
# Uso:
|
||||
# ./build.sh <company> [--load]
|
||||
# ================================================
|
||||
|
||||
# ---------- 1. Validación de parámetros ----------
|
||||
if [[ $# -eq 0 || "$1" == --* ]]; then
|
||||
echo "❌ ERROR: Falta el parámetro <company>"
|
||||
echo "Uso: ./build.sh <company> [--load]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMPANY="$1"
|
||||
LOAD=false
|
||||
|
||||
if [[ "${2:-}" == "--load" ]]; then
|
||||
LOAD=true
|
||||
fi
|
||||
|
||||
# ---------- 2. Directorios ----------
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(realpath "${SCRIPT_DIR}/..")"
|
||||
OUT_DIR="${PROJECT_DIR}/out/${COMPANY}"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# ---------- 3. Versión del proyecto ----------
|
||||
IMAGE_NAME="factuges-sync"
|
||||
|
||||
IMAGE_VERSION=$(sed -n 's/^version[[:space:]]*=[[:space:]]*\(.*\)$/\1/p' setup.cfg | head -n1)
|
||||
IMAGE_VERSION="${IMAGE_VERSION:-0.0.0}"
|
||||
|
||||
IMAGE_TAG_VERSION="${IMAGE_NAME}:${COMPANY}-${IMAGE_VERSION}"
|
||||
IMAGE_TAG_LATEST="${IMAGE_NAME}:${COMPANY}-latest"
|
||||
|
||||
# ---------- 4. Info del sistema ----------
|
||||
DATE=$(date +'%Y%m%d-%H%M%S')
|
||||
ISO_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
USER_NAME=$(whoami)
|
||||
|
||||
echo ""
|
||||
echo "-------------------------------------------------------"
|
||||
echo " FACTUGES SYNC Build Script v${SCRIPT_VERSION}"
|
||||
echo " Compañía: ${COMPANY}"
|
||||
echo " Version: ${IMAGE_VERSION}"
|
||||
echo " Imagen: ${IMAGE_TAG_VERSION}"
|
||||
echo " Latest tag: ${IMAGE_TAG_LATEST}"
|
||||
echo " Load: ${LOAD}"
|
||||
echo "-------------------------------------------------------"
|
||||
echo ""
|
||||
|
||||
# ---------- 5. Build de la imagen Docker ----------
|
||||
echo "📦 Construyendo imagen Docker..."
|
||||
docker build \
|
||||
-t "${IMAGE_TAG_VERSION}" \
|
||||
-t "${IMAGE_TAG_LATEST}" \
|
||||
.
|
||||
|
||||
echo "✔ Build OK: ${IMAGE_TAG_VERSION}"
|
||||
echo ""
|
||||
|
||||
# ---------- 6. Generar manifest JSON ----------
|
||||
MANIFEST_FILE="${OUT_DIR}/manifest-${IMAGE_VERSION}-${DATE}.json"
|
||||
|
||||
cat > "$MANIFEST_FILE" <<EOF
|
||||
{
|
||||
"company": "${COMPANY}",
|
||||
"image_version": "${IMAGE_VERSION}",
|
||||
"image_tag_version": "${IMAGE_TAG_VERSION}",
|
||||
"image_tag_latest": "${IMAGE_TAG_LATEST}",
|
||||
"build_time": "${ISO_DATE}",
|
||||
"user": "${USER_NAME}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "📄 Manifest generado: ${MANIFEST_FILE}"
|
||||
echo ""
|
||||
|
||||
# ---------- 7. LOAD opcional ----------
|
||||
if [[ "$LOAD" == true ]]; then
|
||||
echo "📥 Subiendo imagen al servidor..."
|
||||
|
||||
TAR_FILE="${OUT_DIR}/${IMAGE_NAME}-${COMPANY}-latest.tar"
|
||||
docker save -o "${TAR_FILE}" "${IMAGE_TAG_LATEST}"
|
||||
|
||||
scp -P 49152 "${TAR_FILE}" \
|
||||
rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/
|
||||
|
||||
ssh -p 49152 rodax@vps-2.rodax-software.com \
|
||||
"docker load -i /opt/factuges/${COMPANY}/$(basename "$TAR_FILE")"
|
||||
|
||||
echo "✔ Imagen cargada en producción"
|
||||
fi
|
||||
|
||||
# ---------- 8. Resumen ----------
|
||||
echo ""
|
||||
echo "-------------------------------------------------------"
|
||||
echo "🎯 Resultado final para '${COMPANY}'"
|
||||
echo "✔ Build OK: ${IMAGE_TAG_VERSION}"
|
||||
if [[ "$LOAD" == true ]]; then
|
||||
echo "✔ Load OK"
|
||||
fi
|
||||
echo "🧩 Script version: ${SCRIPT_VERSION}"
|
||||
echo "-------------------------------------------------------"
|
||||
echo ""
|
||||
19
scripts/release.sh
Normal file
19
scripts/release.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Comentario: genera el siguiente tag semántico automáticamente
|
||||
# Formato: major.minor.patch
|
||||
|
||||
CURRENT=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
|
||||
|
||||
IFS='.' read -r MAJ MIN PAT <<< "$CURRENT"
|
||||
|
||||
# Incremento patch por defecto
|
||||
PAT=$((PAT + 1))
|
||||
|
||||
NEXT="${MAJ}.${MIN}.${PAT}"
|
||||
|
||||
echo "Creating release tag: ${NEXT}"
|
||||
|
||||
git tag "${NEXT}"
|
||||
git push origin "${NEXT}"
|
||||
70
setup.cfg
Normal file
70
setup.cfg
Normal file
@ -0,0 +1,70 @@
|
||||
[metadata]
|
||||
name = factuges-sync
|
||||
version = 0.1.0
|
||||
description = ETL job to sync data from legacy DB to MariaDB
|
||||
author = Rodax Software
|
||||
author_email = info@rodax-software.com
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://factuges.app
|
||||
license = "Propietaria"
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.11
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: OS Independent
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
package_dir =
|
||||
= .
|
||||
python_requires = >=3.11
|
||||
install_requires =
|
||||
about-time==4.2.1
|
||||
alive-progress==3.1.5
|
||||
bcrypt==4.1.3
|
||||
brevo-python==1.1.2
|
||||
certifi==2024.8.30
|
||||
cffi==1.16.0
|
||||
click==8.1.7
|
||||
colorama==0.4.6
|
||||
cryptography==42.0.8
|
||||
fdb==2.0.2
|
||||
future==0.18.3
|
||||
grapheme==0.6.0
|
||||
mysql-connector-python==8.4.0
|
||||
paramiko==3.4.0
|
||||
psutil==6.0.0
|
||||
pycparser==2.22
|
||||
PyNaCl==1.5.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.0
|
||||
six==1.16.0
|
||||
sshtunnel==0.4.0
|
||||
urllib3==2.2.3
|
||||
uuid6
|
||||
|
||||
[options.extras_require]
|
||||
dev =
|
||||
black==24.8.0
|
||||
mypy-extensions==1.0.0
|
||||
packaging==24.1
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
factuges-sync = app.cli:main
|
||||
|
||||
[options.packages.find]
|
||||
where = .
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
exclude = .git,__pycache__,build,dist
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
Loading…
Reference in New Issue
Block a user