commit inicial

This commit is contained in:
David Arranz 2025-08-28 10:51:05 +02:00
commit 2ae6a66a4e
32 changed files with 3719 additions and 0 deletions

38
.env.development Normal file
View File

@ -0,0 +1,38 @@
ENVIRONMENT = development
LOCAL_TZ = Europe/Madrid
#LOG_PATH = ./app.log
FACTUGES_HOST = 192.168.0.135
FACTUGES_PORT = 3050
FACTUGES_DATABASE = C:\Codigo\Output\Debug\Database\FACTUGES.FDB
FACTUGES_USER = sysdba
FACTUGES_PASSWORD = masterkey
FACTUGES_ID_EMPRESA = 1
FACTUGES_CONTRATO_ID_TIENDA = 1
FACTUGES_CONTRATO_SITUACION = "PENDIENTE"
FACTUGES_CONTRATO_ENVIADA_REVISADA = 10
FACTUGES_CONTRATO_TIPO_DETALLE = "Concepto"
FACTUGES_NOMBRE_TARIFA = TARIFA 2024
FACTUGES_PRECIO_PUNTO = 3.31
UECKO_MYSQL_HOST = 192.168.0.116
UECKO_MYSQL_PORT = 3306
UECKO_MYSQL_DATABASE = uecko
UECKO_MYSQL_USER = rodax
UECKO_MYSQL_PASSWORD = rodax
UECKO_DEFAULT_IVA = 2100
UECKO_DEFAULT_CURRENCY_CODE = EUR
UECKO_DEFAULT_VALIDEZ = "30 días"
UECKO_DEFAULT_LOPD = ""
UECKO_DEFAULT_NOTAS = ""
UECKO_DEFAULT_FORMA_PAGO = "50% a la aceptación y 50% a la finalización"
BREVO_API_KEY = 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
BREVO_EMAIL_TEMPLATE = 1
MAIL_FROM = 'no-reply@presupuestos.uecko.com'
MAIL_TO = 'soporte@rodax-software.com'

36
.env.production Normal file
View File

@ -0,0 +1,36 @@
ENVIRONMENT = production
LOCAL_TZ = Europe/Madrid
#LOG_PATH = /var/log/uecko_sync_app/uecko_sync_app.log
FACTUGES_HOST = 83.48.36.69
FACTUGES_PORT = 3050
FACTUGES_DATABASE = D:\RODAX\FACTUGES\BD\FACTUGES_FABRICA.FDB
FACTUGES_USER = sysdba
FACTUGES_PASSWORD = abeto2010
FACTUGES_ID_EMPRESA = 1
FACTUGES_CONTRATO_ID_TIENDA = 1
FACTUGES_CONTRATO_SITUACION = "PENDIENTE"
FACTUGES_CONTRATO_ENVIADA_REVISADA = 10
FACTUGES_CONTRATO_TIPO_DETALLE = "Concepto"
FACTUGES_NOMBRE_TARIFA = TARIFA 2024
FACTUGES_PRECIO_PUNTO = 3.31
UECKO_MYSQL_HOST = mariadb
UECKO_MYSQL_PORT = 3306
UECKO_MYSQL_DATABASE = uecko
UECKO_MYSQL_USER = uecko
UECKO_MYSQL_PASSWORD = u8Ax5Nw3%sjd
UECKO_DEFAULT_IVA = 2100
UECKO_DEFAULT_CURRENCY_CODE = EUR
UECKO_DEFAULT_VALIDEZ = "30 días"
UECKO_DEFAULT_LOPD = ""
UECKO_DEFAULT_NOTAS = ""
UECKO_DEFAULT_FORMA_PAGO = "50% a la aceptación y 50% a la finalización"
BREVO_API_KEY = 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
BREVO_EMAIL_TEMPLATE = 1
MAIL_FROM = 'no-reply@presupuestos.uecko.com'
MAIL_TO = 'pedidos@uecko.com'

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
venv/
__pycache__
.env
input/
output/
FACTUGES.FDB
last_execution*.txt
*.json
*.log

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"bracketSpacing": true,
"useTabs": false,
"printWidth": 100,
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"rcVerbose": true
}

38
Dockerfile Normal file
View File

@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1.4
# Usa una imagen base de Python
FROM python:3.12.6-slim-bookworm AS python_script
RUN apt-get update
RUN apt-get install libfbclient2 -y
# Establece el directorio de trabajo dentro del contenedor
WORKDIR /opt/uecko_sync_app
# Copia los archivos del proyecto al contenedor
COPY . .
COPY ./.env.production ./.env
# Instala las dependencias de Python
RUN pip install --no-cache-dir -r requirements.txt
# Instala cron en el contenedor
RUN apt-get update && apt-get install -y cron nano
# 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

43
Dockerfile.firebird Normal file
View File

