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, 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("notes"), 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, hif: Dict[str, Any], ) -> None: """Inserta impuestos de cabecera""" cur.execute(SQL.INSERT_INVOICE_TAX, ( str(uuid7()), invoice_id, hif.get("base"), hif.get("iva_code"), hif.get("iva_percentage_value"), hif.get("iva"), hif.get("rec_code"), hif.get("rec_percentage_value"), hif.get("re"), hif.get("retention_code"), hif.get("retention_percentage_value"), hif.get("retention_amount_value"), hif.get("taxes_amount_value"), ), ) 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("subtotal_amount_value"), fields.get("discount_percentage_value"), fields.get("discount_amount_value"), fields.get("global_discount_percentage_value"), fields.get("global_discount_amount_value"), fields.get("total_discount_amount_value"), fields.get("taxable_amount_value"), fields.get("total_value"), fields.get("iva_code"), fields.get("iva_percentage_value"), fields.get("iva_amount_value"), fields.get("rec_code"), fields.get("rec_percentage_value"), fields.get("rec_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')) # )