From 0e3d9d4fcf2e69460f01b0a4d8d2691a1522b2cb Mon Sep 17 00:00:00 2001 From: david Date: Wed, 5 Nov 2025 18:43:40 +0100 Subject: [PATCH] funcionando con acana --- .env.development | 16 +- .env.production | 41 ++++- app/__version__.py | 2 +- app/db/__init__.py | 3 - app/db/sql_sentences.py | 74 ++++++++ app/db/sync_catalog.py | 219 ----------------------- app/db/sync_dealers.py | 320 ---------------------------------- app/db/sync_invoices.py | 193 ++++++++------------ app/db/sync_orders.py | 216 ----------------------- app/main.py | 8 +- app/utils/__init__.py | 3 + app/utils/mails_helper.py | 53 ++++++ app/utils/telefonos_helper.py | 19 ++ app/utils/websites_helper.py | 83 +++++++++ 14 files changed, 359 insertions(+), 891 deletions(-) create mode 100644 app/db/sql_sentences.py delete mode 100644 app/db/sync_catalog.py delete mode 100644 app/db/sync_dealers.py delete mode 100644 app/db/sync_orders.py create mode 100644 app/utils/mails_helper.py create mode 100644 app/utils/telefonos_helper.py create mode 100644 app/utils/websites_helper.py diff --git a/.env.development b/.env.development index 654a293..0a41414 100644 --- a/.env.development +++ b/.env.development @@ -18,11 +18,17 @@ FACTUGES_CONTRATO_TIPO_DETALLE = "Concepto" FACTUGES_NOMBRE_TARIFA = TARIFA 2024 FACTUGES_PRECIO_PUNTO = 3.31 -UECKO_MYSQL_HOST = 192.168.0.104 -UECKO_MYSQL_PORT = 3306 -UECKO_MYSQL_DATABASE = uecko_erp_sync -UECKO_MYSQL_USER = rodax -UECKO_MYSQL_PASSWORD = rodax +PRO_UECKO_MYSQL_HOST = 192.168.0.250 +PRO_UECKO_MYSQL_PORT = 3306 +PRO_UECKO_MYSQL_DATABASE = factuges_db +PRO_UECKO_MYSQL_USER = root +PRO_UECKO_MYSQL_PASSWORD = rootpass + +DEV_UECKO_MYSQL_HOST = 192.168.0.104 +DEV_UECKO_MYSQL_PORT = 3306 +DEV_UECKO_MYSQL_DATABASE = uecko_erp_sync +DEV_UECKO_MYSQL_USER = rodax +DEV_UECKO_MYSQL_PASSWORD = rodax UECKO_DEFAULT_IVA = 2100 UECKO_DEFAULT_CURRENCY_CODE = EUR diff --git a/.env.production b/.env.production index 9529231..1828bd6 100644 --- a/.env.production +++ b/.env.production @@ -8,13 +8,26 @@ 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 +# DESARROLLO ALISO FACTUGES FIREBIRD +#FACTUGES_HOST = 192.168.0.105 +#FACTUGES_PORT = 3050 +#FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB +#FACTUGES_USER = sysdba +#FACTUGES_PASSWORD = masterkey + +# PRODUCCION RODAX FACTUGES FIREBIRD +#FACTUGES_HOST = 192.168.0.101 +#FACTUGES_PORT = 3050 +#FACTUGES_DATABASE = C:\FactuGES\FACTUGES.FDB +#FACTUGES_USER = sysdba +#FACTUGES_PASSWORD = masterkey + +# PRODUCCION RODAX MYSQL +#UECKO_MYSQL_HOST = 192.168.0.250 +#UECKO_MYSQL_PORT = 3306 +#UECKO_MYSQL_DATABASE = factuges_db +#UECKO_MYSQL_USER = root +#UECKO_MYSQL_PASSWORD = rootpass UECKO_MYSQL_HOST = mariadb2 UECKO_MYSQL_PORT = 3306 @@ -34,3 +47,17 @@ BREVO_EMAIL_TEMPLATE = 1 MAIL_FROM = 'no-reply@presupuestos.uecko.com' MAIL_TO = 'pedidos@uecko.com' +VERIFACTU_BASE_URL = https://api.verifacti.com/ +VERIFACTU_API_KEY = vf_test_kY9FoI86dH+g1a5hmEnb/0YcLTlMFlu+tpp9iMZp020= +VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno= + +#VERIFACTU_RODAX_TEST_API_KEY = vf_test_C03HL2F0X5OXSDRunjNFoMxD4IrRfK3kCC8PfcvCENI= +#VERIFACTU_RODAX_PROD_API_KEY = vf_prod_yfjonNPv2E4Fij+5J0hct0zCgUeFYT2dZzb23UZlM+Q= + +#VERIFACTU_ALISO_TEST_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= + + + + + + diff --git a/app/__version__.py b/app/__version__.py index 1022a28..1c11a6e 100644 --- a/app/__version__.py +++ b/app/__version__.py @@ -1 +1 @@ -__version__ = "1.0.8" +__version__ = "1.0.0" diff --git a/app/db/__init__.py b/app/db/__init__.py index dd38bb9..ca846bb 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -1,7 +1,4 @@ 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 from .sync_invoices import sync_invoices from .sync_invoices_verifactu import sync_invoices_verifactu diff --git a/app/db/sql_sentences.py b/app/db/sql_sentences.py new file mode 100644 index 0000000..deccd78 --- /dev/null +++ b/app/db/sql_sentences.py @@ -0,0 +1,74 @@ +# ========================= +# SQL (constantes) +# ========================= +SELECT_CUSTOMER_BY_FACTUGES = ( + "SELECT customers.id FROM customers WHERE customers.factuges_id=%s" +) + +INSERT_CUSTOMER = ( + "INSERT INTO customers (id, name, tin, street, city, province, postal_code, country, " + "phone_primary, phone_secondary, mobile_primary, mobile_secondary, " + "email_primary, email_secondary, website, factuges_id, company_id, is_company, " + "language_code, currency_code, status, created_at, updated_at) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,1,'es','EUR','active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + +UPDATE_CUSTOMER = ( + "UPDATE customers SET name=%s, tin=%s, street=%s, city=%s, province=%s, postal_code=%s, country=%s, " + "phone_primary=%s, phone_secondary=%s, mobile_primary=%s, mobile_secondary=%s, " + "email_primary=%s, email_secondary=%s, website=%s, updated_at=CURRENT_TIMESTAMP " + "WHERE (id=%s)" +) + +SELECT_PAYMENT_METHOD_BY_FACTUGES = ( + "SELECT payment_methods.id FROM payment_methods WHERE payment_methods.factuges_id=%s" +) + +INSERT_PAYMENT_METHOD = ( + "INSERT INTO payment_methods (id, description, factuges_id, created_at, updated_at) " + "VALUES (%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + +INSERT_INVOICE = ( + "INSERT INTO customer_invoices (id, company_id, invoice_number, status, series, reference, invoice_date, operation_date, description, " + "subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, taxes_amount_value, total_amount_value, " + "customer_id, customer_tin, customer_name, customer_street, customer_city, customer_province, customer_postal_code, customer_country, " + "payment_method_id, payment_method_description, " + "subtotal_amount_scale, discount_amount_scale, discount_percentage_scale, taxable_amount_scale, taxes_amount_scale, total_amount_scale, " + "language_code, currency_code, 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,%s,%s,%s,2,2,2,2,2,2,'es','EUR',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + +INSERT_INVOICE_ITEM = ( + "INSERT INTO customer_invoice_items " + "(item_id, invoice_id, position, description, quantity_value, unit_amount_value, " + "discount_percentage_value, discount_amount_value, total_amount_value, " + "quantity_scale, unit_amount_scale, discount_amount_scale, total_amount_scale, discount_percentage_scale, created_at, updated_at) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,2,4,2,4,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + +INSERT_INVOICE_TAX = ( + "INSERT INTO customer_invoice_taxes (tax_id, invoice_id, tax_code, taxable_amount_value, taxes_amount_value, " + "taxable_amount_scale, taxes_amount_scale, created_at, updated_at) " + "VALUES (%s,%s,%s,%s,%s,2,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + +INSERT_INVOICE_ITEM_TAX = ( + "INSERT INTO customer_invoice_item_taxes " + "(tax_id, item_id, tax_code, taxable_amount_value, taxes_amount_value, taxable_amount_scale, taxes_amount_scale, created_at, updated_at) " + "VALUES (%s,%s,%s,%s,%s,4,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + +UPDATE_FACTUGES_LINK = ( + "UPDATE FACTURAS_CLIENTE " + "SET ID_VERIFACTU=? " + "WHERE ID=?" +) + +LIMPIAR_FACTUGES_LINK = ( + "UPDATE FACTURAS_CLIENTE " + "SET ID_VERIFACTU = NULL, " + "VERIFACTU = 0 " + "WHERE (ID_VERIFACTU = ?)" + ) + diff --git a/app/db/sync_catalog.py b/app/db/sync_catalog.py deleted file mode 100644 index 86ce471..0000000 --- a/app/db/sync_catalog.py +++ /dev/null @@ -1,219 +0,0 @@ -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() diff --git a/app/db/sync_dealers.py b/app/db/sync_dealers.py deleted file mode 100644 index f80d2c4..0000000 --- a/app/db/sync_dealers.py +++ /dev/null @@ -1,320 +0,0 @@ -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() diff --git a/app/db/sync_invoices.py b/app/db/sync_invoices.py index 2287e67..06f7705 100644 --- a/app/db/sync_invoices.py +++ b/app/db/sync_invoices.py @@ -1,9 +1,10 @@ import logging import textwrap -from uuid import uuid4 +from . import sql_sentences as SQL +from uuid6 import uuid7 from config import load_config from decimal import Decimal, ROUND_HALF_UP -from utils import limpiar_cadena +from utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert def sync_invoices(conn_factuges, conn_mysql, last_execution_date): @@ -38,16 +39,17 @@ def sync_invoices(conn_factuges, conn_mysql, last_execution_date): f"ORDER BY (fac.ID)" ) + # LIMPIAMOS LAS FACTURAS DE FACTUGES QUE HAYAN SIDO ELIMINADAS DEL PROGRAMA NUEVO DE FACTURACION, PARA QUE PUEDAN SER MODIFICADAS # Crear un cursor para ejecutar consultas SQL cursor_mysql = None try: cursor_mysql = conn_mysql.cursor() - # Ejecutar la consulta de FACTURAS_CLIENTE + # Ejecutar la consulta de FACTURAS_CLIENTE cursor_mysql.execute(consulta_sql_customer_invoices_deleted) filas = cursor_mysql.fetchall() cursor_mysql.close() - # Crear un conjunto con los IDs [0] de los customer_inovices que debo liberar en FactuGES + # Crear un conjunto con los IDs [0] de los customer_inovices que debo liberar en FactuGES, porque han sido eliminadas en programa de facturación nuevo ids_verifactu_deleted = {str(fila[0]) for fila in filas} logging.info(f"Customer invoices rows to be deleted: { len(ids_verifactu_deleted)}") @@ -66,6 +68,7 @@ def sync_invoices(conn_factuges, conn_mysql, last_execution_date): logging.error(e) raise e + # BUSCAMOS FACTURAS ENVIADAS A VERIFACTU EN FACTUGES, PARA SUBIRLAS AL NUEVO PROGRAMA DE FACTURACIÓN # Crear un cursor para ejecutar consultas SQL cursor_FactuGES = None try: @@ -101,52 +104,16 @@ def sync_invoices(conn_factuges, conn_mysql, last_execution_date): logging.info( "There are no new FACTURAS rows since the last run.") - # Verificamos que en customer_invoice de mysql no se ha eliminado ninguna factura, - # si se ha eliminado alguna factura, procedemos quitar la asociación en factuges para que se pueda modificar - # en un futuro se modificará solo en el programa nuevo y tendrá que sincronizarse con factuges los importes totales de la factura - # ya pasamos de los detalles ya que no se van a ver las facturas en FactuGES, pero la relación que tengan las facturas con otros módulos - # deben verse, ejemplo contratos-facturas - # 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_factuges, ids_verifactu_deleted, config): - # Recorrer todos los articulos del catálogo web para ver si estan en filas, si no están se eliminan - - update_facturas_cliente_query = ( - f"UPDATE FACTURAS_CLIENTE " - f"SET ID_VERIFACTU = NULL, " - f"VERIFACTU = 0 " - f"WHERE (ID_VERIFACTU = ?)" - ) + # Eliminamos todos los IDs de verifacti que han sido eliminados así liberaremos la factura borrador y podermos modificarla de nuevo, para volverla a subir una vez hechos los cambios. cursor_FactuGES = None try: cursor_FactuGES = conn_factuges.cursor() if ids_verifactu_deleted: logging.info(f"Liberate factures: {ids_verifactu_deleted}") - cursor_FactuGES.executemany(update_facturas_cliente_query, [( + cursor_FactuGES.executemany(SQL.LIMPIAR_FACTUGES_LINK, [( id_verifactu,) for id_verifactu in ids_verifactu_deleted]) else: logging.info("No articles to delete.") @@ -162,60 +129,10 @@ def eliminar_datos(conn_factuges, ids_verifactu_deleted, config): def insertar_datos(conn_mysql, filas, conn_factuges, config): + # Insertaremos cada factura existente en las filas a la nueva estructura de tablas del programa nuevo de facturacion. + + # Compañia RODAX cte_company_id = '5e4dc5b3-96b9-4968-9490-14bd032fec5f' - insert_customer_query = ( - "INSERT INTO customers (id, name, tin, street, city, province, postal_code, country, phone_primary, phone_secondary, mobile_primary, mobile_secondary, " - "email_primary, email_secondary, website, factuges_id, company_id, is_company, language_code, currency_code, status, created_at, updated_at ) " - "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, 'es', 'EUR', 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " - ) - - insert_payment_methods_query = ( - "INSERT INTO payment_methods (id, description, factuges_id, created_at, updated_at ) " - "VALUES (%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " - ) - - insert_customer_invoices_query = ( - "INSERT INTO customer_invoices (id, company_id, status, series, reference, invoice_date, operation_date, description, " - "subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, taxes_amount_value, total_amount_value, " - "customer_id, customer_tin, customer_name, customer_street, customer_city, customer_province, customer_postal_code, customer_country, " - "payment_method_id, payment_method_description, " - "subtotal_amount_scale, discount_amount_scale, discount_percentage_scale, taxable_amount_scale, taxes_amount_scale, total_amount_scale, " - "language_code, currency_code, 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, %s, %s, 2, 2, 2, 2, 2, 2, 'es', 'EUR', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" - ) - - insert_customer_invoices_taxes_query = ( - "INSERT INTO customer_invoice_taxes (tax_id, invoice_id, tax_code, taxable_amount_value, taxes_amount_value, taxable_amount_scale, taxes_amount_scale, " - "created_at, updated_at) " - "VALUES (%s, %s, %s, %s, %s, 2, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" - ) - - insert_customer_invoice_items_query = ( - "INSERT INTO customer_invoice_items (item_id, invoice_id, position, description, quantity_value, unit_amount_value, discount_percentage_value, discount_amount_value, total_amount_value, " - "quantity_scale, unit_amount_scale, discount_amount_scale, total_amount_scale, discount_percentage_scale, created_at, updated_at) " - "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 2, 4, 2, 4, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" - ) - - insert_customer_invoice_item_taxes_query = ( - "INSERT INTO customer_invoice_item_taxes (tax_id, item_id, tax_code, taxable_amount_value, taxes_amount_value, taxable_amount_scale, taxes_amount_scale, " - "created_at, updated_at) " - "VALUES (%s, %s, %s, %s, %s, 4, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" - ) - - update_FACTURAS_CLIENTE_query = ( - "UPDATE FACTURAS_CLIENTE set ID_VERIFACTU = ? WHERE ID = ?") - - select_customer_query = ( - "SELECT customers.id " - "FROM customers " - "WHERE customers.factuges_id = %s" - ) - - select_payment_method_query = ( - "SELECT payment_methods.id " - "FROM payment_methods " - "WHERE payment_methods.factuges_id = %s" - ) cursorMySQL = None cursor_FactuGES = None @@ -225,12 +142,13 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): try: cursorMySQL = conn_mysql.cursor() cursor_FactuGES = conn_factuges.cursor() + contador_serie = 0 # Insertar datos en la tabla 'customer_invoices' for factura_detalle in filas: factuges_id = int(factura_detalle['ID_FACTURA']) invoice_status = str('draft') - invoice_series = str('A') + invoice_series = str('F25/') reference = str(factura_detalle['REFERENCIA']) invoice_date = str(factura_detalle['FECHA_FACTURA']) operation_date = str(factura_detalle['FECHA_FACTURA']) @@ -243,14 +161,16 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): taxable_amount_value = (factura_detalle['BASE_IMPONIBLE'] or 0)*100 # Preparamos el tipo de IVA, en FactuGES es único tax_code = str(factura_detalle['DES_TIPO_IVA']) - if tax_code == 'IVA21': + if (tax_code == 'IVA21') or (tax_code == 'IVA 21'): tax_code = 'iva_21' - elif tax_code == 'IVA18': + elif tax_code == 'IVA18' or (tax_code == 'IVA 18'): tax_code = 'iva_18' - elif tax_code == 'IVA16': + elif tax_code == 'IVA16' or (tax_code == 'IVA 16'): tax_code = 'iva_16' - elif tax_code == 'IVA10': + elif tax_code == 'IVA10' or (tax_code == 'IVA 10'): tax_code = 'iva_10' + elif tax_code == 'IVA4' or (tax_code == 'IVA 4'): + tax_code = 'iva_4' elif tax_code == 'EXENTO': tax_code = 'iva_exenta' else: @@ -261,11 +181,11 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): total_amount_value = (factura_detalle['IMPORTE_TOTAL'] or 0)*100 - payment_method_id = str(uuid4()) + payment_method_id = str(uuid7()) factuges_payment_method_id = str(factura_detalle['ID_FORMA_PAGO']) payment_method_description = str(factura_detalle['DES_FORMA_PAGO']) - customer_id = str(uuid4()) + customer_id = str(uuid7()) factuges_customer_id = str(factura_detalle['ID_CLIENTE']) customer_tin = limpiar_cadena(str(factura_detalle['NIF_CIF'])) customer_name = str(factura_detalle['NOMBRE']) @@ -309,26 +229,63 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): if factuges_id_anterior is None or factuges_id_anterior != factuges_id: # Comprobamos si existe el cliente del primer item de la factura - cursorMySQL.execute(select_customer_query, + cursorMySQL.execute(SQL.SELECT_CUSTOMER_BY_FACTUGES, (factuges_customer_id, )) row = cursorMySQL.fetchone() is_new = (row is None) or (row[0] is None) + #Validamos los campos que pueden dar conflicto + customer_phone_primary_tratado = normalizar_telefono_con_plus(customer_phone_primary) + customer_phone_secondary_tratado = normalizar_telefono_con_plus(customer_phone_secondary) + customer_mobile_primary_tratado = normalizar_telefono_con_plus(customer_mobile_primary) + customer_mobile_secondary_tratado = normalizar_telefono_con_plus(customer_mobile_secondary) + customer_webside_tratado = normalizar_url_para_insert(customer_webside) + customer_email_primary_ok, customer_email_primary_tratado = corregir_y_validar_email(customer_email_primary) + customer_email_secondary_ok, customer_email_secondary_tratado = corregir_y_validar_email(customer_email_secondary) + + + logging.info(f"La ficha de cliente {customer_tin} se modifican los siguientes campos:") + if (customer_phone_primary_tratado != customer_phone_primary): + logging.info(f"phone_primary ({customer_phone_primary}) se cambia por ({customer_phone_primary_tratado})") + if (customer_phone_secondary_tratado != customer_phone_secondary): + logging.info(f"phone_secondary ({customer_phone_secondary}) se cambia por ({customer_phone_secondary_tratado})") + if (customer_mobile_primary_tratado != customer_mobile_primary): + logging.info(f"mobile_primary ({customer_mobile_primary}) se cambia por ({customer_mobile_primary_tratado})") + if (customer_mobile_secondary_tratado != customer_mobile_secondary): + logging.info(f"mobile_secondary ({customer_mobile_secondary}) se cambia por ({customer_mobile_secondary_tratado})") + if (customer_webside_tratado != customer_webside): + logging.info(f"customer_webside ({customer_webside}) se cambia por ({customer_webside_tratado})") + + if customer_email_primary_ok: + if (customer_email_primary_tratado != customer_email_primary): + logging.info(f"email_primary ({customer_email_primary}) se cambia por ({customer_email_primary_tratado})") + else: + logging.info(f"Omitimos email_primary invalido({customer_email_primary})") + if customer_email_secondary_ok: + if (customer_email_secondary_tratado != customer_email_secondary): + logging.info(f"email_secondary ({customer_email_secondary}) se cambia por ({customer_email_secondary_tratado})") + else: + logging.info(f"Omitimos email_secondary invalido({customer_email_secondary})") + + 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_province, - customer_postal_code, customer_country, customer_phone_primary, customer_phone_secondary, customer_mobile_primary, - customer_mobile_secondary, customer_email_primary, customer_email_secondary, customer_webside, factuges_customer_id, cte_company_id)) + cursorMySQL.execute(SQL.INSERT_CUSTOMER, (customer_id, customer_name, customer_tin, customer_street, customer_city, customer_province, + customer_postal_code, customer_country, customer_phone_primary_tratado, customer_phone_secondary_tratado, customer_mobile_primary_tratado, + customer_mobile_secondary_tratado, customer_email_primary_tratado, customer_email_secondary_tratado, customer_webside_tratado, factuges_customer_id, cte_company_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_customer_query, .....) + cursorMySQL.execute(SQL.UPDATE_CUSTOMER, (customer_name, customer_tin, customer_street, customer_city, customer_province, customer_postal_code, customer_country, + customer_phone_primary_tratado, customer_phone_secondary_tratado, customer_mobile_primary_tratado, customer_mobile_secondary_tratado, + customer_email_primary_tratado, customer_email_secondary_tratado, customer_webside_tratado, customer_id)) + # Comprobamos si existe la forma de pago del primer item de la factura - cursorMySQL.execute(select_payment_method_query, + cursorMySQL.execute(SQL.SELECT_PAYMENT_METHOD_BY_FACTUGES, (factuges_payment_method_id, )) row = cursorMySQL.fetchone() is_new = (row is None) or (row[0] is None) @@ -336,7 +293,7 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): if is_new: logging.info( f"Inserting cuspayment method {factuges_payment_method_id} {payment_method_id} {payment_method_description}") - cursorMySQL.execute(insert_payment_methods_query, ( + cursorMySQL.execute(SQL.INSERT_PAYMENT_METHOD, ( payment_method_id, payment_method_description, factuges_payment_method_id)) else: # Si ya exite ponemos el id del customer correspondiente @@ -347,20 +304,22 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): # Insertamos cabecera de la factura # Generar un ID único para la tabla customer_invoices - id_customer_invoice = str(uuid4()) + id_customer_invoice = str(uuid7()) + contador_serie = contador_serie + 1 logging.info( f"Inserting customer_invoice {id_customer_invoice} {reference} {invoice_date}") - cursorMySQL.execute(insert_customer_invoices_query, (id_customer_invoice, cte_company_id, invoice_status, invoice_series, reference, invoice_date, operation_date, description, + cursorMySQL.execute(SQL.INSERT_INVOICE, (id_customer_invoice, cte_company_id, contador_serie, invoice_status, invoice_series, reference, invoice_date, operation_date, description, subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, tax_amount_value, total_amount_value, customer_id, customer_tin, customer_name, customer_street, customer_city, customer_province, customer_postal_code, customer_country, payment_method_id, payment_method_description)) + # Insertamos el IVA y RE si viene if (factura_detalle['IVA'] >= 0): taxable_amount_value = ( factura_detalle['BASE_IMPONIBLE'])*100 tax_amount_value = (factura_detalle['IMPORTE_IVA'])*100 - cursorMySQL.execute(insert_customer_invoices_taxes_query, (str(uuid4()), + cursorMySQL.execute(SQL.INSERT_INVOICE_TAX, (str(uuid7()), id_customer_invoice, tax_code, taxable_amount_value, tax_amount_value)) if (factura_detalle['RECARGO_EQUIVALENCIA'] > 0): @@ -368,22 +327,22 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): taxable_amount_value = ( factura_detalle['BASE_IMPONIBLE'])*100 tax_amount_value = (factura_detalle['IMPORTE_RE'])*100 - cursorMySQL.execute(insert_customer_invoices_taxes_query, (str(uuid4()), + cursorMySQL.execute(SQL.INSERT_INVOICE_TAX, (str(uuid7()), id_customer_invoice, tax_code, taxable_amount_value, tax_amount_value)) # 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)) + SQL.UPDATE_FACTUGES_LINK, (id_customer_invoice, factuges_id)) num_fac_procesed += 1 # Insertamos detalles y taxes correspondientes siempre # Generar un ID único para la tabla customer_invoice_items - item_id = str(uuid4()) + item_id = str(uuid7()) logging.info( f"Inserting customer_invoice_items {id_customer_invoice} {item_position} {item_quantity_value}") - cursorMySQL.execute(insert_customer_invoice_items_query, (item_id, id_customer_invoice, item_position, item_description, + cursorMySQL.execute(SQL.INSERT_INVOICE_ITEM, (item_id, id_customer_invoice, item_position, item_description, item_quantity_value, item_unit_amount_value, item_discount_percentage_value, item_discount_amount, item_total_amount)) if tax_code == 'iva_21': @@ -407,7 +366,7 @@ def insertar_datos(conn_mysql, filas, conn_factuges, config): logging.info( f"Inserting customer_invoice_item_taxes {item_id} {item_position} {tax_code} {item_total_amount} {tax_amount_value}") - cursorMySQL.execute(insert_customer_invoice_item_taxes_query, (str(uuid4()), item_id, tax_code, + cursorMySQL.execute(SQL.INSERT_INVOICE_ITEM_TAX, (str(uuid7()), item_id, tax_code, item_total_amount, tax_amount_value)) # Asignamos el id factura anterior para no volver a inserta cabecera diff --git a/app/db/sync_orders.py b/app/db/sync_orders.py deleted file mode 100644 index 8361ff5..0000000 --- a/app/db/sync_orders.py +++ /dev/null @@ -1,216 +0,0 @@ -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() diff --git a/app/main.py b/app/main.py index f98d534..f04fcf1 100644 --- a/app/main.py +++ b/app/main.py @@ -37,11 +37,12 @@ def main(): logging.info("Last execution (Local time): %s", last_execution_date_local_tz) + # Abrimos conexiones con una única transacción para que todo esté controlado conn_factuges = get_factuges_connection(config) conn_mysql = get_mysql_connection(config) # Sync invoices - logging.info(f"Sync invoices") + logging.info(f">>>>>>>>>>> Sync invoices FactuGES escritorio to FactuGES web") sync_invoices(conn_factuges, conn_mysql, last_execution_date_local_tz) # Confirmar los cambios @@ -49,17 +50,18 @@ def main(): conn_factuges.commit() conn_factuges.close() conn_mysql.close() + logging.info(f"FIN Sync FactuGES web >>>>>>>>>>") # ESTO OTRO DEBERIA SER OTRA TRANSACCION POR LO QUE HACEMOS NUEVA CONEXION # Vamos que deberia ir en otro lado conn_mysql = get_mysql_connection(config) # Sync Verifactu - logging.info(f"Sync Verifactu") + logging.info(f">>>>>>>>>> Sync facturas emitidas en FactuGES web to Verifactu") sync_invoices_verifactu(conn_mysql, last_execution_date_local_tz) - logging.info(f"SALGO Sync Verifactu") conn_mysql.commit() conn_mysql.close() + logging.info(f"FIN Sync Verifactu >>>>>>>>>>") # actualizar_fecha_ultima_ejecucion() diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 86f4473..374fbd6 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -6,3 +6,6 @@ from .text_converter import text_converter, limpiar_cadena from .send_rest_api import validar_nif, estado_factura, crear_factura from .tax_catalog_helper import TaxCatalog, get_default_tax_catalog from .importes_helper import unscale_to_str, unscale_to_decimal +from .telefonos_helper import normalizar_telefono_con_plus +from .mails_helper import corregir_y_validar_email +from .websites_helper import normalizar_url_para_insert \ No newline at end of file diff --git a/app/utils/mails_helper.py b/app/utils/mails_helper.py new file mode 100644 index 0000000..f4375b5 --- /dev/null +++ b/app/utils/mails_helper.py @@ -0,0 +1,53 @@ +import re +from typing import Any, Optional, Tuple + +# Regex práctico (no RFC completo) para validar emails +_EMAIL_RE = re.compile( + r"""^(?=.{3,254}$) # longitud total razonable + (?=.{1,64}@) # parte local máx. 64 + [A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+ + (?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)* # puntos en la parte local + @ + (?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+ # labels dominio + [A-Za-z]{2,63}$ # TLD + """, + re.X, +) + +def corregir_y_validar_email(texto: Any) -> Tuple[bool, Optional[str]]: + """ + Normaliza y valida un email. + Correcciones aplicadas: + - Reemplaza comas por puntos. + - Elimina espacios alrededor y dentro (p. ej. 'a @ b . com' -> 'a@b.com'). + - Convierte el dominio a minúsculas. + - Colapsa puntos consecutivos en el dominio ('..' -> '.'). + Devuelve (es_valido, email_corregido | None). + """ + if texto is None: + return False, None + + s = str(texto).strip() + + # 1) comas -> puntos + s = s.replace(",", ".") + + # 2) quita espacios + s = re.sub(r"\s+", "", s) + + # 3) separar local y dominio (si es posible) + if s.count("@") != 1: + # no intentamos correcciones más agresivas si hay 0 o >1 '@' + return False, s or None + + local, domain = s.split("@", 1) + + # 4) normalizaciones de dominio + domain = domain.lower() + domain = re.sub(r"\.{2,}", ".", domain) # colapsar puntos dobles + + candidato = f"{local}@{domain}" + + # 5) validar + es_valido = _EMAIL_RE.match(candidato) is not None + return es_valido, (candidato if candidato else None) diff --git a/app/utils/telefonos_helper.py b/app/utils/telefonos_helper.py new file mode 100644 index 0000000..5566ca7 --- /dev/null +++ b/app/utils/telefonos_helper.py @@ -0,0 +1,19 @@ +import re +from typing import Optional, Any + +def normalizar_telefono_con_plus(texto: Any) -> Optional[str]: + """ + Mantiene '+' si está al principio y el resto sólo dígitos. + - ' (+34) 600-123-456 ' -> '+34600123456' + - '620 61 24 91 Tamara' -> '620612491' + - None o sin dígitos -> None + """ + if texto is None: + return None + s = str(texto).strip() + keep_plus = s.startswith("+") + # Quitar todo lo que NO sea dígito + digits = re.sub(r"\D", "", s) + if not digits: + return None + return ("+" if keep_plus else "") + digits diff --git a/app/utils/websites_helper.py b/app/utils/websites_helper.py new file mode 100644 index 0000000..f2cea18 --- /dev/null +++ b/app/utils/websites_helper.py @@ -0,0 +1,83 @@ +import re +from typing import Any, Optional +from urllib.parse import urlsplit, urlunsplit + +_SCHEMES = {"http", "https"} +_LABEL_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$") +_TLD_RE = re.compile(r"^[A-Za-z]{2,63}$") + +def _is_valid_host(host: str) -> bool: + if not host or len(host) > 253: + return False + if host.endswith("."): + host = host[:-1] + parts = host.split(".") + if len(parts) < 2: + return False + if not all(_LABEL_RE.match(p) for p in parts): + return False + if not _TLD_RE.match(parts[-1]): + return False + return True + +def _normalize_host(host: str) -> str: + host = host.strip().strip(".").lower() + host = re.sub(r"\.{2,}", ".", host) # colapsa '..' -> '.' + return host + +def normalizar_url_para_insert(texto: Any) -> Optional[str]: + """ + Devuelve una URL normalizada lista para insertar en BD. + Si no es válida, devuelve None (-> SQL NULL). + """ + if texto is None: + return None + + s = str(texto).strip() + if not s: + return None + + # Correcciones ligeras + s = s.replace(",", ".") + s = re.sub(r"\s+", "", s) + + # Añadir esquema por defecto si falta + candidate = s if "://" in s else f"http://{s}" + + sp = urlsplit(candidate) + scheme = (sp.scheme or "http").lower() + netloc, path = sp.netloc, sp.path + + # Caso: dominio sin esquema puede quedar en path + if not netloc and path and "." in path and "/" not in path: + netloc, path = path, "" + + # Separar userinfo/host:port (no soportamos IPv6 con corchetes aquí) + hostport = netloc.rsplit("@", 1)[-1] + host, port = hostport, "" + if ":" in hostport: + h, p = hostport.rsplit(":", 1) + if h and p.isdigit(): + host, port = h, p + + host = _normalize_host(host) + + # Validaciones + if scheme not in _SCHEMES: + return None + if not _is_valid_host(host): + return None + if port: + try: + pi = int(port) + if not (1 <= pi <= 65535): + return None + except ValueError: + return None + + # Reconstrucción netloc (preservando userinfo si existía) + userinfo = netloc[:-len(hostport)] if netloc.endswith(hostport) else "" + new_netloc = f"{userinfo}{host}{(':' + port) if port else ''}" + + fixed = urlunsplit((scheme, new_netloc, path or "", sp.query, sp.fragment)) + return fixed