From 634d04dc1068b7da2c935fd0441606ebecc9d2a3 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 27 Nov 2025 20:08:06 +0100 Subject: [PATCH] . --- .env | 28 +----- .env.production | 57 ------------- .env.production.sync.factuges | 33 +++++++ .env_V | 21 +++++ app.last_run.txt | 1 + ...sion__.py => __version_sync_factuges__.py} | 0 app/__version_sync_verifactu__.py | 1 + app/config/settings.py | 1 + app/db/__init__.py | 2 +- app/db/sql_sentences.py | 6 +- ..._invoices.py => sync_invoices_factuges.py} | 85 +++++++++++-------- app/{main.py => sync_factuges_main.py} | 34 +++----- app/sync_verifactu_main.py | 81 ++++++++++++++++++ app/utils/last_execution_helper.py | 63 +++++++++++--- 14 files changed, 256 insertions(+), 157 deletions(-) delete mode 100644 .env.production create mode 100644 .env.production.sync.factuges create mode 100644 .env_V create mode 100644 app.last_run.txt rename app/{__version__.py => __version_sync_factuges__.py} (100%) create mode 100644 app/__version_sync_verifactu__.py rename app/db/{sync_invoices.py => sync_invoices_factuges.py} (76%) rename app/{main.py => sync_factuges_main.py} (65%) create mode 100644 app/sync_verifactu_main.py diff --git a/.env b/.env index f192633..6df7f00 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ ENVIRONMENT = development LOCAL_TZ = Europe/Madrid +LAST_RUN_PATH = ./app.last_run.txt #LOG_PATH = ./app.log #DESARROLLO ACANA @@ -9,20 +10,6 @@ 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 = rodax_db -#UECKO_MYSQL_USER = rodax_usr -#UECKO_MYSQL_PASSWORD = supersecret - #DESARROLLO UECKO_MYSQL_HOST = localhost UECKO_MYSQL_PORT = 3306 @@ -35,25 +22,16 @@ CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a' CTE_SERIE = 'F25/' CTE_STATUS_INVOICE = 'issued' CTE_IS_PROFORMA = 0 -CTE_STATUS_VERIFACTU = 'pendiente' +CTE_STATUS_VERIFACTU = 'Pendiente' CTE_LANGUAGE_CODE = 'es' #En uecko vendrá de su ficha CTE_COUNTRY_CODE = 'es' #En uecko vendrá de su ficha CTE_IS_COMPANY = 1 -#CONFIGURACION RODAX -#CTE_COMPANY_ID = '5e4dc5b3-96b9-4968-9490-14bd032fec5f' -#CTE_SERIE = 'F25/' -#CTE_STATUS_INVOICE = 'approved' -#CTE_IS_PROFORMA = 1 -#CTE_STATUS_VERIFACTU = 'Pendiente' - - VERIFACTU_BASE_URL = https://api.verifacti.com/ VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno= - #BREVO_API_KEY = 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 #BREVO_EMAIL_TEMPLATE = 1 #MAIL_FROM = 'no-reply@presupuestos.uecko.com' -#MAIL_TO = 'soporte@rodax-software.com' +#MAIL_TO = 'soporte@rodax-software.com' \ No newline at end of file diff --git a/.env.production b/.env.production deleted file mode 100644 index 6ecdc9f..0000000 --- a/.env.production +++ /dev/null @@ -1,57 +0,0 @@ -ENVIRONMENT = production -LOCAL_TZ = Europe/Madrid -#LOG_PATH = /var/log/uecko_sync_app/uecko_sync_app.log - -FACTUGES_HOST = 83.48.36.692 -FACTUGES_PORT = 3050 -FACTUGES_DATABASE = D:\RODAX\FACTUGES\BD\FACTUGES_FABRICA.FDB -FACTUGES_USER = sysdba -FACTUGES_PASSWORD = abeto2010 - -# 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 = rodax_db -#UECKO_MYSQL_USER = rodax_usr -#UECKO_MYSQL_PASSWORD = supersecret - -UECKO_MYSQL_HOST = mariadb2 -UECKO_MYSQL_PORT = 3306 -UECKO_MYSQL_DATABASE = uecko -UECKO_MYSQL_USER = uecko -UECKO_MYSQL_PASSWORD = u8Ax5Nw3%sjd - - -BREVO_API_KEY = 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 -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= - - - - - - diff --git a/.env.production.sync.factuges b/.env.production.sync.factuges new file mode 100644 index 0000000..b7f9581 --- /dev/null +++ b/.env.production.sync.factuges @@ -0,0 +1,33 @@ +ENVIRONMENT = development +LOCAL_TZ = Europe/Madrid +LAST_RUN_PATH = ./app.last_run.txt +#LOG_PATH = ./app.log + +#DESARROLLO ACANA +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 + +#DESARROLLO +UECKO_MYSQL_HOST = localhost +UECKO_MYSQL_PORT = 3306 +UECKO_MYSQL_DATABASE = uecko_erp_sync +UECKO_MYSQL_USER = rodax +UECKO_MYSQL_PASSWORD = rodax + +#CONFIGURACION ACANA +CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a' +CTE_SERIE = 'F25/' +CTE_STATUS_INVOICE = 'issued' +CTE_IS_PROFORMA = 0 +CTE_STATUS_VERIFACTU = 'Pendiente' +CTE_LANGUAGE_CODE = 'es' #En uecko vendrá de su ficha +CTE_COUNTRY_CODE = 'es' #En uecko vendrá de su ficha +CTE_IS_COMPANY = 1 + +#BREVO_API_KEY = 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 +#BREVO_EMAIL_TEMPLATE = 1 +#MAIL_FROM = 'no-reply@presupuestos.uecko.com' +#MAIL_TO = 'soporte@rodax-software.com' diff --git a/.env_V b/.env_V new file mode 100644 index 0000000..ddb143d --- /dev/null +++ b/.env_V @@ -0,0 +1,21 @@ +ENVIRONMENT = development +LOCAL_TZ = Europe/Madrid +LAST_RUN_PATH = ./app.last_run.txt +#LOG_PATH = ./app.log + +#DESARROLLO +UECKO_MYSQL_HOST = localhost +UECKO_MYSQL_PORT = 3306 +UECKO_MYSQL_DATABASE = uecko_erp_sync +UECKO_MYSQL_USER = rodax +UECKO_MYSQL_PASSWORD = rodax + +VERIFACTU_BASE_URL = https://api.verifacti.com/ +VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= +VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno= + + +#BREVO_API_KEY = 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 +#BREVO_EMAIL_TEMPLATE = 1 +#MAIL_FROM = 'no-reply@presupuestos.uecko.com' +#MAIL_TO = 'soporte@rodax-software.com' diff --git a/app.last_run.txt b/app.last_run.txt new file mode 100644 index 0000000..dbe7b1f --- /dev/null +++ b/app.last_run.txt @@ -0,0 +1 @@ +2025-11-27 18:58:46 \ No newline at end of file diff --git a/app/__version__.py b/app/__version_sync_factuges__.py similarity index 100% rename from app/__version__.py rename to app/__version_sync_factuges__.py diff --git a/app/__version_sync_verifactu__.py b/app/__version_sync_verifactu__.py new file mode 100644 index 0000000..1c11a6e --- /dev/null +++ b/app/__version_sync_verifactu__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/app/config/settings.py b/app/config/settings.py index e84bcd6..73045d1 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -10,6 +10,7 @@ def load_config(): return { 'ENVIRONMENT': os.getenv('ENVIRONMENT'), 'LOCAL_TZ': os.getenv('LOCAL_TZ', 'Europe/Madrid'), + 'LAST_RUN_PATH': os.getenv('LAST_RUN_PATH'), # 'LOG_PATH': os.getenv('LOG_PATH', 'app.log'), 'FACTUGES_HOST': os.getenv('FACTUGES_HOST'), diff --git a/app/db/__init__.py b/app/db/__init__.py index ca846bb..b08483d 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -1,4 +1,4 @@ from .db_connection import get_factuges_connection from .db_connection import get_mysql_connection -from .sync_invoices import sync_invoices +from .sync_invoices_factuges import sync_invoices_factuges from .sync_invoices_verifactu import sync_invoices_verifactu diff --git a/app/db/sql_sentences.py b/app/db/sql_sentences.py index 295f9c9..02f14b4 100644 --- a/app/db/sql_sentences.py +++ b/app/db/sql_sentences.py @@ -121,14 +121,16 @@ SELECT_FACTUGES_FACTURAS_CLIENTE = ( f"LEFT JOIN TIPOS_IVA AS ti ON fac.ID_TIPO_IVA = ti.ID " f"LEFT JOIN FACTURAS_CLIENTE_DETALLES as facdet ON fac.ID = facdet.ID_FACTURA " f"WHERE " - f"(fac.VERIFACTU > 0) " + f"(fac.VERIFACTU = 1) " f"AND (fac.ID_VERIFACTU is null)" f"ORDER BY (fac.ID)" ) UPDATE_FACTUGES_LINK = ( "UPDATE FACTURAS_CLIENTE " - "SET ID_VERIFACTU=? " + "SET VERIFACTU=?, " + "ID_VERIFACTU=?, " + "VERIFACTU_NOTES =? " "WHERE ID=?" ) diff --git a/app/db/sync_invoices.py b/app/db/sync_invoices_factuges.py similarity index 76% rename from app/db/sync_invoices.py rename to app/db/sync_invoices_factuges.py index 320b279..0043208 100644 --- a/app/db/sync_invoices.py +++ b/app/db/sync_invoices_factuges.py @@ -4,9 +4,10 @@ from uuid6 import uuid7 from config import load_config from . import sql_sentences as SQL from . import normalizations as NORMALIZA +from utils import validar_nif -def sync_invoices(conn_factuges, conn_mysql, last_execution_date): +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 @@ -67,7 +68,7 @@ def sync_invoices(conn_factuges, conn_mysql, last_execution_date): conn_mysql, tuplas_seleccionadas, conn_factuges, config) else: logging.info( - "There are no new FACTURAS rows since the last run.") + "There are NOT new FACTURAS rows since the last run.") def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config): @@ -101,6 +102,8 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config): cursor_FactuGES = None factuges_id_anterior = None num_fac_procesed = 0 + customer_valid = True + invoice_id = None try: cursorMySQL = conn_mysql.cursor() @@ -120,47 +123,56 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config): factuges_id = int(factura_detalle['ID_FACTURA']) if factuges_id_anterior is None or factuges_id_anterior != factuges_id: - # 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, - ) + # 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 = 1 + 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 + # ---- 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 - ) + # ---- 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) + # ---- 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) + # ---- registro verifactu + insert_verifactu_record( + cursorMySQL, header_invoice_fields, invoice_id, config) + + else: + sync_result = 2 + cadena = (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") + logging.info(cadena) # Guardamos en Factuges el id de la customer_invoice logging.info( - f"Updating FACTURAS_CLIENTE {invoice_id} {factuges_id}") - cursor_FactuGES.execute( - SQL.UPDATE_FACTUGES_LINK, (invoice_id, factuges_id)) + f"Updating FACTURAS_CLIENTE {sync_result} {invoice_id} {factuges_id} {cadena}") + cursor_FactuGES.execute(SQL.UPDATE_FACTUGES_LINK, (sync_result, invoice_id, cadena, factuges_id)) num_fac_procesed += 1 - # Insertamos detalles y taxes correspondientes siempre - # Siempre insertamos la línea - insert_item_and_taxes(cursorMySQL, invoice_id, - details_invoice_fields) + # Insertamos detalles y taxes correspondientes siempre que hayamos insertado cabecera + if customer_valid: + 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 @@ -240,7 +252,7 @@ def insert_invoice_header(cur: str, cf: Dict[str, Any], hif: Dict[str, Any], cus """ invoice_id = str(uuid7()) - logging.info("Inserting invoice %s %s %s %s", + logging.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( SQL.INSERT_INVOICE, @@ -298,8 +310,7 @@ def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None: """ item_id = str(uuid7()) - logging.info("Inserting item %s pos=%s qty=%s", - item_id, fields.get('position'), fields.get('quantity_value')) + # logging.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'), diff --git a/app/main.py b/app/sync_factuges_main.py similarity index 65% rename from app/main.py rename to app/sync_factuges_main.py index 713f6d3..e583c9a 100644 --- a/app/main.py +++ b/app/sync_factuges_main.py @@ -1,12 +1,12 @@ import sys import logging -import __version__ +from __version_sync_factuges__ import __version__ from datetime import datetime from dateutil import tz from config import setup_logging, load_config -from db import get_mysql_connection, get_factuges_connection, sync_invoices, sync_invoices_verifactu -from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail, limpiar_cadena +from db import get_mysql_connection, get_factuges_connection, sync_invoices_factuges +from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail def main(): @@ -19,8 +19,8 @@ def main(): # Logging setup_logging() - logging.info("== START ==") - logging.info(f"Version: {__version__.__version__}") + logging.info("== START SYNC FACTUGES ==") + logging.info(f"Version: {__version__}") logging.info(f"Environment: {config['ENVIRONMENT']}") log_system_metrics() @@ -28,7 +28,7 @@ def main(): conn_mysql = None try: # Obtener la fecha de la última ejecución del programa - last_execution_date_utc = obtener_fecha_ultima_ejecucion() + last_execution_date_utc = obtener_fecha_ultima_ejecucion(config['LAST_RUN_PATH']) last_execution_date_local_tz = last_execution_date_utc.astimezone( tz=local_tz).strftime("%Y-%m-%d %H:%M:%S") @@ -41,31 +41,19 @@ def main(): conn_factuges = get_factuges_connection(config) conn_mysql = get_mysql_connection(config) - # Sync invoices + # Sincronizamos logging.info( - f">>>>>>>>>>> Sync invoices FactuGES escritorio to FactuGES web") - sync_invoices(conn_factuges, conn_mysql, last_execution_date_local_tz) + f">>>>>>>>>>> INI Sync invoices FactuGES escritorio to FactuGES web") + sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date_local_tz) # Confirmar los cambios conn_mysql.commit() conn_factuges.commit() conn_factuges.close() conn_mysql.close() - logging.info(f"FIN Sync FactuGES web >>>>>>>>>>") + logging.info(f">>>>>>>>>>> FIN Sync invoices FactuGES escritorio to 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 facturas emitidas en FactuGES web to Verifactu") - sync_invoices_verifactu(conn_mysql, last_execution_date_local_tz) - conn_mysql.commit() - conn_mysql.close() - logging.info(f"FIN Sync Verifactu >>>>>>>>>>") - - # actualizar_fecha_ultima_ejecucion() + actualizar_fecha_ultima_ejecucion(config['LAST_RUN_PATH']) # Enviar email # send_orders_mail(inserted_orders) diff --git a/app/sync_verifactu_main.py b/app/sync_verifactu_main.py new file mode 100644 index 0000000..51310ac --- /dev/null +++ b/app/sync_verifactu_main.py @@ -0,0 +1,81 @@ +import sys +import logging + +from __version_sync_factuges__ import __version__ +from datetime import datetime +from dateutil import tz +from config import setup_logging, load_config +from db import get_mysql_connection, get_factuges_connection, sync_invoices_verifactu +from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail + + +def main(): + + # Cargar la configuración + config = load_config() + + local_tz = tz.gettz(config['LOCAL_TZ']) + + # Logging + setup_logging() + + logging.info("== START SYNC VERIFACTU ==") + logging.info(f"Version: {__version__}") + logging.info(f"Environment: {config['ENVIRONMENT']}") + log_system_metrics() + + conn_factuges = None + conn_mysql = None + try: + # Obtener la fecha de la última ejecución del programa + last_execution_date_utc = obtener_fecha_ultima_ejecucion() + last_execution_date_local_tz = last_execution_date_utc.astimezone( + tz=local_tz).strftime("%Y-%m-%d %H:%M:%S") + + logging.info("Last execution (UTC): %s", + last_execution_date_utc.strftime("%Y-%m-%d %H:%M:%S %Z")) + logging.info("Last execution (Local time): %s", + last_execution_date_local_tz) + + # Abrimos conexión con una única transacción para que todo esté controlado + conn_mysql = get_mysql_connection(config) + + # Sync Verifactu + logging.info( + f">>>>>>>>>> INI Sync facturas emitidas to Verifactu") + sync_invoices_verifactu(conn_mysql, last_execution_date_local_tz) + conn_mysql.commit() + conn_mysql.close() + logging.info(f">>>>>>>>>> FIN Sync facturas emitidas to Verifactu") + + actualizar_fecha_ultima_ejecucion() + + # Enviar email + # send_orders_mail(inserted_orders) + + logging.info("== END (0) ==") + sys.exit(0) + + except Exception as e: + logging.error("Se ha producido un error en la última ejecución.") + logging.error(e) + logging.error("Traceback:", exc_info=True) + logging.info("== END (1) ==") + + if conn_mysql is not None: + conn_mysql.rollback() + + if conn_factuges is not None: + conn_factuges.rollback() + + sys.exit(1) + + finally: + if conn_factuges: + conn_factuges.close() + if conn_mysql: + conn_mysql.close() + + +if __name__ == "__main__": + main() diff --git a/app/utils/last_execution_helper.py b/app/utils/last_execution_helper.py index e1d3d92..ecafb14 100644 --- a/app/utils/last_execution_helper.py +++ b/app/utils/last_execution_helper.py @@ -1,21 +1,60 @@ +from __future__ import annotations from datetime import datetime, timezone -from dateutil import tz +from typing import Optional +import os -# Función para obtener la fecha de la última ejecución del programa desde un archivo de texto +DEFAULT_PATH = "./last_execution.txt" +FMT = "%Y-%m-%d %H:%M:%S" -def obtener_fecha_ultima_ejecucion(): +def obtener_fecha_ultima_ejecucion( + path: str = DEFAULT_PATH, + *, + fallback: Optional[datetime] = None, +) -> datetime: + """ + Lee la última fecha de ejecución desde `path` y la devuelve como aware (UTC). + Si no existe o hay error de parseo, devuelve `fallback` (por defecto 2024-01-01 UTC). + """ + if fallback is None: + fallback = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + try: - with open('./last_execution.txt', 'r', encoding="utf8") as f: + with open(path, "r", encoding="utf8") as f: fecha_str = f.read().strip() - return datetime.strptime(fecha_str, '%Y-%m-%d %H:%M:%S').astimezone(tz=tz.UTC) + # Se guarda como texto sin tz; interpretamos como UTC + dt_naive = datetime.strptime(fecha_str, FMT) + return dt_naive.replace(tzinfo=timezone.utc) except FileNotFoundError: - # Si el archivo no existe, se asume que el programa nunca se ha ejecutado antes - return datetime(2024, 1, 1, 0, 0, 0).astimezone(tz=tz.UTC) - -# Función para actualizar la fecha de la última ejecución del programa en el archivo de texto + return fallback + except ValueError: + # Formato inválido en el archivo -> usar fallback + return fallback -def actualizar_fecha_ultima_ejecucion(): - with open('./last_execution.txt', 'w', encoding="utf8") as f: - f.write(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')) +def actualizar_fecha_ultima_ejecucion( + path: str = DEFAULT_PATH, + *, + momento: Optional[datetime] = None, +) -> None: + """ + Escribe en `path` la fecha/hora (UTC) en formato YYYY-MM-DD HH:MM:SS. + Si `momento` es None, usa ahora en UTC. + Crea directorios intermedios si no existen. + """ + if momento is None: + momento = datetime.now(timezone.utc) + else: + # Normalizamos a UTC si viene con tz; si es naive, asumimos UTC + if momento.tzinfo is None: + momento = momento.replace(tzinfo=timezone.utc) + else: + momento = momento.astimezone(timezone.utc) + + # Asegurar carpeta si `path` incluye directorios + folder = os.path.dirname(os.path.abspath(path)) + if folder and not os.path.exists(folder): + os.makedirs(folder, exist_ok=True) + + with open(path, "w", encoding="utf8") as f: + f.write(momento.strftime(FMT))