@ -0,0 +1,43 @@
# syntax=docker/dockerfile:1.4
FROM debian:bookworm-slim AS build
ENV FIREBIRD_PATH=/opt/firebird
ENV FIREBIRD_DB_PASSWORD=masterkey
ENV FIREBIRD_DB_PASSWORD_DEFAULT=masterkey
RUN apt-get update
RUN apt-get install wget -y
RUN apt-get install libstdc++5 -y
RUN apt-get install xinetd -y
RUN wget http://sourceforge.net/projects/firebird/files/firebird-linux-amd64/2.1.7-Release/FirebirdCS-2.1.7.18553-0.amd64.tar.gz
RUN tar -vzxf FirebirdCS-2.1.7.18553-0.amd64.tar.gz
RUN rm FirebirdCS-2.1.7.18553-0.amd64.tar.gz
RUN rm FirebirdCS-2.1.7.18553-0.amd64/install.sh
RUN rm FirebirdCS-2.1.7.18553-0.amd64/scripts/postinstall.sh
COPY ./firebird/install.sh FirebirdCS-2.1.7.18553-0.amd64
COPY ./firebird/postinstall.sh FirebirdCS-2.1.7.18553-0.amd64/scripts
# Otorgar permisos de ejecución al script install.sh
RUN chmod +x FirebirdCS-2.1.7.18553-0.amd64/install.sh
RUN chmod +x FirebirdCS-2.1.7.18553-0.amd64/scripts/postinstall.sh
RUN cd FirebirdCS-2.1.7.18553-0.amd64 && ./install.sh ${FIREBIRD_DB_PASSWORD_DEFAULT}
RUN rm -r FirebirdCS-2.1.7.18553-0.amd64
COPY ./firebird/fbudflib2.so ${FIREBIRD_PATH}/UDF
COPY ./firebird/launch.sh ${FIREBIRD_PATH}
RUN chmod +x ${FIREBIRD_PATH}/launch.sh
RUN cd ${FIREBIRD_PATH} && mkdir DBA && chown firebird:firebird DBA && chmod -R 770 DBA
RUN cd / && mkdir dba && chown firebird:firebird dba && chmod -R 770 dba
#RUN cp ${FIREBIRD_PATH}/lib/libfbclient.so.2.1.7 /usr/lib/libfbclient.so.2.1.7
#RUN ln -s /usr/lib/libfbclient.so.2.1.7 /usr/lib/libfbclient.so.2
#RUN ln -s /usr/lib//usr/lib/libfbclient.so.2 /usr/lib/libfbclient.so
EXPOSE 3050/tcp
WORKDIR ${FIREBIRD_PATH}
ENTRYPOINT ${FIREBIRD_PATH}/launch.sh

0
app/__init__.py Normal file
View File

1
app/__version__.py Normal file
View File

@ -0,0 +1 @@
__version__ = "1.0.8"

3
app/config/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .settings import load_config
from .setup_logging import setup_logging
from .setup_brevo import setup_brevo

46
app/config/settings.py Normal file
View File

@ -0,0 +1,46 @@
import os
from os.path import join, dirname
from dotenv import load_dotenv
def load_config():
dotenv_path = join(dirname(__file__), '../../.env')
load_dotenv(dotenv_path)
return {
'ENVIRONMENT': os.getenv('ENVIRONMENT'),
'LOCAL_TZ': os.getenv('LOCAL_TZ', 'Europe/Madrid'),
# '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'),
'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'),
'FACTUGES_ID_EMPRESA': os.getenv('FACTUGES_ID_EMPRESA'),
'FACTUGES_PRECIO_PUNTO': os.getenv('FACTUGES_PRECIO_PUNTO'),
'FACTUGES_NOMBRE_TARIFA': os.getenv('FACTUGES_NOMBRE_TARIFA'),
'FACTUGES_CONTRATO_ID_TIENDA': os.getenv('FACTUGES_CONTRATO_ID_TIENDA'),
'FACTUGES_CONTRATO_SITUACION': os.getenv('FACTUGES_CONTRATO_SITUACION'),
'FACTUGES_CONTRATO_ENVIADA_REVISADA': os.getenv('FACTUGES_CONTRATO_ENVIADA_REVISADA'),
'FACTUGES_CONTRATO_TIPO_DETALLE': os.getenv('FACTUGES_CONTRATO_TIPO_DETALLE'),
'UECKO_DEFAULT_IVA': os.getenv('UECKO_IVA', 2100),
'UECKO_DEFAULT_CURRENCY_CODE': os.getenv('UECKO_CURRENCY_CODE', "EUR"),
'UECKO_DEFAULT_VALIDEZ': os.getenv('UECKO_DEFAULT_VALIDEZ', ""),
'UECKO_DEFAULT_LOPD': os.getenv('UECKO_DEFAULT_LOPD', ""),
'UECKO_DEFAULT_NOTAS': os.getenv('UECKO_DEFAULT_NOTAS', ""),
'UECKO_DEFAULT_FORMA_PAGO': os.getenv('UECKO_DEFAULT_FORMA_PAGO', ""),
'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'),
}

10
app/config/setup_brevo.py Normal file
View File

@ -0,0 +1,10 @@
from __future__ import print_function
import brevo_python
def setup_brevo(config):
# Configure API key authorization: api-key
configuration = brevo_python.Configuration()
configuration.api_key['api-key'] = config['BREVO_API_KEY']
return configuration

View File

@ -0,0 +1,18 @@
import logging
import sys
from logging.handlers import RotatingFileHandler
def setup_logging():
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def setup_rotating_logging(log_path):
# Rotación de logs con un tamaño máximo de 5 MB y mantiene 15 archivos de backup
handler = RotatingFileHandler(log_path, maxBytes=5*1024*1024, backupCount=15, encoding="utf8")
# Configuración básica de logging
logging.basicConfig(
handlers=[handler],
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)

5
app/db/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from .db_connection import get_factuges_connection
from .db_connection import get_mysql_connection
from .sync_catalog import sync_catalog
from .sync_dealers import sync_dealers
from .sync_orders import sync_orders

44
app/db/db_connection.py Normal file
View File

