2025-11-30 21:31:09 +00:00
from typing import Any , Dict
2025-11-05 17:43:40 +00:00
from uuid6 import uuid7
2025-11-30 21:31:09 +00:00
from app . config import load_config , logger
2025-11-30 09:43:57 +00:00
from app . utils import validar_nif
2025-09-04 16:54:32 +00:00
2025-11-30 21:31:09 +00:00
from . import normalizations as NORMALIZA
from . import sql_sentences as SQL
2025-09-04 16:54:32 +00:00
2025-11-27 19:08:06 +00:00
def sync_invoices_factuges ( conn_factuges , conn_mysql , last_execution_date ) :
2025-09-04 16:54:32 +00:00
config = load_config ( )
2025-11-05 17:43:40 +00:00
# LIMPIAMOS LAS FACTURAS DE FACTUGES QUE HAYAN SIDO ELIMINADAS DEL PROGRAMA NUEVO DE FACTURACION, PARA QUE PUEDAN SER MODIFICADAS
2025-09-10 17:38:41 +00:00
# Crear un cursor para ejecutar consultas SQL
cursor_mysql = None
try :
cursor_mysql = conn_mysql . cursor ( )
2025-11-06 19:18:37 +00:00
cursor_mysql . execute ( SQL . SELECT_INVOICES_DELETED )
2025-09-10 17:38:41 +00:00
filas = cursor_mysql . fetchall ( )
cursor_mysql . close ( )
2025-11-05 17:43:40 +00:00
# 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
2025-09-10 17:38:41 +00:00
ids_verifactu_deleted = { str ( fila [ 0 ] ) for fila in filas }
2025-11-30 18:28:44 +00:00
# logger.info(f"Customer invoices rows to be deleted: {len(ids_verifactu_deleted)}")
2025-09-10 17:38:41 +00:00
# Verificar si hay filas en el resultado
if ids_verifactu_deleted :
2025-11-07 18:22:36 +00:00
sync_delete_invoices ( conn_factuges , ids_verifactu_deleted , config )
2025-09-10 17:38:41 +00:00
else :
2025-11-30 21:31:09 +00:00
logger . info ( " There are NOT customer invoices deleted since the last run " )
2025-09-10 17:38:41 +00:00
except Exception as e :
if cursor_mysql is not None :
cursor_mysql . close ( )
2025-11-30 21:31:09 +00:00
logger . error (
f " (ERROR) Failed to fetch from database: { config [ ' FWEB_MYSQL_DATABASE ' ] } - using user: { config [ ' FWEB_MYSQL_USER ' ] } "
)
2025-11-30 18:28:44 +00:00
logger . error ( e )
2025-09-10 17:38:41 +00:00
raise e
2025-11-05 17:43:40 +00:00
# BUSCAMOS FACTURAS ENVIADAS A VERIFACTU EN FACTUGES, PARA SUBIRLAS AL NUEVO PROGRAMA DE FACTURACIÓN
2025-09-04 16:54:32 +00:00
# Crear un cursor para ejecutar consultas SQL
cursor_FactuGES = None
try :
cursor_FactuGES = conn_factuges . cursor ( )
# Ejecutar la consulta de FACTURAS_CLIENTE
2025-11-06 19:18:37 +00:00
cursor_FactuGES . execute ( SQL . SELECT_FACTUGES_FACTURAS_CLIENTE )
2025-09-04 16:54:32 +00:00
filas = cursor_FactuGES . fetchall ( )
except Exception as e :
if cursor_FactuGES is not None :
cursor_FactuGES . close ( )
2025-11-30 21:31:09 +00:00
logger . error (
f " (ERROR) Failed to fetch from database: { config [ ' FACTUGES_DATABASE ' ] } - using user: { config [ ' FACTUGES_USER ' ] } "
)
2025-11-30 18:28:44 +00:00
logger . error ( e )
2025-09-04 16:54:32 +00:00
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 )
# Verificar si hay filas en el resultado
if tuplas_seleccionadas :
2025-11-07 18:22:36 +00:00
sync_invoices_from_FACTUGES (
2025-11-30 21:31:09 +00:00
conn_mysql , tuplas_seleccionadas , conn_factuges , config
)
2025-09-04 16:54:32 +00:00
else :
2025-11-30 21:31:09 +00:00
logger . info ( " There are NOT new FACTURAS rows since the last run. " )
2025-09-10 17:38:41 +00:00
2025-09-04 16:54:32 +00:00
2025-11-07 18:22:36 +00:00
def sync_delete_invoices ( conn_factuges , ids_verifactu_deleted , config ) :
2025-11-06 19:18:37 +00:00
# Eliminamos todos los IDs asociados en FactuGES que han sido eliminados así liberaremos la factura borrador y podermos modificarla de nuevo, para volverla a subir una vez hechos los cambios.
# VERIFACTU = 0 and ID_VERIFACTU = NULL
2025-09-10 17:38:41 +00:00
cursor_FactuGES = None
2025-09-04 16:54:32 +00:00
try :
2025-09-10 17:38:41 +00:00
cursor_FactuGES = conn_factuges . cursor ( )
if ids_verifactu_deleted :
2025-11-30 18:28:44 +00:00
logger . info ( f " Liberate factuGES: { ids_verifactu_deleted } " )
2025-11-30 21:31:09 +00:00
cursor_FactuGES . executemany (
SQL . LIMPIAR_FACTUGES_LINK ,
[ ( id_verifactu , ) for id_verifactu in ids_verifactu_deleted ] ,
)
2025-09-04 16:54:32 +00:00
else :
2025-11-30 18:28:44 +00:00
logger . info ( " No articles to delete. " )
2025-09-04 16:54:32 +00:00
except Exception as e :
# Escribir el error en el archivo de errores
2025-11-30 18:28:44 +00:00
logger . error ( str ( e ) )
2025-09-04 16:54:32 +00:00
raise e # Re-lanzar la excepción para detener el procesamiento
finally :
# Cerrar la conexión
2025-09-10 17:38:41 +00:00
if cursor_FactuGES is not None :
cursor_FactuGES . close ( )
2025-09-04 16:54:32 +00:00
2025-11-07 18:22:36 +00:00
def sync_invoices_from_FACTUGES ( conn_mysql , filas , conn_factuges , config ) :
2025-11-05 17:43:40 +00:00
# Insertaremos cada factura existente en las filas a la nueva estructura de tablas del programa nuevo de facturacion.
2025-11-30 18:28:44 +00:00
# logger.info(f"FACTURAS_CLIENTE_DETALLE rows to be processed: {len(filas)}")
2025-09-04 16:54:32 +00:00
cursorMySQL = None
cursor_FactuGES = None
2025-09-10 17:38:41 +00:00
factuges_id_anterior = None
num_fac_procesed = 0
2025-11-27 19:08:06 +00:00
customer_valid = True
2025-11-30 21:31:09 +00:00
invoice_id : str | None = None
2025-11-06 19:18:37 +00:00
2025-09-04 16:54:32 +00:00
try :
cursorMySQL = conn_mysql . cursor ( )
cursor_FactuGES = conn_factuges . cursor ( )
2025-11-06 19:18:37 +00:00
2025-09-04 16:54:32 +00:00
# Insertar datos en la tabla 'customer_invoices'
2025-09-10 17:38:41 +00:00
for factura_detalle in filas :
2025-11-07 18:22:36 +00:00
# Preparamos los campos para evitar errores
2025-11-30 21:31:09 +00:00
customer_fields = NORMALIZA . normalize_customer_fields ( factura_detalle )
2025-11-20 18:51:03 +00:00
header_invoice_fields = NORMALIZA . normalize_header_invoice_fields (
2025-11-30 21:31:09 +00:00
factura_detalle
)
2025-11-20 18:51:03 +00:00
details_invoice_fields = NORMALIZA . normalize_details_invoice_fields (
2025-11-30 21:31:09 +00:00
factura_detalle
)
2025-09-04 16:54:32 +00:00
2025-11-30 21:31:09 +00:00
factuges_id = int ( factura_detalle [ " ID_FACTURA " ] )
2025-09-10 17:38:41 +00:00
if factuges_id_anterior is None or factuges_id_anterior != factuges_id :
2025-11-27 19:08:06 +00:00
# Validamos que el cif de la factura exista en la AEAT si no es así no se hace la sincro de la factura
2025-11-30 21:31:09 +00:00
sync_result = int ( config [ " CTE_SYNC_RESULT_OK " ] )
2025-11-28 09:30:52 +00:00
sync_notes = None
2025-11-30 21:31:09 +00:00
customer_valid = validar_nif (
customer_fields [ " tin " ] , customer_fields [ " name " ] , config
)
2025-11-27 19:08:06 +00:00
if customer_valid :
# Comprobamos si existe el cliente del primer item de la factura
customer_id = get_or_create_customer (
cursorMySQL ,
2025-11-30 21:31:09 +00:00
config [ " CTE_COMPANY_ID " ] ,
2025-11-27 19:08:06 +00:00
str ( factura_detalle [ " ID_CLIENTE " ] ) ,
customer_fields ,
)
# ---- forma de pago
pm_id = get_or_create_payment_method (
cursorMySQL ,
str ( factura_detalle [ " ID_FORMA_PAGO " ] ) ,
str ( factura_detalle [ " DES_FORMA_PAGO " ] ) ,
)
# campos pendiente de revisar en un futuro
# xxxxxxx = str(factura_detalle['ID_FORMA_PAGO']) según este id se debe de guardar en la factura los vencimiento asociados a la forma de pago
# ---- cabecera factura
2025-11-30 21:31:09 +00:00
invoice_id = insert_invoice_header (
cursorMySQL ,
customer_fields ,
header_invoice_fields ,
customer_id ,
pm_id ,
str ( factura_detalle [ " DES_FORMA_PAGO " ] ) ,
config ,
2025-11-27 19:08:06 +00:00
)
# ---- impuestos cabecera
insert_header_taxes_if_any (
2025-11-30 21:31:09 +00:00
cursorMySQL ,
invoice_id ,
header_invoice_fields ,
)
2025-11-27 19:08:06 +00:00
# ---- registro verifactu
insert_verifactu_record (
2025-11-30 21:31:09 +00:00
cursorMySQL , header_invoice_fields , invoice_id , config
)
2025-11-27 19:08:06 +00:00
else :
2025-11-30 21:31:09 +00:00
sync_result = int ( config [ " CTE_SYNC_RESULT_FAIL " ] )
sync_notes = (
f " >>> Factura { header_invoice_fields [ ' reference ' ] } no cumple requisitos para ser mandada a Verifactu: "
f " >>>>>> El NIF/NOMBRE ( { customer_fields [ ' tin ' ] } / { customer_fields [ ' name ' ] } ) no está registrado en la AEAT. "
f " El NIF/CIF debe estar registrado en la AEAT y el nombre debe ser suficientemente parecido al nombre registrado en la AEAT "
)
2025-11-30 18:28:44 +00:00
logger . info ( sync_notes )
2025-11-20 18:51:03 +00:00
2025-09-10 17:38:41 +00:00
# Guardamos en Factuges el id de la customer_invoice
2025-11-30 18:28:44 +00:00
logger . info (
2025-11-30 21:31:09 +00:00
f " Updating FACTURAS_CLIENTE { sync_result } { invoice_id } { factuges_id } { sync_notes } "
)
cursor_FactuGES . execute (
SQL . UPDATE_FACTUGES_LINK ,
( sync_result , invoice_id , sync_notes , factuges_id ) ,
)
2025-09-10 17:38:41 +00:00
num_fac_procesed + = 1
2025-11-27 19:08:06 +00:00
# Insertamos detalles y taxes correspondientes siempre que hayamos insertado cabecera
if customer_valid :
2025-11-30 21:31:09 +00:00
if invoice_id is None :
raise RuntimeError ( " BUG: invoice_id no debería ser None si customer_valid es True " )
2025-11-27 19:08:06 +00:00
insert_item_and_taxes ( cursorMySQL , invoice_id , details_invoice_fields )
2025-11-06 19:18:37 +00:00
# Asignamos el id factura anterior para no volver a inserta cabecera
factuges_id_anterior = factuges_id
2025-11-30 21:31:09 +00:00
logger . info ( f " FACTURAS_CLIENTE rows to be processed: { str ( num_fac_procesed ) } " )
2025-11-06 19:18:37 +00:00
except Exception as e :
# Escribir el error en el archivo de errores
2025-11-30 18:28:44 +00:00
logger . error ( str ( e ) )
2025-11-06 19:18:37 +00:00
raise e # Re-lanzar la excepción para detener el procesamiento
finally :
# Cerrar la conexión
if cursorMySQL is not None :
cursorMySQL . close ( )
if cursor_FactuGES is not None :
cursor_FactuGES . close ( )
2025-11-30 21:31:09 +00:00
def get_or_create_customer (
cur , company_id : str , factuges_customer_id : str , fields : Dict [ str , Any ]
) - > str :
2025-11-07 18:22:36 +00:00
"""
Comprobamos si existe el cliente del primer item de la factura y si no lo creamos
"""
2025-11-06 19:18:37 +00:00
cur . execute ( SQL . SELECT_CUSTOMER_BY_FACTUGES , ( factuges_customer_id , ) )
row = cur . fetchone ( )
if not row or not row [ 0 ] :
customer_id = str ( uuid7 ( ) )
2025-11-30 21:31:09 +00:00
logger . info (
" Inserting customer %s %s %s " ,
factuges_customer_id ,
fields [ " tin " ] ,
fields [ " name " ] ,
)
2025-11-06 19:18:37 +00:00
cur . execute (
SQL . INSERT_CUSTOMER ,
(
2025-11-30 21:31:09 +00:00
customer_id ,
fields [ " name " ] ,
fields [ " tin " ] ,
fields [ " street " ] ,
fields [ " city " ] ,
fields [ " province " ] ,
fields [ " postal_code " ] ,
fields [ " country " ] ,
fields [ " language_code " ] ,
fields [ " phone_primary " ] ,
fields [ " phone_secondary " ] ,
fields [ " mobile_primary " ] ,
fields [ " mobile_secondary " ] ,
fields [ " email_primary " ] ,
fields [ " email_secondary " ] ,
fields [ " website " ] ,
factuges_customer_id ,
company_id ,
fields [ " is_company " ] ,
2025-11-06 19:18:37 +00:00
) ,
)
return customer_id
customer_id = str ( row [ 0 ] )
2025-11-30 18:28:44 +00:00
logger . info ( " Updating customer %s %s " , factuges_customer_id , customer_id )
2025-11-06 19:18:37 +00:00
cur . execute (
SQL . UPDATE_CUSTOMER ,
(
2025-11-30 21:31:09 +00:00
fields [ " name " ] ,
fields [ " tin " ] ,
fields [ " street " ] ,
fields [ " city " ] ,
fields [ " province " ] ,
fields [ " postal_code " ] ,
fields [ " country " ] ,
fields [ " language_code " ] ,
fields [ " is_company " ] ,
fields [ " phone_primary " ] ,
fields [ " phone_secondary " ] ,
fields [ " mobile_primary " ] ,
fields [ " mobile_secondary " ] ,
fields [ " email_primary " ] ,
fields [ " email_secondary " ] ,
fields [ " website " ] ,
customer_id ,
2025-11-06 19:18:37 +00:00
) ,
)
return customer_id
2025-11-30 21:31:09 +00:00
def get_or_create_payment_method (
cur , factuges_payment_id : str , description : str
) - > str :
2025-11-07 18:22:36 +00:00
"""
En el caso de que la forma de pago no exista la creamos
"""
2025-11-06 19:18:37 +00:00
cur . execute ( SQL . SELECT_PAYMENT_METHOD_BY_FACTUGES , ( factuges_payment_id , ) )
row = cur . fetchone ( )
if not row or not row [ 0 ] :
pm_id = str ( uuid7 ( ) )
2025-11-30 21:31:09 +00:00
logger . info (
" Inserting payment method %s %s %s " , factuges_payment_id , pm_id , description
)
cur . execute (
SQL . INSERT_PAYMENT_METHOD , ( pm_id , description , factuges_payment_id )
)
2025-11-06 19:18:37 +00:00
return pm_id
pm_id = str ( row [ 0 ] )
2025-11-30 18:28:44 +00:00
logger . info ( " Payment method exists %s -> %s " , factuges_payment_id , pm_id )
2025-11-06 19:18:37 +00:00
return pm_id
2025-11-30 21:31:09 +00:00
def insert_invoice_header (
cur : str ,
cf : Dict [ str , Any ] ,
hif : Dict [ str , Any ] ,
customer_id : str ,
payment_method_id : str ,
payment_method_description : str ,
config ,
) - > str :
2025-11-06 19:18:37 +00:00
"""
2025-11-07 18:22:36 +00:00
Inserta cabecera y devuelve invoice_id
2025-11-06 19:18:37 +00:00
"""
invoice_id = str ( uuid7 ( ) )
2025-11-30 21:31:09 +00:00
logger . info (
" Inserting invoice %s %s %s %s %s " ,
invoice_id ,
hif . get ( " reference " ) ,
hif . get ( " invoice_date " ) ,
hif . get ( " operation_date " ) ,
config [ " CTE_STATUS_INVOICE " ] ,
)
cur . execute ( # type: ignore
2025-11-06 19:18:37 +00:00
SQL . INSERT_INVOICE ,
(
2025-11-30 21:31:09 +00:00
invoice_id ,
hif . get ( " company_id " ) ,
hif . get ( " status " ) ,
hif . get ( " is_proforma " ) ,
hif . get ( " series " ) ,
hif . get ( " reference " ) ,
hif . get ( " invoice_date " ) ,
hif . get ( " operation_date " ) ,
hif . get ( " description " ) ,
2025-12-01 09:54:49 +00:00
hif . get ( " notes " ) ,
2025-11-30 21:31:09 +00:00
hif . get ( " subtotal_amount_value " ) ,
hif . get ( " discount_amount_value " ) ,
hif . get ( " discount_percentage_val " ) ,
hif . get ( " taxable_amount_value " ) ,
hif . get ( " taxes_amount_value " ) ,
hif . get ( " total_amount_value " ) ,
customer_id ,
cf . get ( " tin " ) ,
cf . get ( " name " ) ,
cf . get ( " street " ) ,
cf . get ( " city " ) ,
cf . get ( " province " ) ,
cf . get ( " postal_code " ) ,
" es " ,
payment_method_id ,
payment_method_description ,
hif . get ( " factuges_id " ) ,
hif . get ( " company_id " ) ,
hif . get ( " is_proforma " ) ,
2025-11-06 19:18:37 +00:00
) ,
)
2025-11-07 18:22:36 +00:00
return invoice_id
2025-11-06 19:18:37 +00:00
2025-11-30 21:31:09 +00:00
def insert_verifactu_record (
cur : str , hif : Dict [ str , Any ] , invoice_id : str , config
) - > str :
2025-11-20 18:51:03 +00:00
"""
Inserta registro verifactu vacio y devuelve id
"""
id = str ( uuid7 ( ) )
2025-11-30 21:31:09 +00:00
logger . info (
" Inserting verifactu record %s %s %s " ,
id ,
hif . get ( " reference " ) ,
hif . get ( " invoice_date " ) ,
)
cur . execute ( # type: ignore
2025-11-20 18:51:03 +00:00
SQL . INSERT_VERIFACTU_RECORD ,
2025-11-30 21:31:09 +00:00
( id , invoice_id , config [ " CTE_STATUS_VERIFACTU " ] ) ,
2025-11-20 18:51:03 +00:00
)
return id
2025-11-30 21:31:09 +00:00
def insert_header_taxes_if_any (
cur ,
invoice_id : str ,
hif : Dict [ str , Any ] ,
) - > None :
""" Inserta impuestos de cabecera """
2025-11-06 19:18:37 +00:00
2025-12-03 21:49:06 +00:00
cur . execute ( SQL . INSERT_INVOICE_TAX ,
(
str ( uuid7 ( ) ) ,
invoice_id ,
hif . get ( " base " ) ,
hif . get ( " iva_code " ) ,
hif . get ( " iva_percentage_value " ) ,
hif . get ( " iva " ) ,
hif . get ( " rec_code " ) ,
hif . get ( " rec_percentage_value " ) ,
hif . get ( " re " ) ,
hif . get ( " retention_code " ) ,
hif . get ( " retention_percentage_value " ) ,
hif . get ( " retention_amount_value " ) ,
hif . get ( " taxes_amount_value " ) ,
) ,
)
2025-11-06 19:18:37 +00:00
2025-11-07 18:22:36 +00:00
def insert_item_and_taxes ( cur , invoice_id : str , fields : Dict [ str , Any ] ) - > None :
2025-11-06 19:18:37 +00:00
"""
Inserta línea y sus impuestos derivados .
"""
item_id = str ( uuid7 ( ) )
2025-11-30 18:28:44 +00:00
# logger.info("Inserting item %s pos=%s qty=%s", item_id, fields.get('position'), fields.get('quantity_value'))
2025-11-06 19:18:37 +00:00
cur . execute (
SQL . INSERT_INVOICE_ITEM ,
2025-11-30 21:31:09 +00:00
(
item_id ,
invoice_id ,
fields . get ( " position " ) ,
fields . get ( " description " ) ,
fields . get ( " quantity_value " ) ,
fields . get ( " unit_value " ) ,
2025-12-03 21:49:06 +00:00
fields . get ( " subtotal_amount_value " ) ,
2025-11-30 21:31:09 +00:00
fields . get ( " discount_percentage_value " ) ,
2025-12-03 21:49:06 +00:00
fields . get ( " discount_amount_value " ) ,
2025-12-11 09:31:15 +00:00
fields . get ( " global_discount_percentage_value " ) ,
2025-12-03 21:49:06 +00:00
fields . get ( " global_discount_amount_value " ) ,
fields . get ( " total_discount_amount_value " ) ,
fields . get ( " taxable_amount_value " ) ,
2025-11-30 21:31:09 +00:00
fields . get ( " total_value " ) ,
fields . get ( " iva_code " ) ,
fields . get ( " iva_percentage_value " ) ,
2025-12-03 21:49:06 +00:00
fields . get ( " iva_amount_value " ) ,
2025-11-30 21:31:09 +00:00
fields . get ( " rec_code " ) ,
fields . get ( " rec_percentage_value " ) ,
2025-12-03 21:49:06 +00:00
fields . get ( " rec_amount_value " ) ,
fields . get ( " taxes_amount_value " ) ,
2025-11-30 21:31:09 +00:00
) ,
2025-11-06 19:18:37 +00:00
)
2025-11-30 18:28:44 +00:00
# logger.info("Inserting item tax %s code=%s base=%s tax=%s",
2025-11-24 12:13:54 +00:00
# item_id, fields.get('tax_code'), fields.get('total_value'), fields.get('tax_amount'))
# cur.execute(
# SQL.INSERT_INVOICE_ITEM_TAX, (str(uuid7()), item_id, fields.get('tax_code'),
# fields.get('total_value'), fields.get('tax_amount'))
# )