472 lines
17 KiB
Python
472 lines
17 KiB
Python
from typing import Any, Dict
|
|
|
|
from uuid6 import uuid7
|
|
|
|
from app.config import load_config, logger
|
|
from app.utils import validar_nif
|
|
|
|
from . import normalizations as NORMALIZA
|
|
from . import sql_sentences as SQL
|
|
|
|
|
|
def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
|
config = load_config()
|
|
|
|
# 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()
|
|
cursor_mysql.execute(SQL.SELECT_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, porque han sido eliminadas en programa de facturación nuevo
|
|
ids_verifactu_deleted = {str(fila[0]) for fila in filas}
|
|
# logger.info(f"Customer invoices rows to be deleted: {len(ids_verifactu_deleted)}")
|
|
# Verificar si hay filas en el resultado
|
|
if ids_verifactu_deleted:
|
|
sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config)
|
|
else:
|
|
logger.info("There are NOT customer invoices deleted since the last run")
|
|
|
|
except Exception as e:
|
|
if cursor_mysql is not None:
|
|
cursor_mysql.close()
|
|
logger.error(
|
|
f"(ERROR) Failed to fetch from database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}"
|
|
)
|
|
logger.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:
|
|
cursor_FactuGES = conn_factuges.cursor()
|
|
# Ejecutar la consulta de FACTURAS_CLIENTE
|
|
cursor_FactuGES.execute(SQL.SELECT_FACTUGES_FACTURAS_CLIENTE)
|
|
filas = cursor_FactuGES.fetchall()
|
|
except Exception as e:
|
|
if cursor_FactuGES is not None:
|
|
cursor_FactuGES.close()
|
|
logger.error(
|
|
f"(ERROR) Failed to fetch from database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}"
|
|
)
|
|
logger.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)
|
|
# Verificar si hay filas en el resultado
|
|
if tuplas_seleccionadas:
|
|
sync_invoices_from_FACTUGES(
|
|
conn_mysql, tuplas_seleccionadas, conn_factuges, config
|
|
)
|
|
else:
|
|
logger.info("There are NOT new FACTURAS rows since the last run.")
|
|
|
|
|
|
def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config):
|
|
# 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
|
|
cursor_FactuGES = None
|
|
try:
|
|
cursor_FactuGES = conn_factuges.cursor()
|
|
if ids_verifactu_deleted:
|
|
logger.info(f"Liberate factuGES: {ids_verifactu_deleted}")
|
|
cursor_FactuGES.executemany(
|
|
SQL.LIMPIAR_FACTUGES_LINK,
|
|
[(id_verifactu,) for id_verifactu in ids_verifactu_deleted],
|
|
)
|
|
else:
|
|
logger.info("No articles to delete.")
|
|
|
|
except Exception as e:
|
|
# Escribir el error en el archivo de errores
|
|
logger.error(str(e))
|
|
raise e # Re-lanzar la excepción para detener el procesamiento
|
|
finally:
|
|
# Cerrar la conexión
|
|
if cursor_FactuGES is not None:
|
|
cursor_FactuGES.close()
|
|
|
|
|
|
def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
|
|
# Insertaremos cada factura existente en las filas a la nueva estructura de tablas del programa nuevo de facturacion.
|
|
# logger.info(f"FACTURAS_CLIENTE_DETALLE rows to be processed: {len(filas)}")
|
|
|
|
cursorMySQL = None
|
|
cursor_FactuGES = None
|
|
factuges_id_anterior = None
|
|
num_fac_procesed = 0
|
|
customer_valid = True
|
|
invoice_id: str | None = None
|
|
|
|
try:
|
|
cursorMySQL = conn_mysql.cursor()
|
|
cursor_FactuGES = conn_factuges.cursor()
|
|
|
|
# Insertar datos en la tabla 'customer_invoices'
|
|
for factura_detalle in filas:
|
|
# Preparamos los campos para evitar errores
|
|
customer_fields = NORMALIZA.normalize_customer_fields(factura_detalle)
|
|
header_invoice_fields = NORMALIZA.normalize_header_invoice_fields(
|
|
factura_detalle
|
|
)
|
|
details_invoice_fields = NORMALIZA.normalize_details_invoice_fields(
|
|
factura_detalle
|
|
)
|
|
|
|
factuges_id = int(factura_detalle["ID_FACTURA"])
|
|
if factuges_id_anterior is None or factuges_id_anterior != factuges_id:
|
|
# Validamos que el cif de la factura exista en la AEAT si no es así no se hace la sincro de la factura
|
|
sync_result = int(config["CTE_SYNC_RESULT_OK"])
|
|
sync_notes = None
|
|
customer_valid = validar_nif(
|
|
customer_fields["tin"], customer_fields["name"], config
|
|
)
|
|
if customer_valid:
|
|
# Comprobamos si existe el cliente del primer item de la factura
|
|
customer_id = get_or_create_customer(
|
|
cursorMySQL,
|
|
config["CTE_COMPANY_ID"],
|
|
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
|
|
invoice_id = insert_invoice_header(
|
|
cursorMySQL,
|
|
customer_fields,
|
|
header_invoice_fields,
|
|
customer_id,
|
|
pm_id,
|
|
str(factura_detalle["DES_FORMA_PAGO"]),
|
|
config,
|
|
)
|
|
|
|
# ---- impuestos cabecera
|
|
insert_header_taxes_if_any(
|
|
cursorMySQL,
|
|
invoice_id,
|
|
factura_detalle["IVA"],
|
|
factura_detalle["RECARGO_EQUIVALENCIA"],
|
|
header_invoice_fields,
|
|
)
|
|
|
|
# ---- registro verifactu
|
|
insert_verifactu_record(
|
|
cursorMySQL, header_invoice_fields, invoice_id, config
|
|
)
|
|
|
|
else:
|
|
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"
|
|
)
|
|
logger.info(sync_notes)
|
|
|
|
# Guardamos en Factuges el id de la customer_invoice
|
|
logger.info(
|
|
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),
|
|
)
|
|
num_fac_procesed += 1
|
|
|
|
# Insertamos detalles y taxes correspondientes siempre que hayamos insertado cabecera
|
|
if customer_valid:
|
|
if invoice_id is None:
|
|
raise RuntimeError("BUG: invoice_id no debería ser None si customer_valid es True")
|
|
insert_item_and_taxes(cursorMySQL, invoice_id, details_invoice_fields)
|
|
|
|
# Asignamos el id factura anterior para no volver a inserta cabecera
|
|
factuges_id_anterior = factuges_id
|
|
|
|
logger.info(f"FACTURAS_CLIENTE rows to be processed: {str(num_fac_procesed)}")
|
|
|
|
except Exception as e:
|
|
# Escribir el error en el archivo de errores
|
|
logger.error(str(e))
|
|
raise e # Re-lanzar la excepción para detener el procesamiento
|
|
|
|
finally:
|
|
# Cerrar la conexión
|
|
if cursorMySQL is not None:
|
|
cursorMySQL.close()
|
|
if cursor_FactuGES is not None:
|
|
cursor_FactuGES.close()
|
|
|
|
|
|
def get_or_create_customer(
|
|
cur, company_id: str, factuges_customer_id: str, fields: Dict[str, Any]
|
|
) -> str:
|
|
"""
|
|
Comprobamos si existe el cliente del primer item de la factura y si no lo creamos
|
|
"""
|
|
cur.execute(SQL.SELECT_CUSTOMER_BY_FACTUGES, (factuges_customer_id,))
|
|
row = cur.fetchone()
|
|
if not row or not row[0]:
|
|
customer_id = str(uuid7())
|
|
logger.info(
|
|
"Inserting customer %s %s %s",
|
|
factuges_customer_id,
|
|
fields["tin"],
|
|
fields["name"],
|
|
)
|
|
cur.execute(
|
|
SQL.INSERT_CUSTOMER,
|
|
(
|
|
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"],
|
|
),
|
|
)
|
|
return customer_id
|
|
customer_id = str(row[0])
|
|
logger.info("Updating customer %s %s", factuges_customer_id, customer_id)
|
|
cur.execute(
|
|
SQL.UPDATE_CUSTOMER,
|
|
(
|
|
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,
|
|
),
|
|
)
|
|
return customer_id
|
|
|
|
|
|
def get_or_create_payment_method(
|
|
cur, factuges_payment_id: str, description: str
|
|
) -> str:
|
|
"""
|
|
En el caso de que la forma de pago no exista la creamos
|
|
"""
|
|
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())
|
|
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)
|
|
)
|
|
return pm_id
|
|
pm_id = str(row[0])
|
|
logger.info("Payment method exists %s -> %s", factuges_payment_id, pm_id)
|
|
return pm_id
|
|
|
|
|
|
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:
|
|
"""
|
|
Inserta cabecera y devuelve invoice_id
|
|
"""
|
|
invoice_id = str(uuid7())
|
|
|
|
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
|
|
SQL.INSERT_INVOICE,
|
|
(
|
|
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"),
|
|
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"),
|
|
),
|
|
)
|
|
return invoice_id
|
|
|
|
|
|
def insert_verifactu_record(
|
|
cur: str, hif: Dict[str, Any], invoice_id: str, config
|
|
) -> str:
|
|
"""
|
|
Inserta registro verifactu vacio y devuelve id
|
|
"""
|
|
id = str(uuid7())
|
|
|
|
logger.info(
|
|
"Inserting verifactu record %s %s %s",
|
|
id,
|
|
hif.get("reference"),
|
|
hif.get("invoice_date"),
|
|
)
|
|
cur.execute( # type: ignore
|
|
SQL.INSERT_VERIFACTU_RECORD,
|
|
(id, invoice_id, config["CTE_STATUS_VERIFACTU"]),
|
|
)
|
|
return id
|
|
|
|
|
|
def insert_header_taxes_if_any(
|
|
cur,
|
|
invoice_id: str,
|
|
IVA: str,
|
|
RECARGO: str,
|
|
hif: Dict[str, Any],
|
|
) -> None:
|
|
"""Inserta impuestos de cabecera"""
|
|
|
|
# Conversión segura a número
|
|
try:
|
|
iva_val = float(IVA)
|
|
except (TypeError, ValueError):
|
|
iva_val = 0.0
|
|
|
|
try:
|
|
rec_val = float(RECARGO)
|
|
except (TypeError, ValueError):
|
|
rec_val = 0.0
|
|
|
|
# IVA (>= 0 acepta 0%)
|
|
if iva_val >= 0:
|
|
cur.execute(
|
|
SQL.INSERT_INVOICE_TAX,
|
|
(
|
|
str(uuid7()),
|
|
invoice_id,
|
|
hif.get("tax_code"),
|
|
hif.get("base"),
|
|
hif.get("iva"),
|
|
),
|
|
)
|
|
|
|
# Recargo equivalencia (> 0)
|
|
if rec_val > 0:
|
|
cur.execute(
|
|
SQL.INSERT_INVOICE_TAX,
|
|
(
|
|
str(uuid7()),
|
|
invoice_id,
|
|
"rec_5_2",
|
|
hif.get("base"),
|
|
hif.get("re"),
|
|
),
|
|
)
|
|
|
|
|
|
def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None:
|
|
"""
|
|
Inserta línea y sus impuestos derivados.
|
|
"""
|
|
item_id = str(uuid7())
|
|
|
|
# logger.info("Inserting item %s pos=%s qty=%s", item_id, fields.get('position'), fields.get('quantity_value'))
|
|
cur.execute(
|
|
SQL.INSERT_INVOICE_ITEM,
|
|
(
|
|
item_id,
|
|
invoice_id,
|
|
fields.get("position"),
|
|
fields.get("description"),
|
|
fields.get("quantity_value"),
|
|
fields.get("unit_value"),
|
|
fields.get("discount_percentage_value"),
|
|
None,
|
|
fields.get("total_value"),
|
|
fields.get("iva_code"),
|
|
fields.get("iva_percentage_value"),
|
|
fields.get("tax_amount"),
|
|
fields.get("rec_code"),
|
|
fields.get("rec_percentage_value"),
|
|
fields.get("rec_amount"),
|
|
),
|
|
)
|
|
|
|
# logger.info("Inserting item tax %s code=%s base=%s tax=%s",
|
|
# 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'))
|
|
# )
|