# services/utils/data_cleaner.py import logging import locale import re from dateutil import parser from typing import Optional, Dict from datetime import datetime # --- ESTA SECCIÓN NO REQUIERE CAMBIOS --- SPANISH_TO_ENGLISH_MONTHS = { 'enero': 'january', 'febrero': 'february', 'marzo': 'march', 'abril': 'april', 'mayo': 'may', 'junio': 'june', 'julio': 'july', 'agosto': 'august', 'septiembre': 'september', 'octubre': 'october', 'noviembre': 'november', 'diciembre': 'december'} def _parse_with_fallback(date_string: str) -> Optional[datetime]: temp_string = date_string.lower().replace(' de ', ' ').replace(' del ', ' ') for spa, eng in SPANISH_TO_ENGLISH_MONTHS.items(): if spa in temp_string: temp_string = temp_string.replace(spa, eng); break try: return parser.parse(temp_string) except (parser.ParserError, ValueError): return None def normalize_date(date_string: str) -> Optional[str]: if not date_string: return None original_locale = locale.getlocale(locale.LC_TIME) parsed_date = None try: try: locale.setlocale(locale.LC_TIME, 'es_ES.UTF-8') except locale.Error: locale.setlocale(locale.LC_TIME, 'Spanish') parsed_date = parser.parse(date_string) except (parser.ParserError, ValueError, locale.Error): parsed_date = _parse_with_fallback(date_string) finally: locale.setlocale(locale.LC_TIME, original_locale) return parsed_date.strftime('%d/%m/%Y') if parsed_date else None # --- FIN DE LA SECCIÓN SIN CAMBIOS --- def clean_numeric_value(text: str) -> str: """Función pública para limpiar y normalizar un string numérico.""" if not text: return "0.00" cleaned = text.strip().replace('.', '').replace(',', '.') try: return f"{float(cleaned):.2f}" except (ValueError, TypeError): logging.warning(f"Could not convert '{text}' to a numeric value. Defaulting to 0.00.") return "0.00" def parse_total_and_tax(text: str) -> Dict[str, Optional[str]]: """ Versión final y robusta. Parsea un string que contiene el total y el IVA. """ logging.info(f"Texto original recibido para parsing: '{text}'") normalized_text = " ".join(text.split()) logging.info(f"Texto normalizado para la regex: '{normalized_text}'") result = {'total_amount': None, 'total_tax_amount': None} total_match = re.search(r'([\d.,]+)', normalized_text) if total_match: result['total_amount'] = clean_numeric_value(total_match.group(1)) # Regex Definitiva: más tolerante con el texto que rodea al número del IVA tax_match = re.search(r'\(.*?(?:incluye|incluido|iva)\s+([\d.,]+).*?\)', normalized_text, re.IGNORECASE) if tax_match: result['total_tax_amount'] = clean_numeric_value(tax_match.group(1)) logging.info(f"¡ÉXITO! Importe de IVA encontrado y limpiado: {result['total_tax_amount']}") else: logging.warning(f"No se encontró desglose de IVA en el texto normalizado: '{normalized_text}'") return result def parse_total_and_tax_LEGACY(text: str) -> Dict[str, Optional[str]]: """ Versión final y robusta. Parsea un string que contiene el total y el IVA. Primero normaliza los espacios en blanco y luego aplica una regex de alta precisión. """ logging.info(f"Texto original recibido para parsing: '{text}'") # --- PASO 1: PRE-PROCESAMIENTO Y NORMALIZACIÓN DEL TEXTO --- # Esto convierte saltos de línea, tabs y espacios múltiples en un único espacio. # Ej: "398,00€\n (incluye..." -> "398,00€ (incluye..." normalized_text = " ".join(text.split()) logging.info(f"Texto normalizado para la regex: '{normalized_text}'") result = {'total_amount': None, 'total_tax_amount': None} # 2. Extraer el importe total (el primer número que encuentre del texto normalizado) total_match = re.search(r'([\d.,]+)', normalized_text) if total_match: result['total_amount'] = clean_numeric_value(total_match.group(1)) logging.info(f"Importe total encontrado y limpiado: {result['total_amount']}") # 3. Regex de alta precisión aplicada sobre el texto normalizado. tax_match = re.search(r'\(.*?(?:incluye|incluido)\s+([\d.,]+)€?\s*IVA.*?\)', normalized_text, re.IGNORECASE) if tax_match: result['total_tax_amount'] = clean_numeric_value(tax_match.group(1)) logging.info(f"¡ÉXITO! Importe de IVA encontrado y limpiado: {result['total_tax_amount']}") else: logging.warning(f"No se encontró desglose de IVA en el texto normalizado: '{normalized_text}'") return result