diff --git a/.env.development b/.env.development index 51c91ae..4885917 100644 --- a/.env.development +++ b/.env.development @@ -4,9 +4,9 @@ ENVIRONMENT = development LOCAL_TZ = Europe/Madrid #LOG_PATH = ./app.log -FACTUGES_HOST = 192.168.0.135 +FACTUGES_HOST = 192.168.0.101 FACTUGES_PORT = 3050 -FACTUGES_DATABASE = C:\Codigo\Output\Debug\Database\FACTUGES.FDB +FACTUGES_DATABASE = C:\FactuGES\FACTUGES.FDB FACTUGES_USER = sysdba FACTUGES_PASSWORD = masterkey @@ -18,9 +18,9 @@ FACTUGES_CONTRATO_TIPO_DETALLE = "Concepto" FACTUGES_NOMBRE_TARIFA = TARIFA 2024 FACTUGES_PRECIO_PUNTO = 3.31 -UECKO_MYSQL_HOST = 192.168.0.116 +UECKO_MYSQL_HOST = 192.168.0.104 UECKO_MYSQL_PORT = 3306 -UECKO_MYSQL_DATABASE = uecko +UECKO_MYSQL_DATABASE = uecko_erp_sync UECKO_MYSQL_USER = rodax UECKO_MYSQL_PASSWORD = rodax diff --git a/.env.production b/.env.production index f5c30c2..9529231 100644 --- a/.env.production +++ b/.env.production @@ -2,7 +2,7 @@ ENVIRONMENT = production LOCAL_TZ = Europe/Madrid #LOG_PATH = /var/log/uecko_sync_app/uecko_sync_app.log -FACTUGES_HOST = 83.48.36.69 +FACTUGES_HOST = 83.48.36.692 FACTUGES_PORT = 3050 FACTUGES_DATABASE = D:\RODAX\FACTUGES\BD\FACTUGES_FABRICA.FDB FACTUGES_USER = sysdba @@ -16,7 +16,7 @@ FACTUGES_CONTRATO_TIPO_DETALLE = "Concepto" FACTUGES_NOMBRE_TARIFA = TARIFA 2024 FACTUGES_PRECIO_PUNTO = 3.31 -UECKO_MYSQL_HOST = mariadb +UECKO_MYSQL_HOST = mariadb2 UECKO_MYSQL_PORT = 3306 UECKO_MYSQL_DATABASE = uecko UECKO_MYSQL_USER = uecko diff --git a/app/db/__init__.py b/app/db/__init__.py index b927063..808461c 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -3,3 +3,4 @@ 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 +from .sync_invoices import sync_invoices diff --git a/app/db/sync_invoices.py b/app/db/sync_invoices.py new file mode 100644 index 0000000..2f87338 --- /dev/null +++ b/app/db/sync_invoices.py @@ -0,0 +1,293 @@ +import logging +from uuid import uuid4 +from config import load_config +from decimal import Decimal + + +def sync_invoices(conn_factuges, conn_mysql, last_execution_date): + # logging.info("ENTROOO") + 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_FACTURAS_CLIENTE = ( + f"SELECT fac.VERIFACTU, fac.ID_VERIFACTU, fac.ID || '' AS ID, fac.ID_EMPRESA || '' AS ID_EMPRESA, fac.REFERENCIA, fac.FECHA_FACTURA, fac.ID_CLIENTE || '' as ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, " + f"fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, fac.FECHA_ALTA, " + f"fac.IMPORTE_NETO, fac.DESCUENTO, fac.IMPORTE_DESCUENTO, fac.BASE_IMPONIBLE, fac.IMPORTE_IVA, fac.IMPORTE_TOTAL, " + f"fac.ID_FORMA_PAGO, fp.DESCRIPCION as DES_FORMA_PAGO, " + f"fac.ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL " + f"FROM FACTURAS_CLIENTE AS fac " + f"LEFT JOIN FORMAS_PAGO AS fp ON fac.ID_FORMA_PAGO = fp.ID " + f"WHERE " + f"(fac.VERIFACTU > 0) " + f"AND (fac.ID_VERIFACTU is null)" + ) + + # Crear un cursor para ejecutar consultas SQL + cursor_FactuGES = None + try: + cursor_FactuGES = conn_factuges.cursor() + # Ejecutar la consulta de FACTURAS_CLIENTE + cursor_FactuGES.execute(consulta_sql_FACTURAS_CLIENTE) + 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"FACTURAS_CLIENTE rows to be processed: {len(tuplas_seleccionadas)}") + + # Verificar si hay filas en el resultado + if tuplas_seleccionadas: + insertar_datos(conn_mysql, tuplas_seleccionadas, conn_factuges, config) + else: + logging.info( + "There are no new or modified FACTURAS 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_FACTURAS_CLIENTE) + 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"customer_invoice 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, conn_factuges, config): + + insert_customer_invoices_query = ( + "INSERT INTO customer_invoices (id, invoice_status, invoice_series, invoice_number, invoice_date, operation_date, " + "subtotal_amount_value, discount_amount_value, discount_percentaje_value, taxable_amount_value, tax_amount_value, total_amount_value, " + "payment_method_id, payment_method_description, " + "customer_id, customer_tin, customer_name, customer_street, customer_city, customer_state, customer_postal_code, customer_country, " + "subtotal_amount_scale, discount_amount_scale, discount_percentaje_scale, taxable_amount_scale, tax_amount_scale, total_amount_scale, " + "invoice_language, invoice_currency, created_at, updated_at) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 2, 2, 2, 2, 2, 2, 'es', 'EUR', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" + ) + + insert_customer_query = ( + "INSERT INTO customers (id, name, tin, street, city, state, postal_code, country, factuges_id, " + # "email, phone, fax, website, legal_record, default_tax," + "is_company, lang_code, currency_code, status, created_at, updated_at) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 1, 'es', 'EUR', 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" + ) + + update_FACTURAS_CLIENTE_query = ( + "UPDATE FACTURAS_CLIENTE set ID_VERIFACTU = ? WHERE ID = ?") + + 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_customer_query = ( + "SELECT customers.id FROM customers WHERE customers.factuges_id = %s" + ) + + cursorMySQL = None + cursor_FactuGES = None + try: + cursorMySQL = conn_mysql.cursor() + cursor_FactuGES = conn_factuges.cursor() + # Insertar datos en la tabla 'customer_invoices' + for factura in filas: + # Generar un ID único para la tabla customer_invoices + id_customer_invoice = str(uuid4()) + factuges_id = int(factura['ID']) + invoice_status = str('draft') + invoice_series = str('A') + invoice_number = str(factura['REFERENCIA']) + invoice_date = str(factura['FECHA_FACTURA']) + operation_date = str(factura['FECHA_FACTURA']) + # siempre tendrán 2 decimales + subtotal_amount_value = (factura['IMPORTE_NETO'])*100 + discount_amount_value = (factura['IMPORTE_DESCUENTO'])*100 + discount_percentaje_value = ( + factura['DESCUENTO'])*100 if (factura['DESCUENTO']) is not None else None + taxable_amount_value = (factura['BASE_IMPONIBLE'])*100 + tax_amount_value = (factura['IMPORTE_IVA'])*100 + total_amount_value = (factura['IMPORTE_TOTAL'])*100 + payment_method_id = str(factura['ID_FORMA_PAGO']) + payment_method_description = str(factura['DES_FORMA_PAGO']) + customer_id = str(uuid4()) + factuges_customer_id = str(factura['ID_CLIENTE']) + customer_tin = str(factura['NIF_CIF']) + customer_name = str(factura['NOMBRE']) + customer_street = str(factura['CALLE']) + customer_city = str(factura['POBLACION']) + customer_state = str(factura['PROVINCIA']) + customer_postal_code = str(factura['CODIGO_POSTAL']) + customer_country = 'es' + + # campos pendiente de revisar en un futuro + # xxxxxxx = str(factura['ID_EMPRESA']) + # xxxxxxx = str(factura['ID_FORMA_PAGO']) según este id se debe de guardar en la factura los vencimiento asociados a la forma de pago + # xxxxxxx = str(factura['OBSERVACIONES']) + # RE, IMPORTE_RE >> en el caso que este relleno debo trasladarlo a los detalles de la factura + + # Comprobamos si existe el cliente + cursorMySQL.execute(select_customer_query, + (factuges_customer_id, )) + row = cursorMySQL.fetchone() + is_new = (row is None) or (row[0] is None) + + if is_new: + logging.info( + f"Inserting customer {factuges_customer_id} {customer_tin} {customer_name}") + cursorMySQL.execute(insert_customer_query, (customer_id, customer_name, customer_tin, customer_street, customer_city, customer_state, + customer_postal_code, customer_country, factuges_customer_id)) + else: + # Si ya exite ponemos el id del customer correspondiente + customer_id = str(row[0]) + logging.info( + f"Updating customer {factuges_customer_id} {customer_id}") + # cursorMySQL.execute(update_catalog_query, + # (points, retail_price, id_article)) + + # Insertamos la factura + logging.info( + f"Inserting customer_invoice {invoice_number} {invoice_date}") + cursorMySQL.execute(insert_customer_invoices_query, (id_customer_invoice, invoice_status, invoice_series, invoice_number, invoice_date, operation_date, + subtotal_amount_value, discount_amount_value, discount_percentaje_value, taxable_amount_value, tax_amount_value, total_amount_value, + payment_method_id, payment_method_description, + customer_id, customer_tin, customer_name, customer_street, customer_city, customer_state, customer_postal_code, customer_country)) + + # Guardamos en Factuges el id de la customer_invoice + logging.info( + f"Updating FACTURAS_CLIENTE {id_customer_invoice} {factuges_id}") + cursor_FactuGES.execute( + update_FACTURAS_CLIENTE_query, (id_customer_invoice, factuges_id)) + + # 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() + if cursor_FactuGES is not None: + cursor_FactuGES.close() diff --git a/app/main.py b/app/main.py index efb71ec..559586e 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ 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 db import get_mysql_connection, get_factuges_connection, sync_invoices from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail @@ -40,20 +40,20 @@ def main(): 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) + # Sync invoices + sync_invoices(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() + # actualizar_fecha_ultima_ejecucion() # Confirmar los cambios conn_mysql.commit() conn_factuges.commit() # Enviar email - send_orders_mail(inserted_orders) + # send_orders_mail(inserted_orders) logging.info("== END (0) ==") sys.exit(0) @@ -61,6 +61,7 @@ def main(): except Exception as e: logging.error("Se ha producido un error en la última ejecución.") logging.error(e) + logging.error("Traceback:", exc_info=True) logging.info("== END (1) ==") if conn_mysql is not None: diff --git a/readme.md b/readme.md index 58856e1..6983546 100644 --- a/readme.md +++ b/readme.md @@ -1,16 +1,38 @@ +Instalar Python en Ubuntu (alias de Python3) +-------------------------------------------- +sudo apt-get install python-is-python3 +sudo apt install python3.12-venv + +Instalar el cliente de FirebirdSQL 2.0/2.1 en Ubuntu +---------------------------------------------------- +- Opción 1. Revisar si el cliente Firebird 2.1 (libfbclient2) está en repositorios. Si aparece, se puede instalar directamente: +sudo apt-get update +apt-cache search firebird | grep client +sudo apt-get install libfbclient2 + +- Opción 2. No aparece en el repositorio de Ubuntu. Instalación manual -> investigar + + Crear el entorno por primera vez: --------------------------------- -python3 -m venv venv +python -m venv venv source venv/bin/activate <-- en linux .\venv\Scripts\activat <-- en Windows + +Meter librerias requeridas al entorno creado +-------------------------------------------- pip3 install -r requirements.txt Lanzar el entorno para hacer pruebas del script: ----------------------------------------------- -source venv/bin/activate <-- en linux -.\venv\Scripts\activat <-- en Windows -python app\main.py +Linux: + source venv/bin/activate + python app/main.py + +Windows: + .\venv\Scripts\activat + python app\main.py