commit inicial
This commit is contained in:
commit
2ae6a66a4e
38
.env.development
Normal file
38
.env.development
Normal 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
36
.env.production
Normal 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
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
venv/
|
||||
__pycache__
|
||||
.env
|
||||
input/
|
||||
output/
|
||||
FACTUGES.FDB
|
||||
last_execution*.txt
|
||||
*.json
|
||||
*.log
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"rcVerbose": true
|
||||
}
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal 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
43
Dockerfile.firebird
Normal 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
0
app/__init__.py
Normal file
1
app/__version__.py
Normal file
1
app/__version__.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = "1.0.8"
|
||||
3
app/config/__init__.py
Normal file
3
app/config/__init__.py
Normal 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
46
app/config/settings.py
Normal 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
10
app/config/setup_brevo.py
Normal 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
|
||||
18
app/config/setup_logging.py
Normal file
18
app/config/setup_logging.py
Normal 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
5
app/db/__init__.py
Normal 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
44
app/db/db_connection.py
Normal 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
219
app/db/sync_catalog.py
Normal 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
320
app/db/sync_dealers.py
Normal 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
216
app/db/sync_orders.py
Normal 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
82
app/main.py
Normal 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
5
app/utils/__init__.py
Normal 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
|
||||
21
app/utils/last_execution_helper.py
Normal file
21
app/utils/last_execution_helper.py
Normal 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'))
|
||||
9
app/utils/log_system_metrics.py
Normal file
9
app/utils/log_system_metrics.py
Normal 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
5
app/utils/password.py
Normal 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)
|
||||
29
app/utils/send_orders_mail.py
Normal file
29
app/utils/send_orders_mail.py
Normal 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)
|
||||
38
app/utils/text_converter.py
Normal file
38
app/utils/text_converter.py
Normal 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
1
cronjob
Normal 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
17
docker-compose.yml
Normal 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
BIN
firebird/fbudflib2.so
Normal file
Binary file not shown.
1171
firebird/install.sh
Normal file
1171
firebird/install.sh
Normal file
File diff suppressed because it is too large
Load Diff
7
firebird/launch.sh
Normal file
7
firebird/launch.sh
Normal 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
1191
firebird/postinstall.sh
Normal file
File diff suppressed because it is too large
Load Diff
61
readme.md
Normal file
61
readme.md
Normal 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
27
requeriments.txt
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user