Subida a producción

This commit is contained in:
David Arranz 2025-11-30 22:31:09 +01:00
parent 51bd9fe7af
commit c349d12f9d
9 changed files with 419 additions and 189 deletions

View File

@ -1,9 +1,15 @@
from typing import Any, Mapping
import fdb
import mysql.connector
from app.config import logger
def get_factuges_connection(config):
def get_factuges_connection(config) -> fdb.Connection:
"""
Crea y devuelve una conexión a FactuGES (Firebird).
"""
try:
conn = fdb.connect(
host=config['FACTUGES_HOST'],
@ -11,34 +17,53 @@ def get_factuges_connection(config):
database=config['FACTUGES_DATABASE'],
user=config['FACTUGES_USER'],
password=config['FACTUGES_PASSWORD'],
charset='UTF8'
charset='UTF8',
)
logger.info(
f"Conexión a la base de datos FactuGES establecida: {config['FACTUGES_HOST']} with database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
"Conexión a la base de datos FactuGES establecida: %s with database:%s - using user:%s",
config['FACTUGES_HOST'],
config['FACTUGES_DATABASE'],
config['FACTUGES_USER'],
)
return conn
except Exception as e:
logger.error("Error al conectar a la base de datos FactuGES.")
logger.error(
f"(ERROR) Failed to establish connection to: {config['FACTUGES_HOST']} with database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
"(ERROR) Failed to establish connection to: %s with database:%s - using user:%s",
config['FACTUGES_HOST'],
config['FACTUGES_DATABASE'],
config['FACTUGES_USER'],
)
logger.error(str(e))
raise e
def get_mysql_connection(config):
def get_mysql_connection(config: Mapping[str, Any]):
"""
Crea y devuelve una conexión a MySQL.
"""
try:
conn = mysql.connector.connect(
host=config['FWEB_MYSQL_HOST'],
port=config['FWEB_MYSQL_PORT'],
database=config['FWEB_MYSQL_DATABASE'],
user=config['FWEB_MYSQL_USER'],
password=config['FWEB_MYSQL_PASSWORD']
host=config["FWEB_MYSQL_HOST"],
port=config["FWEB_MYSQL_PORT"],
database=config["FWEB_MYSQL_DATABASE"],
user=config["FWEB_MYSQL_USER"],
password=config["FWEB_MYSQL_PASSWORD"],
)
logger.info(
f"Conexión a la base de datos MySQL establecida a: {config['FWEB_MYSQL_HOST']} with database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}")
"Conexión a la base de datos MySQL establecida a: %s with database:%s - using user:%s",
config['FWEB_MYSQL_HOST'],
config['FWEB_MYSQL_DATABASE'],
config['FWEB_MYSQL_USER'],
)
return conn
except Exception as e:
logger.error("Error al conectar a la base de datos MySQL.")
logger.error(
f"(ERROR) Failed to establish connection to: {config['FWEB_MYSQL_HOST']} with database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}")
"(ERROR) Failed to establish connection to: %s with database:%s - using user:%s",
config['FWEB_MYSQL_HOST'],
config['FWEB_MYSQL_DATABASE'],
config['FWEB_MYSQL_USER'],
)
logger.error(str(e))
raise e

View File

