funcionando con acana

This commit is contained in:
David Arranz 2025-11-05 18:43:40 +01:00
parent 405a6317ed
commit 0e3d9d4fcf
14 changed files with 359 additions and 891 deletions

View File

@ -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

View File

@ -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=

View File

@ -1 +1 @@
__version__ = "1.0.8"
__version__ = "1.0.0"

View File

@ -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

74
app/db/sql_sentences.py Normal file
View File

@ -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 = ?)"
)

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

53
app/utils/mails_helper.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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