@ -0,0 +1,44 @@
import fdb
import mysql.connector
import logging
def get_factuges_connection(config):
try:
conn = fdb.connect(
host=config['FACTUGES_HOST'],
port=int(config['FACTUGES_PORT']),
database=config['FACTUGES_DATABASE'],
user=config['FACTUGES_USER'],
password=config['FACTUGES_PASSWORD'],
charset='UTF8'
)
logging.info(
f"Conexión a la base de datos FactuGES establecida: {config['FACTUGES_HOST']} with database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
return conn
except Exception as e:
logging.error("Error al conectar a la base de datos FactuGES.")
logging.error(
f"(ERROR) Failed to establish connection to: {config['FACTUGES_HOST']} with database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
logging.error(str(e))
raise e
def get_mysql_connection(config):
try:
conn = mysql.connector.connect(
host=config['UECKO_MYSQL_HOST'],
port=config['UECKO_MYSQL_PORT'],
database=config['UECKO_MYSQL_DATABASE'],
user=config['UECKO_MYSQL_USER'],
password=config['UECKO_MYSQL_PASSWORD']
)
logging.info(
f"Conexión a la base de datos MySQL establecida a: {config['UECKO_MYSQL_HOST']} with database:{config['UECKO_MYSQL_DATABASE']} - using user:{config['UECKO_MYSQL_USER']}")
return conn
except Exception as e:
logging.error("Error al conectar a la base de datos MySQL.")
logging.error(
f"(ERROR) Failed to establish connection to: {config['UECKO_MYSQL_HOST']} with database:{config['UECKO_MYSQL_DATABASE']} - using user:{config['UECKO_MYSQL_USER']}")
logging.error(str(e))
raise e

219
app/db/sync_catalog.py Normal file
View File

@ -0,0 +1,219 @@
import logging
from uuid import uuid4
from config import load_config
def sync_catalog(conn_factuges, conn_mysql, last_execution_date):
config = load_config()
logging.info(f"Tarifa: {config['FACTUGES_NOMBRE_TARIFA']}")
logging.info(f"Precio punto: {config['FACTUGES_PRECIO_PUNTO']}")
# Construir la consulta SQL con la condición de fecha de modificación
consulta_sql_art_modificados = (
f"SELECT art.id || '' AS id, art.tarifa as tarifa, COALESCE(art.referencia,'') AS referencia, "
f"TRIM(COALESCE(art.familia, '') || ' ' || COALESCE(art.referencia_prov, '') || ' ' || COALESCE(art.descripcion, '')) as descripcion_es, "
f"TRIM(COALESCE(art_idioma_en.descripcion, '')) AS descripcion_en, "
f"TRUNC(art.precio_coste * 100) || '' AS puntos, "
f"TRUNC(ROUND(art.precio_coste * {
config['FACTUGES_PRECIO_PUNTO']}, 2) * 100) || '' AS pvp "
f"FROM articulos AS art "
f"LEFT JOIN ARTICULOS_IDIOMAS AS art_idioma_en ON art.id = art_idioma_en.id_articulo AND art_idioma_en.id_idioma = 2 "
f"WHERE "
f"(art.eliminado = 0) AND "
f"(art.tarifa = '{config['FACTUGES_NOMBRE_TARIFA']}') "
f"AND (art.FECHA_MODIFICACION > '{last_execution_date}')"
)
consulta_sql_all_tarifa = (
f"SELECT art.id || '' AS id, art.tarifa as tarifa "
f"FROM articulos AS art "
f"WHERE "
f"(art.eliminado = 0) AND "
f"(art.tarifa = '{config['FACTUGES_NOMBRE_TARIFA']}') "
)
# Crear un cursor para ejecutar consultas SQL
cursor_FactuGES = None
try:
cursor_FactuGES = conn_factuges.cursor()
# Ejecutar la consulta de articulos modificados
cursor_FactuGES.execute(consulta_sql_art_modificados)
filas = cursor_FactuGES.fetchall()
except Exception as e:
if cursor_FactuGES is not None:
cursor_FactuGES.close()
logging.error(f"(ERROR) Failed to fetch from database:{
config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
logging.error(e)
raise e
# Obtener los nombres de las columnas
columnas = [desc[0] for desc in cursor_FactuGES.description]
cursor_FactuGES.close()
# Convertir las filas en diccionarios con nombres de columnas como claves
tuplas_seleccionadas = []
for fila in filas:
tupla = dict(zip(columnas, fila))
tuplas_seleccionadas.append(tupla)
logging.info(f"Catalog rows to be processed: {len(tuplas_seleccionadas)}")
# Verificar si hay filas en el resultado
if tuplas_seleccionadas:
insertar_datos(conn_mysql, tuplas_seleccionadas, config)
else:
logging.info(
"There are no new or modified catalog rows since the last run.")
# Verificamos que en el catálogo de mysql solo hay los artículos del catálogo de FactuGES
# es decir, que si un artículo lo asignan a otra tarifa debe desaparecer del catálogo mysql
try:
cursor_FactuGES.execute(consulta_sql_all_tarifa)
filas = cursor_FactuGES.fetchall()
cursor_FactuGES.close()
# Crear un conjunto con los IDs [0] de los artículos en FactuGES para una búsqueda rápida
ids_factuges = {str(fila[0]) for fila in filas}
logging.info(f"{config['FACTUGES_NOMBRE_TARIFA']} rows to be processed: {
len(ids_factuges)}")
# Verificar si hay filas en el resultado
if ids_factuges:
eliminar_datos(conn_mysql, ids_factuges, config)
else:
logging.info(f"There are no rows in the {
config['FACTUGES_NOMBRE_TARIFA']}.")
except Exception as e:
if cursor_FactuGES is not None:
cursor_FactuGES.close()
logging.error(f"(ERROR) Failed to fetch from database:{
config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
logging.error(e)
raise e
def eliminar_datos(conn_mysql, ids_factuges, config):
# Recorrer todos los articulos del catálogo web para ver si estan en filas, si no están se eliminan
select_all_catalog_query = (
"SELECT catalog.id, catalog.id_article FROM catalog"
)
delete_catalog_query = (
"DELETE FROM catalog WHERE catalog.id_article = %s"
)
cursorMySQL = None
try:
cursorMySQL = conn_mysql.cursor()
cursorMySQL.execute(select_all_catalog_query)
catalog_rows = cursorMySQL.fetchall()
logging.info(
f">>>>Comprobar que todos los artículos del catálogo existen en FactuGES")
ids_a_eliminar = [
catalog_row[1] # id_article
for catalog_row in catalog_rows
if str(catalog_row[1]) not in ids_factuges
]
if ids_a_eliminar:
logging.info(f"Deleting articles: {ids_a_eliminar}")
cursorMySQL.executemany(delete_catalog_query, [(
id_article,) for id_article in ids_a_eliminar])
else:
logging.info("No articles to delete.")
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
# Cerrar la conexión
if cursorMySQL is not None:
cursorMySQL.close()
def insertar_datos(conn_mysql, filas, config):
insert_catalog_query = (
"INSERT INTO catalog (id, catalog_name, id_article, points, retail_price, created_at, updated_at) "
"VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
)
insert_translation_query = (
"INSERT INTO catalog_translations (id, lang_code, catalog_id, description) "
"VALUES (%s, %s, %s, %s)"
)
update_catalog_query = (
"UPDATE catalog set "
"points = %s, "
"retail_price = %s, "
"updated_at = Now() "
"WHERE id_article=%s"
)
update_translation_query = (
"UPDATE catalog_translations SET "
"description = %s "
"WHERE lang_code = %s AND "
"catalog_id IN (SELECT catalog.id FROM catalog WHERE catalog.id_article = %s)"
)
select_catalog_query = (
"SELECT count(catalog.id) FROM catalog WHERE catalog.id_article = %s"
)
cursorMySQL = None
try:
cursorMySQL = conn_mysql.cursor()
# Insertar datos en la tabla 'catalog'
for articulo in filas:
# Generar un ID único para la tabla catalog
id_catalog = str(uuid4())
id_article = int(articulo['ID'])
points = int(articulo['PUNTOS'])
retail_price = int(articulo['PVP'])
tarifa = config['FACTUGES_NOMBRE_TARIFA']
cursorMySQL.execute(select_catalog_query, (id_article, ))
row_count = cursorMySQL.fetchone()
is_new = row_count[0] < 1
if is_new:
logging.info(f"Inserting article {id_article} {tarifa}")
cursorMySQL.execute(
insert_catalog_query, (id_catalog, tarifa, id_article, points, retail_price))
else:
logging.info(f"Updating article {id_article} {tarifa}")
cursorMySQL.execute(update_catalog_query,
(points, retail_price, id_article))
# Insertar traducciones en la tabla 'catalog_translations'
for lang_code, desc_key in [('es', 'DESCRIPCION_ES'), ('en', 'DESCRIPCION_EN')]:
descripcion_traducida = articulo.get(desc_key, '')
if descripcion_traducida:
if (is_new):
logging.info(f"Inserting translation {
lang_code} {descripcion_traducida}")
# Generar un ID único para cada traducción
id_translation = str(uuid4())
cursorMySQL.execute(
insert_translation_query, (id_translation, lang_code, id_catalog, descripcion_traducida))
else:
logging.info(f"Updating translation {
lang_code} {descripcion_traducida}")
cursorMySQL.execute(
update_translation_query, (descripcion_traducida, lang_code, id_article))
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
# Cerrar la conexión
if cursorMySQL is not None:
cursorMySQL.close()

320
app/db/sync_dealers.py Normal file
View File

@ -0,0 +1,320 @@
import logging
from uuid import uuid4
from config import load_config
from utils import hashPassword
def sync_dealers(conn_factuges, conn_mysql, last_execution_date):
config = load_config()
consulta_factuges = (
f"select V_CONTACTOS.ID as ID, V_CONTACTOS.NOMBRE, V_CONTACTOS.IDIOMA_ISO, "
f"CLIENTES_DATOS.DIST_EMAIL, CLIENTES_DATOS.DIST_PASSWORD, CLIENTES_DATOS.BLOQUEADO "
f"from V_CONTACTOS "
f"left OUTER JOIN CLIENTES_DATOS on (V_CONTACTOS.ID = CLIENTES_DATOS.ID_CLIENTE) "
f"where (V_CONTACTOS.ID_CATEGORIA = 1) "
f"and (V_CONTACTOS.ID_EMPRESA = '{config['FACTUGES_ID_EMPRESA']}') "
f"and (CLIENTES_DATOS.TIENDA_WEB = 1) "
f"and (V_CONTACTOS.FECHA_MODIFICACION is not null) "
f"and (V_CONTACTOS.FECHA_MODIFICACION > '{last_execution_date}')"
)
consulta_dealer_uecko = (
"SELECT dealers.id, dealers.id_contact, dealers.user_id, dealers.status, dealers.updated_at "
"FROM dealers "
"WHERE dealers.id_contact = %s"
)
cursor_FactuGES = None
try:
cursor_FactuGES = conn_factuges.cursor()
# Ejecutar la consulta
cursor_FactuGES.execute(consulta_factuges)
contactos = cursor_FactuGES.fetchall()
except Exception as e:
if cursor_FactuGES is not None:
cursor_FactuGES.close()
logging.error(f"(ERROR) Failed to fetch from database:{
config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
logging.error(e)
raise e
columnas_contacto = [desc[0] for desc in cursor_FactuGES.description]
cursor_FactuGES.close()
contactos_seleccionados = []
for contacto in contactos:
tupla = dict(zip(columnas_contacto, contacto))
contactos_seleccionados.append(tupla)
logging.info(f"Contacts rows to be processed: {
len(contactos_seleccionados)}")
if contactos_seleccionados:
for contacto in contactos_seleccionados:
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta_dealer_uecko, (contacto['ID'],))
dealer = cursor_MySQL.fetchone()
if (dealer is None):
user_id = insert_user(conn_mysql, contacto, config)
insert_dealer(conn_mysql, user_id, contacto, config)
logging.info(f"Inserted user and dealer from contact {
contacto['ID']} {contacto['NOMBRE']}")
else:
# 0 => 'ID'
# 2 => 'USER_ID'
# Casos:
# - Cambio en el nombre del distribuidor
# - Distribuidor bloqueado / desbloqueado
# - Usuario con baja lógica
id = dealer[0]
user_id = dealer[2]
update_dealer(conn_mysql, id, contacto, config)
update_user(conn_mysql, user_id, contacto, config)
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
# Cerrar la conexión
if cursor_MySQL is not None:
cursor_MySQL.close()
else:
logging.info(
"There are no new or modified contacts rows since the last run.")
# Revisar todos los distribuidores dados de alta y
# comprobar si en FactuGES siguen estando activos (tienda_web = 1)
#
# - USUARIO DISABLED
dealers = fetch_all_dealers(conn_mysql, config)
for dealer in dealers:
# dealer[1] => id_contact
if (dealer[1] is not None) and (not is_valid_dealer(conn_factuges, dealer[1], config)):
user_id = dealer[7] # 7 => user_id
# Desactivar el distribuidor
disable_dealer(conn_mysql, dealer[0], config) # 0 => id
# Baja lógica del usuario del dealer
soft_delete_user(conn_mysql, user_id, config)
logging.info(f"Deleted dealer and user from contact {dealer[1]}")
def fetch_all_dealers(conn_mysql, config):
consulta = (
f"SELECT dealers.id, dealers.id_contact, dealers.default_payment_method, dealers.default_notes, "
f"dealers.default_legal_terms, dealers.default_quote_validity, dealers.status, dealers.user_id "
f"FROM dealers "
)
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta)
return cursor_MySQL.fetchall()
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def is_valid_dealer(conn_factuges, id_contact, config):
consulta = (
f"select CLIENTES_DATOS.ID_CLIENTE from CLIENTES_DATOS where CLIENTES_DATOS.ID_CLIENTE = {
id_contact} and CLIENTES_DATOS.TIENDA_WEB = 1"
)
cursor_FactuGES = None
try:
cursor_FactuGES = conn_factuges.cursor()
cursor_FactuGES.execute(consulta)
exists = cursor_FactuGES.fetchone()
return exists is not None
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_FactuGES is not None:
cursor_FactuGES.close()
def insert_user(conn_mysql, data, config):
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
id = str(uuid4())
name = str(data['NOMBRE'])
email = str(data['DIST_EMAIL'])
password = hashPassword(str(data['DIST_PASSWORD']))
lang_code = str(data['IDIOMA_ISO'])
insert_data = (
"INSERT INTO users (id, name, email, password, lang_code, roles, created_at, updated_at) VALUES ("
"%s, %s, %s, %s, %s, 'ROLE_USER', Now(), Now()"
")"
)
cursor_MySQL.execute(
insert_data, (id, name, email, password, lang_code))
return id
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def update_user(conn_mysql, user_id, data, config):
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
name = str(data['NOMBRE'])
email = str(data['DIST_EMAIL'])
password = hashPassword(str(data['DIST_PASSWORD']))
lang_code = str(data['IDIOMA_ISO'])
update_data = (
"UPDATE users set "
"name = %s, "
"email = %s, "
"password = %s, "
"lang_code = %s, "
"updated_at = Now(), "
"deleted_at = NULL "
"WHERE id = %s"
)
cursor_MySQL.execute(
update_data, (name, email, password, lang_code, user_id))
logging.info(f"Updated user from contact {data['ID']} {name}")
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def insert_dealer(conn_mysql, user_id, data, config):
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
id = str(uuid4())
id_contact = str(data['ID'])
name = str(data['NOMBRE'])
default_payment_method = str(config['UECKO_DEFAULT_FORMA_PAGO'])
default_notes = str(config['UECKO_DEFAULT_NOTAS'])
default_legal_terms = str(config['UECKO_DEFAULT_LOPD'])
default_quote_validity = str(config['UECKO_DEFAULT_VALIDEZ'])
default_tax = str(config["UECKO_DEFAULT_IVA"])
lang_code = str(data['IDIOMA_ISO'])
currency_code = str(config["UECKO_DEFAULT_CURRENCY_CODE"])
insert_data = (
"INSERT INTO dealers (id, id_contact, name, default_payment_method, default_notes, "
"default_legal_terms, default_quote_validity, default_tax, lang_code, "
"currency_code, user_id, status, created_at, updated_at ) values ("
"%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'actived', Now(), Now()"
")"
)
cursor_MySQL.execute(insert_data, (id, id_contact, name, default_payment_method, default_notes,
default_legal_terms, default_quote_validity, default_tax, lang_code, currency_code, user_id))
return id
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def update_dealer(conn_mysql, dealer_id, data, config):
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
name = str(data['NOMBRE'])
status = 'disabled' if data['BLOQUEADO'] == 1 else 'actived'
insert_data = (
"UPDATE dealers SET name = %s, status = %s WHERE dealers.id = %s"
)
cursor_MySQL.execute(insert_data, (name, status, dealer_id))
logging.info(f"Dealer with id = {dealer_id} name = {name} updated")
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def soft_delete_user(conn_mysql, user_id, config):
consulta_sql = "UPDATE users SET users.deleted_at = NOW() WHERE users.id = %s AND users.roles = 'ROLE_USER'"
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta_sql, (user_id, ))
logging.info(f"User with id = {id} soft delete")
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def active_dealer(conn_mysql, id, config):
consulta_sql = "UPDATE dealers SET status = 'actived' WHERE dealers.id = %s"
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta_sql, (id, ))
logging.info(f"Dealer with id = {id} actived")
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def disable_dealer(conn_mysql, id, config):
consulta_sql = "UPDATE dealers SET status = 'disabled' WHERE dealers.id = %s"
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta_sql, (id, ))
logging.info(f"Dealer with id = {id} disabled")
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(str(e))
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()

