This commit is contained in:
David Arranz 2026-03-20 19:34:21 +01:00
parent a032383be3
commit a8e4f76df2
3 changed files with 184 additions and 68 deletions

View File

@ -7,6 +7,7 @@ from app.utils import (
create_invoice_header, create_invoice_header,
insert_invoice_REST_API_FACTUGES, insert_invoice_REST_API_FACTUGES,
insert_item_and_taxes, insert_item_and_taxes,
validar_nif_verifacti,
) )
from . import normalizations as NORMALIZA from . import normalizations as NORMALIZA
@ -58,10 +59,15 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
cursorMySQL = None cursorMySQL = None
cursor_FactuGES = None cursor_FactuGES = None
factuges_id_anterior = None factuges_id_anterior = None
tin_anterior = None
num_fac_procesed = 0 num_fac_procesed = 0
customer_valid = True customer_valid = False
invoice_id: str | None = None invoice_id: str | None = None
factura_payload: dict | None = None factura_payload: dict | None = None
sync_result: str | None = None
sync_notes: str | None = None
sync_result_anterior: str | None = None
sync_notes_anterior: str | None = None
try: try:
cursorMySQL = conn_mysql.cursor() cursorMySQL = conn_mysql.cursor()
@ -79,27 +85,52 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
) )
factuges_id = int(facturas["ID"]) factuges_id = int(facturas["ID"])
if factuges_id_anterior is None or factuges_id_anterior != factuges_id: logger.info(">>>PASO")
# Validamos que el cif de la factura exista en la AEAT si no es así no se hace la sincro de la factura logger.info(str(factuges_id_anterior))
logger.info(str(factuges_id))
# --- Si cambia la factura, primero persistimos la anterior
if factuges_id_anterior is not None and factuges_id_anterior != factuges_id:
if factura_payload is not None:
logger.info(">>>> Se cambia de factura y vamos a insertar la anterior")
resultado_rest = insert_invoice_REST_API_FACTUGES(factura_payload)
logger.info(resultado_rest)
num_fac_procesed += 1
if not resultado_rest.get("ok"):
logger.info(resultado_rest.get("error"))
else:
data = resultado_rest.get("data") or {}
invoice_id = data.get("proforma_id")
logger.info("Updating FACTURAS_CLIENTE %s %s %s",
sync_result_anterior, invoice_id, factuges_id_anterior,)
cursor_FactuGES.execute(
SQL.UPDATE_FACTUGES_LINK,
(
sync_result_anterior,
invoice_id,
sync_notes_anterior,
factuges_id_anterior,
),
)
factura_payload = None
# --- Si no hay payload, estamos arrancando una nueva factura
if factura_payload is None:
sync_result = int(config["CTE_SYNC_RESULT_OK"]) sync_result = int(config["CTE_SYNC_RESULT_OK"])
sync_notes = None sync_notes = None
customer_valid = True # validar_nif_verifacti(
# customer_fields["tin"], customer_fields["name"], config
# )
if customer_valid: # Validamos que el cif de la factura exista en la AEAT si no es así no se hace la sincro de la factura
if tin_anterior is None or tin_anterior != customer_fields["tin"]:
logger.info("VALIDO NIF>>>>>>>>>>>")
customer_valid = validar_nif_verifacti(
customer_fields["tin"], customer_fields["name"], config)
tin_anterior = customer_fields["tin"]
# ---- cabecera factura if not customer_valid:
factura_payload = create_invoice_header(
customer_fields, header_invoice_fields, facturas["ID_FORMA_PAGO"], str(facturas["DES_FORMA_PAGO"]))
# ---- detalles factura
factura_payload["items"].append(insert_item_and_taxes(details_invoice_fields))
logger.info(f"PAYLOAD {factura_payload}")
# InsertamosREST
Resultado_REST = insert_invoice_REST_API_FACTUGES(factura_payload)
logger.info(Resultado_REST)
else:
sync_result = int(config["CTE_SYNC_RESULT_FAIL"]) sync_result = int(config["CTE_SYNC_RESULT_FAIL"])
sync_notes = ( sync_notes = (
f">>> Factura {header_invoice_fields['reference']} no cumple requisitos para ser mandada a Verifactu: " f">>> Factura {header_invoice_fields['reference']} no cumple requisitos para ser mandada a Verifactu: "
@ -107,21 +138,50 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
f"El NIF/CIF debe estar registrado en la AEAT y el nombre debe ser suficientemente parecido al nombre 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) logger.info(sync_notes)
cursor_FactuGES.execute(SQL.UPDATE_FACTUGES_LINK, (sync_result,
invoice_id, sync_notes, factuges_id),)
continue
num_fac_procesed += 1 # ---- cabecera factura
logger.info("Creo cabecera>>>>>>")
factura_payload = create_invoice_header(
customer_fields, header_invoice_fields, facturas["ID_FORMA_PAGO"], str(facturas["DES_FORMA_PAGO"]))
# Guardamos en Factuges el id de la customer_invoice # Guardamos el estado asociado al payload actual
logger.info( sync_result_anterior = sync_result
f"Updating FACTURAS_CLIENTE {sync_result} {invoice_id} {factuges_id} {sync_notes}: ") sync_notes_anterior = sync_notes
# cursor_FactuGES.execute(
# SQL.UPDATE_FACTUGES_LINK,
# (sync_result, invoice_id, sync_notes, factuges_id),
# )
# ---- detalles factura
logger.info("Creo detalle>>>>")
factura_payload["items"].append(insert_item_and_taxes(details_invoice_fields))
# Asignamos el id factura anterior para no volver a inserta cabecera # Asignamos el id factura anterior para no volver a inserta cabecera
factuges_id_anterior = factuges_id factuges_id_anterior = factuges_id
logger.info(f"FACTURAS_CLIENTE rows to be processed: {str(num_fac_procesed)}") # --- Insertar última factura pendiente
if factura_payload is not None:
logger.info(">>>> Última factura y vamos a insertar")
resultado_rest = insert_invoice_REST_API_FACTUGES(factura_payload)
logger.info(resultado_rest)
num_fac_procesed += 1
if not resultado_rest.get("ok"):
logger.info(resultado_rest.get("error"))
else:
data = resultado_rest.get("data") or {}
invoice_id = data.get("proforma_id")
cursor_FactuGES.execute(
SQL.UPDATE_FACTUGES_LINK,
(
sync_result_anterior,
invoice_id,
sync_notes_anterior,
factuges_id_anterior,
),
)
logger.info("FACTURAS_CLIENTE rows to be processed: %s", num_fac_procesed)
except Exception as e: except Exception as e:
# Escribir el error en el archivo de errores # Escribir el error en el archivo de errores
@ -136,6 +196,57 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
cursor_FactuGES.close() cursor_FactuGES.close()
'''
if factuges_id_anterior is not None and factuges_id_anterior != factuges_id:
logger.info(">>>>Se cambia de factura y vamos a insertar")
# InsertamosREST
Resultado_REST = insert_invoice_REST_API_FACTUGES(factura_payload)
logger.info(Resultado_REST)
num_fac_procesed += 1
if not Resultado_REST.get("ok"):
# Solo informamos en logger porque al cliente final este error no le interesa saldría la causa en notes de FactuGES antiguo
logger.info(Resultado_REST.get("error"))
else:
logger.info(Resultado_REST)
data = Resultado_REST.get("data")
invoice_id = data.get("proforma_id")
# 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),)
# Quitamos payload de la factura insertada
factura_payload = None
if factura_payload is not None:
logger.info(">>>>Ultima factura y vamos a insertar")
# InsertamosREST
Resultado_REST = insert_invoice_REST_API_FACTUGES(factura_payload)
logger.info(Resultado_REST)
num_fac_procesed += 1
if not Resultado_REST.get("ok"):
# Solo informamos en logger porque al cliente final este error no le interesa saldría la causa en notes de FactuGES antiguo
logger.info(Resultado_REST.get("error"))
else:
logger.info(Resultado_REST)
data = Resultado_REST.get("data")
invoice_id = data.get("proforma_id")
# 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),)
logger.info(f"FACTURAS_CLIENTE rows to be processed: {str(num_fac_procesed)}")
factura_payload = None
'''
def insert_verifactu_record( def insert_verifactu_record(
cur: str, hif: Dict[str, Any], invoice_id: str, config) -> str: cur: str, hif: Dict[str, Any], invoice_id: str, config) -> str:
""" """

View File

@ -15,7 +15,7 @@ def create_invoice_header(
) -> Dict[str, Any]: ) -> Dict[str, Any]:
return { return {
"id": str(uuid7()), "id": str(uuid7()),
"factuges_id": hif.get("factuges_id"), "factuges_id": str(hif.get("factuges_id")),
"company_id": hif.get("company_id"), "company_id": hif.get("company_id"),
"is_proforma": hif.get("is_proforma"), "is_proforma": hif.get("is_proforma"),
"status": hif.get("status"), "status": hif.get("status"),
@ -27,13 +27,13 @@ def create_invoice_header(
"notes": none_to_empty(hif.get("notes")), "notes": none_to_empty(hif.get("notes")),
"language_code": cf.get("language_code"), "language_code": cf.get("language_code"),
"subtotal_amount_value": none_to_empty(hif.get("subtotal_amount_value")), "subtotal_amount_value": str(none_to_empty(hif.get("subtotal_amount_value"))),
"global_discount_percentage": none_to_empty(hif.get("discount_percentage_val")), "global_discount_percentage": str(none_to_empty(hif.get("discount_percentage_val"))),
"discount_amount_value": none_to_empty(hif.get("discount_amount_value")), "discount_amount_value": str(none_to_empty(hif.get("discount_amount_value"))),
"taxable_amount_value": hif.get("taxable_amount_value"), "taxable_amount_value": str(hif.get("taxable_amount_value")),
"taxes_amount_value": hif.get("taxes_amount_value"), "taxes_amount_value": str(hif.get("taxes_amount_value")),
"total_amount_value": hif.get("total_amount_value"), "total_amount_value": str(hif.get("total_amount_value")),
"payment_method_id": payment_method_id, "payment_method_id": str(payment_method_id),
"payment_method_description": payment_method_description, "payment_method_description": payment_method_description,
"customer": { "customer": {
@ -52,7 +52,7 @@ def create_invoice_header(
"mobile_secondary": none_to_empty(cf["mobile_secondary"]), "mobile_secondary": none_to_empty(cf["mobile_secondary"]),
"email_primary": none_to_empty(cf["email_primary"]), "email_primary": none_to_empty(cf["email_primary"]),
"email_secondary": none_to_empty(cf["email_secondary"]), "email_secondary": none_to_empty(cf["email_secondary"]),
"website": cf["website"], "website": str(none_to_empty(cf["website"])),
}, },
"items": [], "items": [],
@ -65,24 +65,24 @@ def insert_item_and_taxes(fields) -> Dict[str, Any]:
return { return {
"item_id": str(uuid7()), "item_id": str(uuid7()),
"position": str(fields.get("position")), "position": str(fields.get("position")),
"description": fields.get("description"), "description": str(none_to_empty(fields.get("description"))),
"quantity_value": str(fields.get("quantity_value")), "quantity_value": str(none_to_empty(fields.get("quantity_value"))),
"unit_value": str(fields.get("unit_value")), "unit_value": str(none_to_empty(fields.get("unit_value"))),
"subtotal_amount_value": fields.get("subtotal_amount_value"), "subtotal_amount_value": str(fields.get("subtotal_amount_value")),
"item_discount_percentage_value": none_to_empty(fields.get("item_discount_percentage_value")), "item_discount_percentage_value": str(none_to_empty(fields.get("item_discount_percentage_value"))),
"item_discount_amount_value": none_to_empty(fields.get("item_discount_amount_value")), "item_discount_amount_value": str(none_to_empty(fields.get("item_discount_amount_value"))),
"global_discount_percentage_value": none_to_empty(fields.get("global_discount_percentage_value")), "global_discount_percentage_value": str(none_to_empty(fields.get("global_discount_percentage_value"))),
"global_discount_amount_value": none_to_empty(fields.get("global_discount_amount_value")), "global_discount_amount_value": str(none_to_empty(fields.get("global_discount_amount_value"))),
"total_discount_amount_value": fields.get("total_discount_amount_value"), "total_discount_amount_value": str(fields.get("total_discount_amount_value")),
"taxable_amount_value": fields.get("taxable_amount_value"), "taxable_amount_value": str(fields.get("taxable_amount_value")),
"total_value": fields.get("total_value"), "total_value": str(fields.get("total_value")),
"iva_code": fields.get("iva_code"), "iva_code": str(fields.get("iva_code")),
"iva_percentage_value": str(fields.get("iva_percentage_value")), "iva_percentage_value": str(str(fields.get("iva_percentage_value"))),
"iva_amount_value": fields.get("iva_amount_value"), "iva_amount_value": str(fields.get("iva_amount_value")),
"rec_code": none_to_empty(fields.get("rec_code")), "rec_code": str(none_to_empty(fields.get("rec_code"))),
"rec_percentage_value": none_to_empty(fields.get("rec_percentage_value")), "rec_percentage_value": str(none_to_empty(fields.get("rec_percentage_value"))),
"rec_amount_value": fields.get("rec_amount_value"), "rec_amount_value": str(fields.get("rec_amount_value")),
"taxes_amount_value": fields.get("taxes_amount_value"), "taxes_amount_value": str(fields.get("taxes_amount_value")),
} }
# logger.info("Inserting item tax %s code=%s base=%s tax=%s", # logger.info("Inserting item tax %s code=%s base=%s tax=%s",

View File

@ -4,6 +4,9 @@ import requests
from app.config import logger from app.config import logger
SUCCESS_CODES = {200, 201}
VALIDATION_ERROR_CODES = {400, 422}
def estado_factura(uuid_str: str, def estado_factura(uuid_str: str,
config, config,
@ -142,13 +145,11 @@ def validar_nif_verifacti(
resp = requests.post( resp = requests.post(
url, json=payload, headers=headers, timeout=timeout) url, json=payload, headers=headers, timeout=timeout)
if resp.status_code != 200: if resp.status_code != 200:
logger.info("ERRRRRROOOOOOORRRRR LLAMADA REST API") return False, None, f"HTTP {resp.status_code}: {resp.text}"
# return False, None, f"HTTP {resp.status_code}: {resp.text}"
data = resp.json() data = resp.json()
resultado = data.get("resultado", "NO IDENTIFICADO") resultado = data.get("resultado", "NO IDENTIFICADO")
logger.info(f"Resultado Verifacti: {resultado}") logger.info(f"Resultado Verifacti: {resultado}")
return resultado == "IDENTIFICADO" return resultado == "IDENTIFICADO"
except requests.RequestException as e: except requests.RequestException as e:
@ -177,26 +178,30 @@ def insert_invoice_REST_API_FACTUGES(
"nombre": nombre, "nombre": nombre,
} }
""" """
logger.info("-----------------------------------------------------------------------------")
logger.info(payload)
logger.info("-----------------------------------------------------------------------------")
try: try:
resp = requests.post( resp = requests.post(
url, json=payload, headers=headers, timeout=timeout) url, json=payload, headers=headers, timeout=timeout)
if resp.status_code == 200: if resp.status_code in SUCCESS_CODES:
location = resp.headers.get("Location")
try: try:
data = resp.json() data = resp.json()
logger.info(data)
except ValueError: except ValueError:
return {"ok": False, "status": 200, "error": "Respuesta 200 sin JSON válido", "raw": resp.text} data = None
return {"ok": True, "status": 200, "data": data} return {"ok": True, "status": resp.status_code, "data": data, "location": location}
if resp.status_code == 400: if resp.status_code in VALIDATION_ERROR_CODES:
try: try:
body = resp.json() body = resp.json()
msg = body.get( msg = (body.get("title") + str(body.get("errors"))
"error") or "Error de validación (400) sin detalle" or f"Error de validación ({resp.status_code}) sin detalle")
except ValueError: except ValueError:
msg = f"Error de validación (400): {resp.text}" msg = f"Error de validación (400): {resp.text}"
return {"ok": False, "status": 400, "error": msg} return {"ok": False, "status": resp.status_code, "error": msg}
# Otros códigos: devuelve mensaje genérico, intenta extraer JSON si existe # Otros códigos: devuelve mensaje genérico, intenta extraer JSON si existe
try: try:
@ -207,7 +212,7 @@ def insert_invoice_REST_API_FACTUGES(
return {"ok": False, "status": resp.status_code, "error": f"HTTP {resp.status_code}", "raw": resp.text} return {"ok": False, "status": resp.status_code, "error": f"HTTP {resp.status_code}", "raw": resp.text}
except requests.RequestException as e: except requests.RequestException as e:
logger.error("Error de conexión con la API Verifacti: %s", e) logger.error("Error de conexión con la API Factuges: %s", e)
return {"ok": False, "status": 500, "error": str(e)} return {"ok": False, "status": 500, "error": str(e)}
except ValueError as e: except ValueError as e:
logger.error("Respuesta no es JSON válido: %s", e) logger.error("Respuesta no es JSON válido: %s", e)