From a032383be3624733bb70050f60eb5e49990f3ae0 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 19 Mar 2026 15:36:44 +0100 Subject: [PATCH] . --- app/db/__init__.py | 1 + app/db/normalizations.py | 22 +-- app/db/sql_sentences.py | 26 ++++ app/db/sync_invoices_factuges_REST_API.py | 156 ++++++++++++++++++++++ app/sync_factuges_main.py | 4 +- app/utils/payloads_factuges.py | 93 +++++++++++++ app/utils/send_rest_api.py | 2 +- enviroment/.env | 4 +- enviroment/.env.production.sync.factuges | 2 +- enviroment/dev.env | 20 ++- 10 files changed, 309 insertions(+), 21 deletions(-) create mode 100644 app/db/sync_invoices_factuges_REST_API.py create mode 100644 app/utils/payloads_factuges.py diff --git a/app/db/__init__.py b/app/db/__init__.py index 7e7b4d5..d60003e 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -1,4 +1,5 @@ from .db_connection import get_factuges_connection, get_mysql_connection from .sync_invoices_deleted_factuges import sync_invoices_deleted_factuges from .sync_invoices_factuges import sync_invoices_factuges +from .sync_invoices_factuges_REST_API import sync_invoices_factuges_REST_API from .sync_invoices_verifactu import sync_invoices_verifactu diff --git a/app/db/normalizations.py b/app/db/normalizations.py index d91803c..934effa 100644 --- a/app/db/normalizations.py +++ b/app/db/normalizations.py @@ -46,12 +46,11 @@ def normalize_customer_fields(fd: Dict[str, Any]) -> Dict[str, Any]: return { "is_company": config["CTE_IS_COMPANY"], "tin": clean_tin(str(fd.get("NIF_CIF"))), - "name": str(fd.get("NOMBRE")), - - "street": str(fd.get("CALLE")), - "city": str(fd.get("POBLACION")), - "province": str(fd.get("PROVINCIA")), - "postal_code": str(fd.get("CODIGO_POSTAL")), + "name": fd.get("NOMBRE"), + "street": fd.get("CALLE"), + "city": fd.get("POBLACION"), + "province": fd.get("PROVINCIA"), + "postal_code": fd.get("CODIGO_POSTAL"), "country": config['CTE_COUNTRY_CODE'], "language_code": config['CTE_LANGUAGE_CODE'], "phone_primary": clean_phone(fd.get("TELEFONO_1")), @@ -103,7 +102,7 @@ def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: "series": config['CTE_SERIE'], "factuges_id": int(fd['ID_FACTURA']), - "reference": str(fd['REFERENCIA']), + "reference": fd['REFERENCIA'], # Se asigna la fecha de la subida que es cuando se va a presentar a la AEAT "invoice_date": date.today().strftime("%Y-%m-%d"), "operation_date": str(fd['FECHA_FACTURA']), @@ -175,10 +174,13 @@ def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: disc_global_pct: Optional[Decimal] = None if disc_global_raw is None else Decimal(str(disc_global_raw)) global_discount_percentage_value = None if ( disc_global_pct is None or disc_global_pct == 0) else cents(disc_global_pct) - global_discount_amount_value, taxable_amount_value = apply_discount_cents4( - subtotal_amount_with_dto, disc_global_pct) - total_discount_amount_value = discount_amount_value + global_discount_amount_value + if (global_discount_percentage_value is None): + global_discount_amount_value, taxable_amount_value, total_discount_amount_value = None, subtotal_amount_value, None + else: + global_discount_amount_value, taxable_amount_value = apply_discount_cents4( + subtotal_amount_with_dto, disc_global_pct) + total_discount_amount_value = discount_amount_value + global_discount_amount_value # --- total de línea (escala 4) --- total_value = cents4(total_det) diff --git a/app/db/sql_sentences.py b/app/db/sql_sentences.py index 65f433e..12be9ad 100644 --- a/app/db/sql_sentences.py +++ b/app/db/sql_sentences.py @@ -131,6 +131,32 @@ SELECT_FACTUGES_FACTURAS_CLIENTE = ( "ORDER BY fac.ID, facdet.POSICION" ) +SELECT_FACTUGES_FACTURAS_CLIENTE_CAB = ( + "SELECT fac.VERIFACTU, fac.ID_VERIFACTU, fac.ID || '' AS ID, fac.ID_EMPRESA || '' AS ID_EMPRESA, fac.REFERENCIA, fac.FECHA_FACTURA, fac.ID_CLIENTE || '' as ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, " + "fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, fac.FECHA_ALTA, " + "fac.IMPORTE_NETO, fac.DESCUENTO, fac.IMPORTE_DESCUENTO, fac.BASE_IMPONIBLE, fac.IVA, fac.IMPORTE_IVA, fac.IMPORTE_TOTAL, " + "fac.OBSERVACIONES, fac.ID_FORMA_PAGO, fp.DESCRIPCION as DES_FORMA_PAGO, fac.ID_TIPO_IVA, ti.REFERENCIA as DES_TIPO_IVA, fac.RECARGO_EQUIVALENCIA, fac.RE, fac.IMPORTE_RE, " + "fac.ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, " + "cc.TELEFONO_1, cc.TELEFONO_2, cc.MOVIL_1, cc.MOVIL_2, cc.EMAIL_1, cc.EMAIL_2, cc.PAGINA_WEB " + "FROM FACTURAS_CLIENTE AS fac " + "LEFT JOIN CONTACTOS AS cc ON fac.ID_CLIENTE = cc.ID " + "LEFT JOIN FORMAS_PAGO AS fp ON fac.ID_FORMA_PAGO = fp.ID " + "LEFT JOIN TIPOS_IVA AS ti ON fac.ID_TIPO_IVA = ti.ID " + "WHERE " + "(fac.VERIFACTU = 1) " + "AND (fac.ID_VERIFACTU is null)" + "ORDER BY fac.ID" +) + +SELECT_FACTUGES_FACTURAS_CLIENTE_DET = ( + "SELECT facdet.ID || '' as ID_DET, facdet.ID_FACTURA, facdet.POSICION, facdet.TIPO_DETALLE, facdet.ID_ARTICULO, facdet.CONCEPTO, facdet.CANTIDAD, " + "facdet.IMPORTE_UNIDAD, facdet.DESCUENTO as DESCUENTO_DET, facdet.IMPORTE_TOTAL as IMPORTE_TOTAL_DET, facdet.VISIBLE, facdet.FECHA_ALTA as FECHA_ALTA_DET, facdet.FECHA_MODIFICACION as FECHA_MODIFICACION_DET " + "FROM FACTURAS_CLIENTE_DETALLES AS facdet " + "WHERE " + "(facdet.ID = ?) " + "ORDER BY facdet.POSICION" +) + UPDATE_FACTUGES_LINK = ( "UPDATE FACTURAS_CLIENTE " "SET VERIFACTU=?, " diff --git a/app/db/sync_invoices_factuges_REST_API.py b/app/db/sync_invoices_factuges_REST_API.py new file mode 100644 index 0000000..b0965ce --- /dev/null +++ b/app/db/sync_invoices_factuges_REST_API.py @@ -0,0 +1,156 @@ +from typing import Any, Dict + +from uuid6 import uuid7 + +from app.config import load_config, logger +from app.utils import ( + create_invoice_header, + insert_invoice_REST_API_FACTUGES, + insert_item_and_taxes, +) + +from . import normalizations as NORMALIZA +from . import sql_sentences as SQL + + +def sync_invoices_factuges_REST_API(conn_factuges, conn_mysql, last_execution_date): + config = load_config() + + # 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_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 + factura_payload: dict | None = None + + try: + cursorMySQL = conn_mysql.cursor() + cursor_FactuGES = conn_factuges.cursor() + + # Insertar datos en la tabla 'customer_invoices' + for facturas in filas: + # Preparamos los campos para evitar errores + customer_fields = NORMALIZA.normalize_customer_fields(facturas) + header_invoice_fields = NORMALIZA.normalize_header_invoice_fields( + facturas + ) + details_invoice_fields = NORMALIZA.normalize_details_invoice_fields( + facturas + ) + + factuges_id = int(facturas["ID"]) + 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 = True # validar_nif_verifacti( +# customer_fields["tin"], customer_fields["name"], config +# ) + + if customer_valid: + + # ---- cabecera factura + 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_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) + + num_fac_procesed += 1 + + # 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), + # ) + + # 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 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 diff --git a/app/sync_factuges_main.py b/app/sync_factuges_main.py index f15ab22..c18f96c 100644 --- a/app/sync_factuges_main.py +++ b/app/sync_factuges_main.py @@ -9,7 +9,7 @@ from app.db import ( get_factuges_connection, get_mysql_connection, sync_invoices_deleted_factuges, - sync_invoices_factuges, + sync_invoices_factuges_REST_API, ) from app.utils import actualizar_fecha_ultima_ejecucion, obtener_fecha_ultima_ejecucion @@ -63,7 +63,7 @@ def main(): logger.info( ">>>>>>>>>>> INI Sync invoices FactuGES escritorio to FactuGES web") sync_invoices_deleted_factuges(conn_factuges, conn_mysql, last_execution_date_local_tz) - sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date_local_tz) + sync_invoices_factuges_REST_API(conn_factuges, conn_mysql, last_execution_date_local_tz) # Confirmar los cambios conn_mysql.commit() diff --git a/app/utils/payloads_factuges.py b/app/utils/payloads_factuges.py new file mode 100644 index 0000000..9d33eab --- /dev/null +++ b/app/utils/payloads_factuges.py @@ -0,0 +1,93 @@ +from typing import Any, Dict + +from uuid6 import uuid7 + + +def none_to_empty(value: Any) -> str: + return "" if value is None else value + + +def create_invoice_header( + cf, + hif, + payment_method_id, + payment_method_description +) -> Dict[str, Any]: + return { + "id": str(uuid7()), + "factuges_id": hif.get("factuges_id"), + "company_id": hif.get("company_id"), + "is_proforma": hif.get("is_proforma"), + "status": hif.get("status"), + "series": hif.get("series"), + "reference": hif.get("reference"), + "description": hif.get("description"), + "invoice_date": hif.get("invoice_date"), + "operation_date": hif.get("operation_date"), + "notes": none_to_empty(hif.get("notes")), + "language_code": cf.get("language_code"), + + "subtotal_amount_value": none_to_empty(hif.get("subtotal_amount_value")), + "global_discount_percentage": none_to_empty(hif.get("discount_percentage_val")), + "discount_amount_value": none_to_empty(hif.get("discount_amount_value")), + "taxable_amount_value": hif.get("taxable_amount_value"), + "taxes_amount_value": hif.get("taxes_amount_value"), + "total_amount_value": hif.get("total_amount_value"), + "payment_method_id": payment_method_id, + "payment_method_description": payment_method_description, + + "customer": { + "is_company": cf["is_company"], + "name": cf["name"], + "tin": cf["tin"], + "street": cf["street"], + "city": cf["city"], + "province": cf["province"], + "postal_code": cf["postal_code"], + "country": cf["country"], + "language_code": cf["language_code"], + "phone_primary": none_to_empty(cf["phone_primary"]), + "phone_secondary": none_to_empty(cf["phone_secondary"]), + "mobile_primary": none_to_empty(cf["mobile_primary"]), + "mobile_secondary": none_to_empty(cf["mobile_secondary"]), + "email_primary": none_to_empty(cf["email_primary"]), + "email_secondary": none_to_empty(cf["email_secondary"]), + "website": cf["website"], + }, + + "items": [], + + } + + +def insert_item_and_taxes(fields) -> Dict[str, Any]: + # logger.info(fields) + return { + "item_id": str(uuid7()), + "position": str(fields.get("position")), + "description": fields.get("description"), + "quantity_value": str(fields.get("quantity_value")), + "unit_value": str(fields.get("unit_value")), + "subtotal_amount_value": fields.get("subtotal_amount_value"), + "item_discount_percentage_value": none_to_empty(fields.get("item_discount_percentage_value")), + "item_discount_amount_value": none_to_empty(fields.get("item_discount_amount_value")), + "global_discount_percentage_value": none_to_empty(fields.get("global_discount_percentage_value")), + "global_discount_amount_value": none_to_empty(fields.get("global_discount_amount_value")), + "total_discount_amount_value": fields.get("total_discount_amount_value"), + "taxable_amount_value": fields.get("taxable_amount_value"), + "total_value": fields.get("total_value"), + "iva_code": fields.get("iva_code"), + "iva_percentage_value": str(fields.get("iva_percentage_value")), + "iva_amount_value": fields.get("iva_amount_value"), + "rec_code": none_to_empty(fields.get("rec_code")), + "rec_percentage_value": none_to_empty(fields.get("rec_percentage_value")), + "rec_amount_value": fields.get("rec_amount_value"), + "taxes_amount_value": fields.get("taxes_amount_value"), + } + + # 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')) + # ) diff --git a/app/utils/send_rest_api.py b/app/utils/send_rest_api.py index 37f2775..cbd21e3 100644 --- a/app/utils/send_rest_api.py +++ b/app/utils/send_rest_api.py @@ -168,7 +168,7 @@ def insert_invoice_REST_API_FACTUGES( Retorna: """ - url = "http://192.168.0.104:3002/api/v1/proformas" + url = "http://192.168.0.104:3002/api/v1/factuges" timeout: int = 10 headers = {"Content-Type": "application/json", "Accept": "application/json"} diff --git a/enviroment/.env b/enviroment/.env index d155033..593ba3b 100644 --- a/enviroment/.env +++ b/enviroment/.env @@ -4,12 +4,12 @@ STATE_PATH = ./ #LOG_PATH = ./app.log #DESARROLLO ACANA -FACTUGES_HOST = 192.168.0.105 +FACTUGES_HOST = 192.168.0.109 FACTUGES_PORT = 3050 FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB FACTUGES_USER = sysdba FACTUGES_PASSWORD = masterkey -CONFIGURACION ACANA +#CONFIGURACION ACANA CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a' VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= diff --git a/enviroment/.env.production.sync.factuges b/enviroment/.env.production.sync.factuges index 99a9bb0..c476277 100644 --- a/enviroment/.env.production.sync.factuges +++ b/enviroment/.env.production.sync.factuges @@ -4,7 +4,7 @@ STATE_PATH = ./ #LOG_PATH = ./app.log #DESARROLLO ACANA -FACTUGES_HOST = 192.168.0.105 +FACTUGES_HOST = 192.168.0.109 FACTUGES_PORT = 3050 FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB FACTUGES_USER = sysdba diff --git a/enviroment/dev.env b/enviroment/dev.env index 5647c48..db119fc 100644 --- a/enviroment/dev.env +++ b/enviroment/dev.env @@ -14,17 +14,27 @@ STATE_PATH = ./ #VERIFACTU_API_KEY = vf_prod_yfjonNPv2E4Fij+5J0hct0zCgUeFYT2dZzb23UZlM+Q= #CTE_SERIE = 'F25' -#DESARROLLO ACANA -FACTUGES_HOST = 192.168.0.105 +#DESARROLLO RODAX +FACTUGES_HOST = 192.168.0.156 FACTUGES_PORT = 3050 -FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB +FACTUGES_DATABASE = C:\Codigo\Output\Debug\Database\FACTUGES.FDB FACTUGES_USER = sysdba FACTUGES_PASSWORD = masterkey -#CONFIGURACION ACANA -CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a' +CTE_COMPANY_ID = '5e4dc5b3-96b9-4968-9490-14bd032fec5f' VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= CTE_SERIE = 'F25/' +#DESARROLLO ACANA +#FACTUGES_HOST = 192.168.0.109 +#FACTUGES_PORT = 3050 +#FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB +#FACTUGES_USER = sysdba +#FACTUGES_PASSWORD = masterkey +#CONFIGURACION ACANA +#CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a' +#VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= +#CTE_SERIE = 'F25/' + #DESARROLLO ALONSO Y SAL #FACTUGES_HOST = 192.168.0.146 #FACTUGES_PORT = 3050