216
app/db/sync_orders.py Normal file
View File

@ -0,0 +1,216 @@
import logging
from decimal import Decimal
from config import load_config
from utils import text_converter
def sync_orders(conn_factuges, conn_mysql):
config = load_config()
consulta_quotes_uecko = (
"SELECT quotes.id, quotes.date_sent, quotes.reference, quotes.customer_reference, "
"quotes.customer_information, quotes.dealer_id, dealers.id_contact, dealers.name "
"FROM quotes INNER JOIN dealers ON (dealers.id = quotes.dealer_id) "
"WHERE quotes.date_sent IS NOT NULL AND "
"quotes.id_contract IS NULL"
)
update_quotes_uecko = (
"UPDATE quotes SET "
"id_contract = %s "
"WHERE id = %s"
)
inserted_orders = []
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta_quotes_uecko)
quotes = cursor_MySQL.fetchall()
quote_columns = [desc[0] for desc in cursor_MySQL.description]
selected_quotes = []
for quote in quotes:
tupla = dict(zip(quote_columns, quote))
selected_quotes.append(tupla)
logging.info(f"Quotes rows to be processed: {len(selected_quotes)}")
if selected_quotes:
for quote in selected_quotes:
logging.info(f"Quote reference: {quote['reference']}")
if (quote['id_contact'] is None):
logging.info(
f"Error: Quote unprocesable (id_contact missing)")
continue
items = fetch_quote_items(conn_mysql, quote['id'])
id_contrato = insert_quote_to_factuges(
conn_factuges, quote, items, config)
cursor_MySQL.execute(update_quotes_uecko,
(int(id_contrato), str(quote['id'])))
inserted_orders.append({
"customer_reference": quote['customer_reference'],
"dealer_name": quote['name'],
})
cursor_MySQL.close()
return inserted_orders
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(msg=e, stack_info=True)
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
# Cerrar la conexión
if cursor_MySQL is not None:
cursor_MySQL.close()
def fetch_quote_items(conn_mysql, quote_id):
consulta_quotes_items_uecko = (
"SELECT quote_items.item_id, quote_items.id_article, quote_items.position, "
"quote_items.description, quote_items.quantity, quote_items.unit_price, "
"quote_items.discount, quote_items.total_price "
"FROM quote_items "
"WHERE quote_items.quote_id = %s "
"ORDER BY quote_items.position"
)
cursor_MySQL = None
try:
cursor_MySQL = conn_mysql.cursor()
cursor_MySQL.execute(consulta_quotes_items_uecko, (quote_id, ))
items = cursor_MySQL.fetchall()
items_columns = [desc[0] for desc in cursor_MySQL.description]
cursor_MySQL.close()
selected_items = []
for item in items:
tupla = dict(zip(items_columns, item))
selected_items.append(tupla)
return selected_items
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(msg=e, stack_info=True)
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_MySQL is not None:
cursor_MySQL.close()
def insert_quote_to_factuges(conn_factuges, quote, items, config):
id_empresa = int(config['FACTUGES_ID_EMPRESA'])
situacion = str(config['FACTUGES_CONTRATO_SITUACION'])
id_tienda = int(config['FACTUGES_CONTRATO_ID_TIENDA'])
enviada_revisada = int(config['FACTUGES_CONTRATO_ENVIADA_REVISADA'])
id_cliente = int(quote['id_contact'])
# nombre_clliente = str(quote['name'])
fecha_presupuesto = quote['date_sent'].date()
persona_contacto = str(quote['customer_information'])
referencia_cliente = str(quote['customer_reference'])
select_gen_id_contrato_cliente = (
"select GEN_ID(GEN_CONTRATOS_CLI_ID, 1) from RDB$DATABASE"
)
select_gen_id_presupuesto_cliente = (
"select GEN_ID(GEN_PRESUPUESTOS_CLI_ID, 1) from RDB$DATABASE"
)
insert_contrato_cliente_data = (
"insert into CONTRATOS_CLIENTE ("
"ID, ID_EMPRESA, ID_TIENDA, ID_CLIENTE, NOMBRE, SITUACION, "
"NOTAS_ENVIO, REFERENCIA_CLIENTE, "
"ENVIADA_REVISADA, FECHA_CONTRATO "
") values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
insert_presupuesto_cliente_data = (
"insert into PRESUPUESTOS_CLIENTE ("
"ID, ID_EMPRESA, ID_TIENDA, ID_CLIENTE, SITUACION, "
"OBSERVACIONES, REFERENCIA_CLIENTE, "
"ENVIADA_REVISADA, FECHA_PRESUPUESTO "
") values (?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
insert_contrato_cliente_detalles_data = (
"insert into CONTRATOS_CLIENTE_DETALLES ("
"ID, ID_CONTRATO, POSICION, ID_ARTICULO, TIPO_DETALLE, "
"CONCEPTO, CANTIDAD, IMPORTE_UNIDAD, "
"VALORADO, VISIBLE, FECHA_ALTA "
") values ("
"GEN_ID(GEN_CONTRATOS_CLI_DETALLE_ID, 1), ?, ?, ?, ?, "
"?, ?, ?, "
"1, 1, CURRENT_TIMESTAMP"
")"
)
insert_presupuesto_cliente_detalles_data = (
"insert into PRESUPUESTOS_CLIENTE_DETALLES ("
"ID, ID_PRESUPUESTO, POSICION, ID_ARTICULO, TIPO_DETALLE, "
"CONCEPTO, CANTIDAD, IMPORTE_UNIDAD, "
"VALORADO, VISIBLE, FECHA_ALTA "
") values ("
"GEN_ID(GEN_PRESUPUESTOS_CLI_DETALLE_ID, 1), ?, ?, ?, ?, "
"?, ?, ?, "
"1, 1, CURRENT_TIMESTAMP"
")"
)
cursor_FactuGES = None
try:
cursor_FactuGES = conn_factuges.cursor()
cursor_FactuGES.execute(select_gen_id_presupuesto_cliente)
id_presupuesto = int(cursor_FactuGES.fetchone()[0])
logging.info(
f"Inserting quote on FactuGES -> id_preupuesto = {str(id_presupuesto)}")
logging.info(insert_presupuesto_cliente_data)
logging.info((id_presupuesto, id_empresa, id_tienda, id_cliente,
situacion, fecha_presupuesto, persona_contacto,
referencia_cliente, enviada_revisada, fecha_presupuesto))
cursor_FactuGES.execute(insert_presupuesto_cliente_data,
(id_presupuesto, id_empresa, id_tienda, id_cliente,
situacion, persona_contacto,
referencia_cliente, enviada_revisada, fecha_presupuesto))
logging.info(
f"Inserting items. Quote items length to be processed: {len(items)}")
for item in items:
descripcion_iso = text_converter(
item['description'], charset_destino='ISO8859_1', longitud_maxima=2000)
quantity = Decimal(
int(item['quantity'])) / Decimal(100) if item['quantity'] is not None else None
unit_price = Decimal(int(
item['unit_price'])) / Decimal(100) if item['unit_price'] is not None else None
# total_price = item['total_price']
logging.info(str(insert_presupuesto_cliente_detalles_data))
logging.info((
id_presupuesto, item['position'], item['id_article'], config['FACTUGES_CONTRATO_TIPO_DETALLE'],
descripcion_iso, quantity, unit_price
))
cursor_FactuGES.execute(insert_presupuesto_cliente_detalles_data, (
id_presupuesto, item['position'], item['id_article'], config['FACTUGES_CONTRATO_TIPO_DETALLE'],
descripcion_iso, quantity, unit_price
))
cursor_FactuGES.close()
return id_presupuesto
except Exception as e:
# Escribir el error en el archivo de errores
logging.error(msg=e, stack_info=True)
raise e # Re-lanzar la excepción para detener el procesamiento
finally:
if cursor_FactuGES is not None:
cursor_FactuGES.close()

82
app/main.py Normal file
View File

@ -0,0 +1,82 @@
import sys
import logging
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_catalog, sync_dealers, sync_orders
from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail
def main():
# Cargar la configuración
config = load_config()
local_tz = tz.gettz(config['LOCAL_TZ'])
# Logging
setup_logging()
logging.info("== START ==")
logging.info(f"Version: {__version__.__version__}")
logging.info(f"Environment: {config['ENVIRONMENT']}")
log_system_metrics()
conn_factuges = None
conn_mysql = None
try:
# Obtener la fecha de la última ejecución del programa
last_execution_date_utc = obtener_fecha_ultima_ejecucion()
last_execution_date_local_tz = last_execution_date_utc.astimezone(
tz=local_tz).strftime("%Y-%m-%d %H:%M:%S")
logging.info("Last execution (UTC): %s",
last_execution_date_utc.strftime("%Y-%m-%d %H:%M:%S %Z"))
logging.info("Last execution (Local time): %s",
last_execution_date_local_tz)
conn_factuges = get_factuges_connection(config)
conn_mysql = get_mysql_connection(config)
# Sync catalog
sync_catalog(conn_factuges, conn_mysql, last_execution_date_local_tz)
sync_dealers(conn_factuges, conn_mysql, last_execution_date_local_tz)
inserted_orders = sync_orders(
conn_factuges, conn_mysql)
actualizar_fecha_ultima_ejecucion()
# Confirmar los cambios
conn_mysql.commit()
conn_factuges.commit()
# Enviar email
send_orders_mail(inserted_orders)
logging.info("== END (0) ==")
sys.exit(0)
except Exception as e:
logging.error("Se ha producido un error en la última ejecución.")
logging.error(e)
logging.info("== END (1) ==")
if conn_mysql is not None:
conn_mysql.rollback()
if conn_factuges is not None:
conn_factuges.rollback()
sys.exit(1)
finally:
if conn_factuges:
conn_factuges.close()
if conn_mysql:
conn_mysql.close()
if __name__ == "__main__":
main()

5
app/utils/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from .last_execution_helper import actualizar_fecha_ultima_ejecucion, obtener_fecha_ultima_ejecucion
from .log_system_metrics import log_system_metrics
from .password import hashPassword
from .send_orders_mail import send_orders_mail
from .text_converter import text_converter

View File

@ -0,0 +1,21 @@
from datetime import datetime, timezone
from dateutil import tz
# Función para obtener la fecha de la última ejecución del programa desde un archivo de texto
def obtener_fecha_ultima_ejecucion():
try:
with open('./last_execution.txt', 'r', encoding="utf8") as f:
fecha_str = f.read().strip()
return datetime.strptime(fecha_str, '%Y-%m-%d %H:%M:%S').astimezone(tz=tz.UTC)
except FileNotFoundError:
# Si el archivo no existe, se asume que el programa nunca se ha ejecutado antes
return datetime(2024, 1, 1, 0, 0, 0).astimezone(tz=tz.UTC)
# Función para actualizar la fecha de la última ejecución del programa en el archivo de texto
def actualizar_fecha_ultima_ejecucion():
with open('./last_execution.txt', 'w', encoding="utf8") as f:
f.write(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))

View File

@ -0,0 +1,9 @@
import psutil
import logging
def log_system_metrics():
cpu_usage = psutil.cpu_percent(interval=1)
memory_usage = psutil.virtual_memory().percent
logging.info(f'CPU Usage: {cpu_usage}%')
logging.info(f'Memory Usage: {memory_usage}%')

5
app/utils/password.py Normal file
View File

@ -0,0 +1,5 @@
import bcrypt
def hashPassword(plain_text_password):
salt = bcrypt.gensalt(rounds=10)
return bcrypt.hashpw(plain_text_password.encode('utf-8'), salt)

View File

@ -0,0 +1,29 @@
import logging
import brevo_python
from brevo_python.rest import ApiException
from config import setup_brevo
from brevo_python.rest import ApiException
from 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))
for order in inserted_orders:
send_smtp_email = brevo_python.SendSmtpEmail(
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"],
"dealer_name": order["dealer_name"]
},
)
api_response = api_instance.send_transac_email(send_smtp_email)
logging.info(msg=api_response)
except ApiException as e:
logging.error(msg=e)