@ -1,11 +1,13 @@
from app.config import logger
from typing import Dict, Any
from typing import Any, Dict
from uuid6 import uuid7
from app.config import load_config
from . import sql_sentences as SQL
from . import normalizations as NORMALIZA
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()
@ -26,14 +28,14 @@ def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
if ids_verifactu_deleted:
sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config)
else:
logger.info(
f"There are NOT customer invoices deleted since the last run")
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(
f"(ERROR) Failed to fetch from database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}"
)
logger.error(e)
raise e
@ -48,8 +50,9 @@ def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
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(
f"(ERROR) Failed to fetch from database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}"
)
logger.error(e)
raise e
@ -65,10 +68,10 @@ def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
# Verificar si hay filas en el resultado
if tuplas_seleccionadas:
sync_invoices_from_FACTUGES(
conn_mysql, tuplas_seleccionadas, conn_factuges, config)
conn_mysql, tuplas_seleccionadas, conn_factuges, config
)
else:
logger.info(
"There are NOT new FACTURAS rows since the last run.")
logger.info("There are NOT new FACTURAS rows since the last run.")
def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config):
@ -79,8 +82,10 @@ def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config):
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])
cursor_FactuGES.executemany(
SQL.LIMPIAR_FACTUGES_LINK,
[(id_verifactu,) for id_verifactu in ids_verifactu_deleted],
)
else:
logger.info("No articles to delete.")
@ -103,7 +108,7 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
factuges_id_anterior = None
num_fac_procesed = 0
customer_valid = True
invoice_id = None
invoice_id: str | None = None
try:
cursorMySQL = conn_mysql.cursor()
@ -111,27 +116,28 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
# 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)
customer_fields = NORMALIZA.normalize_customer_fields(factura_detalle)
header_invoice_fields = NORMALIZA.normalize_header_invoice_fields(
factura_detalle)
factura_detalle
)
details_invoice_fields = NORMALIZA.normalize_details_invoice_fields(
factura_detalle)
factura_detalle
)
factuges_id = int(factura_detalle['ID_FACTURA'])
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_result = int(config["CTE_SYNC_RESULT_OK"])
sync_notes = None
customer_valid = validar_nif(customer_fields["tin"], customer_fields["name"], config)
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'],
config["CTE_COMPANY_ID"],
str(factura_detalle["ID_CLIENTE"]),
customer_fields,
)
@ -146,40 +152,59 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
# 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
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)
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)
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")
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))
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)}")
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
@ -194,7 +219,9 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
cursor_FactuGES.close()
def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fields: Dict[str, Any]) -> str:
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
"""
@ -202,15 +229,34 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel
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"])
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"]
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
@ -219,16 +265,31 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel
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,
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:
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
"""
@ -236,73 +297,142 @@ def get_or_create_payment_method(cur, factuges_payment_id: str, description: str
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))
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:
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(
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')
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:
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(
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']
),
(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
"""
# IVA (>= 0 acepta 0% también, si no quieres registrar 0, cambia condición a > 0)
if (IVA or 0) >= 0:
cur.execute(SQL.INSERT_INVOICE_TAX,
(str(uuid7()), invoice_id, hif.get('tax_code'), hif.get('base'), hif.get('iva')))
def insert_header_taxes_if_any(
cur,
invoice_id: str,
IVA: str,
RECARGO: str,
hif: Dict[str, Any],
) -> None:
"""Inserta impuestos de cabecera"""
# Recargo equivalencia
if (RECARGO or 0) > 0:
cur.execute(SQL.INSERT_INVOICE_TAX,
(str(uuid7()), invoice_id, 'rec_5_2', hif.get('base'), hif.get('re')))
# 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:
@ -314,11 +444,23 @@ def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None:
# 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'),
)
(
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",

View File

@ -1,8 +1,8 @@
from app.config import logger
from typing import Dict, Any, Tuple, Optional, List, Iterable
from app.config import load_config
from decimal import Decimal
from app.utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str
from typing import Any, Dict, Iterable, List, Optional, Tuple
from app.config import load_config, logger
from app.utils import TaxCatalog, crear_factura, estado_factura, unscale_to_str
from . import sql_sentences as SQL
@ -32,9 +32,9 @@ def sync_invoices_verifactu(conn_mysql, last_execution_date):
# Verificar si hay filas en el resultado
if tuplas_seleccionadas:
enviar_datos(tuplas_seleccionadas, cursor_mysql, config)
logger.info(f"Ok send Verifactu")
logger.info("Ok send Verifactu")
else:
logger.info(f"There are no rows to send")
logger.info("There are no rows to send")
except Exception as error:
if cursor_mysql is not None:
@ -45,40 +45,57 @@ def sync_invoices_verifactu(conn_mysql, last_execution_date):
raise error
def enviar_datos(invoices_to_verifactu, cursor_mysql, config):
def enviar_datos(
invoices_to_verifactu,
cursor_mysql,
config: Dict[str, Any]
) -> None:
factura: Optional[Dict[str, Any]] = None
invoice_id: Optional[str] = None
# Recorrer todas las facturas para crear json de envio
try:
invoice_id = None
factura = None
for fila in invoices_to_verifactu:
fila_id = str(fila["id"])
# Si los ids de factura anterior y actual no coinciden, empezamos factura nueva, miramos si ya existe una factura si es así la mandamos AEAT
# y creamos la cabecera de la factura siguiente, si no existe factura solo la creamos
if invoice_id != str(fila['id']):
if invoice_id != fila_id:
procesar_factura_verifactu(factura, cursor_mysql, config)
# Cerrar la factura anterior (si existe)
if factura is not None:
procesar_factura_verifactu(factura, cursor_mysql, config)
# preparamos nueva factura
ok, respuesta = preparar_factura(fila)
ok, factura_nueva, error = preparar_factura(fila)
if not ok:
logger.info(
f"ERROR >>>>>> Factura {fila['reference']} no cumple requisitos para ser mandada a Verifactu:")
logger.info(
f">>>>>> Faltan campos requeridos: {respuesta}")
factura = None
logger.info(f"ERROR >>> Factura {fila['reference']} no válida: {error}")
continue
factura = respuesta
factura = factura_nueva
invoice_id = fila_id
# Añadir línea a factura actual
ok, linea = preparar_linea(fila)
if not ok:
ref = factura["reference"] if factura else "(desconocida)"
logger.info(
f"ERROR >>>>>> Factura {factura.get('reference')} no cumple requisitos para ser mandada a Verifactu:")
f"ERROR >>>>>> Factura {ref} no cumple requisitos para ser mandada a Verifactu:")
logger.info(f">>>>>> Faltan campos requeridos: {linea}")
factura = None
else:
continue
# Línea válida
# Garantizamos que factura no es None
if factura is not None:
factura["lineas"].append(linea)
procesar_factura_verifactu(factura, cursor_mysql, config)
# Procesar la última factura
if factura is not None:
procesar_factura_verifactu(factura, cursor_mysql, config)
except Exception as e:
# Escribir el error en el archivo de errores
@ -92,58 +109,95 @@ def procesar_factura_verifactu(
config: Dict[str, Any]
) -> bool:
if factura != None:
# Creamos registro de factura en verifactu
if factura.get('uuid') == '':
# logger.info(f"Send to create Verifactu: {factura}")
respuesta = crear_factura(factura, config)
if respuesta.get("status") == 200 and respuesta.get("ok"):
data = respuesta.get("data")
qr_verifactu = f"data:image/png;base64,{data.get('qr', '')}"
cursor_mysql.execute(SQL.update_verifactu_records_with_invoiceId, (data.get("estado"), data.get(
"uuid"), data.get("url"), qr_verifactu, factura.get("id")))
logger.info(
f">>> Factura {factura.get("reference")} registrada en Verifactu")
return True
else:
logger.info(
f">>> Factura {factura.get("reference")} enviada a Verifactu con error {respuesta}")
return False
# Actualizamos registro de factura en verifactu
if factura is None:
return False
# Creamos registro de factura en verifactu
if factura.get('uuid') == '':
# logger.info(f"Send to create Verifactu: {factura}")
respuesta = crear_factura(factura, config)
if respuesta.get("status") == 200 and respuesta.get("ok"):
data = respuesta.get("data") or {}
qr_verifactu = f"data:image/png;base64,{data.get('qr', '')}"
cursor_mysql.execute(
SQL.update_verifactu_records_with_invoiceId,
(
data.get("estado"),
data.get("uuid"),
data.get("url"),
qr_verifactu,
factura.get("id"),
)
)
logger.info(
f">>> Factura {factura.get('reference')} registrada en Verifactu")
return True
else:
# logger.info(f"Send to update Verifactu: {factura}")
respuesta = estado_factura(factura.get('uuid'), config)
if respuesta.get("status") == 200 and respuesta.get("ok"):
data = respuesta.get("data")
cursor_mysql.execute(SQL.update_verifactu_records_with_uuid, (data.get(
'estado'), data.get('operacion'), factura.get('uuid')))
logger.info(
f">>> Factura {factura.get("reference")} actualizado registro de Verifactu")
return True
else:
return False
logger.info(
f">>> Factura {factura.get('reference')} enviada a Verifactu con error {respuesta}")
return False
# Actualizamos registro de factura en verifactu
# logger.info(f"Send to update Verifactu: {factura}")
uuid_val = factura.get("uuid")
if not uuid_val: # None, '', o falsy
logger.error(f"Factura {factura.get('reference')} sin UUID válido")
return False
respuesta = estado_factura(uuid_val, config)
if respuesta.get("status") == 200 and respuesta.get("ok"):
data = respuesta.get("data") or {}
cursor_mysql.execute(
SQL.update_verifactu_records_with_uuid,
(data.get('estado'),
data.get('operacion'),
uuid_val)
)
logger.info(
f">>> Factura {factura.get('reference')} actualizado registro de Verifactu")
return True
else:
return False
def preparar_factura(fila: Dict[str, Any]) -> Tuple[bool, Dict[str, Any] | list]:
def preparar_factura(
fila: Dict[str, Any]
) -> Tuple[bool, Dict[str, Any] | None, str | None]:
"""
Prepara el JSON de factura para Verifactu a partir de 'fila'.
Retorna:
(ok, factura, error)
"""
campos_requeridos = ("series", "invoice_number",
"invoice_date", "description", "total_amount_value")
campos_requeridos = (
"series", "invoice_number", "invoice_date",
"description", "total_amount_value"
)
ok, missing = validar_requeridos(fila, campos_requeridos)
if not ok:
return False, missing
# Devolvemos string, NO lista
return False, None, ", ".join(missing)
factura = {
"nif": fila['customer_tin'],
"nombre": fila['customer_name'],
"serie": fila['series'],
"numero": fila['invoice_number'],
# convertimos la fecha al formato requerido en el api
"fecha_expedicion": fila['invoice_date'].strftime("%d-%m-%Y"),
# F1: Factura (Art. 6, 7.2 Y 7.3 del RD 1619/2012)
"tipo_factura": "F1",
"descripcion": fila['description'],
# desescalamos el importe para dar su importe real
"importe_total": unscale_to_str(str(fila['total_amount_value']), str(fila['total_amount_scale'])),
"lineas": [],
@ -154,7 +208,7 @@ def preparar_factura(fila: Dict[str, Any]) -> Tuple[bool, Dict[str, Any] | list]
"uuid": fila['uuid'],
}
return True, factura
return True, factura, None
def preparar_linea(fila: Dict[str, Any]) -> Tuple[bool, Dict[str, Any] | list]:

View File

@ -17,7 +17,7 @@ def send_orders_mail(inserted_orders):
for order in inserted_orders:
send_smtp_email = brevo_python.SendSmtpEmail(
to=[{'email': config['MAIL_TO']}],
subject=f"Nuevo pedido del distribuidor {order["dealer_name"]}",
subject=f"Nuevo pedido del distribuidor {order['dealer_name']}",
template_id=int(config["BREVO_EMAIL_TEMPLATE"]),
params={
"customer_reference": order["customer_reference"],

View File

@ -1,11 +1,13 @@
from typing import Any, Dict, Optional, Tuple
import requests
from app.config import logger
from typing import Optional, Dict, Any, Tuple
def estado_factura(uuid_str: str,
config,
) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
) -> Dict[str, Any]:
"""
Llama al endpoint de Verifacti para cosultar el estado de registro.
@ -52,15 +54,15 @@ def estado_factura(uuid_str: str,
except requests.RequestException as e:
logger.error("Error de conexión con la API Verifacti: %s", e)
return False, None, str(e)
return {"ok": False, "status": 500, "error": str(e)}
except ValueError as e:
logger.error("Respuesta no es JSON válido: %s", e)
return False, None, "Respuesta no es JSON válido"
return {"ok": False, "status": 500, "error": "Respuesta no es JSON válido"}
def crear_factura(payload,
config,
) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
) -> Dict[str, Any]:
"""
Llama al endpoint de Verifacti para crear una factura.
@ -107,10 +109,10 @@ def crear_factura(payload,
except requests.RequestException as e:
logger.error("Error de conexión con la API Verifacti: %s", e)
return False, None, str(e)
return {"ok": False, "status": 500, "error": str(e)}
except ValueError as e:
logger.error("Respuesta no es JSON válido: %s", e)
return False, None, "Respuesta no es JSON válido"
return {"ok": False, "status": 500, "error": "Respuesta no es JSON válido"}
def validar_nif(
@ -140,7 +142,7 @@ def validar_nif(
resp = requests.post(
url, json=payload, headers=headers, timeout=timeout)
if resp.status_code != 200:
logger.info(f"ERRRRRROOOOOOORRRRR LLAMADA REST API")
logger.info("ERRRRRROOOOOOORRRRR LLAMADA REST API")
# return False, None, f"HTTP {resp.status_code}: {resp.text}"
data = resp.json()

View File

@ -1,9 +1,8 @@
from app.config import logger
import json
from decimal import Decimal
from typing import Any, Dict, Iterable, Optional, Tuple
from decimal import ROUND_HALF_UP, Decimal
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Tuple
def map_tax_code(raw: str) -> str:

View File

@ -1,21 +1,21 @@
# SYNC
ENV = development
LOCAL_TZ = Europe/Madrid
LAST_RUN_PATH = /usr/share/factuges-app/last_run.txt
LAST_RUN_PATH = /usr/share/factuges-app/last_run_factuges.ini
FACTUGES_HOST = acana.mywire.org
FACTUGES_PORT = 63050
FACTUGES_DATABASE = D:\Rodax\BD\FACTUGES.FDB
FACTUGES_USER = sysdba
FACTUGES_PASSWORD = masterkey
FWEB_MYSQL_HOST = db # ${DB_NAME}
FWEB_MYSQL_PORT = 3306 # ${DB_PORT}
FWEB_MYSQL_DATABASE = factuges_acana # ${DB_NAME}
FWEB_MYSQL_USER = acana # ${DB_USER}
FWEB_MYSQL_PASSWORD = r@U8%GJ+2e/AWR # ${DB_PASS}
CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
FWEB_MYSQL_HOST = db
FWEB_MYSQL_PORT = 3306
FWEB_MYSQL_DATABASE = factuges_acana
FWEB_MYSQL_USER = acana
FWEB_MYSQL_PASSWORD = r@U8%GJ+2e/AWR
CTE_SERIE = 'F25/'
CTE_STATUS_INVOICE = 'issued'
CTE_IS_PROFORMA = 0
@ -26,5 +26,6 @@ CTE_IS_COMPANY = 1
CTE_SYNC_RESULT_OK = 1
CTE_SYNC_RESULT_FAIL = 2
VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
VERIFACTU_BASE_URL = https://api.verifacti.com/
VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno=

View File

@ -4,7 +4,7 @@ services:
container_name: "factuges-sync-factuges-acana"
restart: "no"
environment:
ENV: "prod"
ENV: "production"
LOCAL_TZ: "Europe/Madrid"
LAST_RUN_PATH: "${LAST_RUN_PATH}"
@ -29,7 +29,11 @@ services:
CTE_COUNTRY_CODE: "${CTE_COUNTRY_CODE}"
CTE_IS_COMPANY: "${CTE_IS_COMPANY}"
CTE_SYNC_RESULT_OK: "${CTE_SYNC_RESULT_OK}"
CTE_SYNC_RESULT_FAIL: "${CTE_SYNC_RESULT_FAIL}"
CTE_SYNC_RESULT_FAIL: "${CTE_SYNC_RESULT_FAIL}"
VERIFACTU_API_KEY: "${VERIFACTU_API_KEY}"
VERIFACTU_BASE_URL: "${VERIFACTU_BASE_URL}"
VERIFACTU_NIFS_API_KEY: "${VERIFACTU_NIFS_API_KEY}"
depends_on:
db:
condition: service_healthy

View File

@ -1,6 +1,6 @@
[metadata]
name = factuges-sync
version = 0.0.15
version = 0.0.21
description = ETL job to sync data from legacy DB to MariaDB
author = Rodax Software
author_email = info@rodax-software.com
@ -39,10 +39,12 @@ install_requires =
PyNaCl==1.5.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.0
requests==2.32.5
six==1.16.0
sshtunnel==0.4.0
striprtf==0.0.29
urllib3==2.2.3
uuid6
uuid6==2025.0.1
[options.extras_require]
dev =
@ -51,6 +53,7 @@ dev =
packaging==24.1
pathspec==0.12.1
platformdirs==4.3.6
ruff==0.14.7
[options.entry_points]
console_scripts =
@ -62,9 +65,9 @@ where = .
[tool:pytest]
testpaths = tests
[flake8]
max-line-length = 88
exclude = .git,__pycache__,build,dist
[isort]
profile = black
[tool.ruff]
line-length = 88
target-version = "py312"