View File

@ -0,0 +1,38 @@
import logging
def text_converter(texto, charset_destino='ISO8859_1', longitud_maxima=None):
"""
Convierte un texto al charset especificado, eliminando caracteres incompatibles.
Args:
texto (str): El texto a convertir.
charset_destino (str): El charset de destino (por defecto 'ISO8859_1').
longitud_maxima (int, opcional): La longitud máxima permitida para el texto convertido.
Returns:
str: El texto convertido al charset de destino.
"""
if not texto:
return ""
try:
# Convertir el texto al charset especificado
texto_convertido = texto.encode(
charset_destino, 'ignore').decode(charset_destino)
# Si se especifica una longitud máxima, truncar el texto
if longitud_maxima and len(texto_convertido) > longitud_maxima:
logging.warning(
f"El texto ha sido truncado de {len(texto_convertido)} a {longitud_maxima} caracteres.")
texto_convertido = texto_convertido[:longitud_maxima]
return texto_convertido
except UnicodeEncodeError as e:
logging.error(
f"Error al convertir texto a {charset_destino}: {str(e)}")
return ""
except Exception as e:
logging.error(f"Error inesperado al convertir texto: {str(e)}")
return ""

1
cronjob Normal file
View File

@ -0,0 +1 @@
*/5 * * * * /usr/local/bin/python /opt/uecko_sync_app/app/main.py >> /var/log/cron.log 2>&1

17
docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
uecko_sync:
container_name: uecko_sync_app:v1.0.8
env_file: ".env.production"
build: .
#volumes:
#- logs:/var/log/uecko_sync_app
networks:
- presupuestador-uecko_private
restart: unless-stopped
#volumes:
# logs:
networks:
presupuestador-uecko_private:
external: true

BIN
firebird/fbudflib2.so Normal file

Binary file not shown.

1171
firebird/install.sh Normal file

File diff suppressed because it is too large Load Diff

7
firebird/launch.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
/etc/init.d/xinetd start
$FIREBIRD_PATH/bin/gsec -user SYSDBA -password $FIREBIRD_DB_PASSWORD_DEFAULT -modify SYSDBA -pw $FIREBIRD_DB_PASSWORD
while :
do
echo "[`date`] --> Firebird running..."
done

1191
firebird/postinstall.sh Normal file

File diff suppressed because it is too large Load Diff

61
readme.md Normal file
View File

@ -0,0 +1,61 @@
Crear el entorno por primera vez:
---------------------------------
python3 -m venv venv
source venv/bin/activate <-- en linux
.\venv\Scripts\activat <-- en Windows
pip3 install -r requirements.txt
Lanzar el entorno para hacer pruebas del script:
-----------------------------------------------
source venv/bin/activate <-- en linux
.\venv\Scripts\activat <-- en Windows
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

27
requeriments.txt Normal file
View File

@ -0,0 +1,27 @@
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