""" Ticket format patterns for Sales Bot OCR Supports multiple ticket formats from different stores """ import re from typing import Dict, List, Optional # Ticket format configurations TICKET_FORMATS = { 'oxxo': { 'identificadores': ['oxxo', 'femsa', 'cadena comercial'], 'patron_total': r'total\s*\$?\s*([\d,]+\.\d{2})', 'patron_fecha': r'(\d{2}/\d{2}/\d{4})', 'patron_hora': r'(\d{2}:\d{2}:\d{2})', 'prioridad': 1 }, 'walmart': { 'identificadores': ['walmart', 'walmex', 'wal-mart', 'bodega aurrera'], 'patron_total': r'total\s*\$\s*([\d,]+\.\d{2})', 'patron_fecha': r'(\d{2}-\d{2}-\d{4})', 'prioridad': 2 }, 'soriana': { 'identificadores': ['soriana', 'mega soriana', 'city club'], 'patron_total': r'total\s*a?\s*pagar\s*\$?\s*([\d,]+\.\d{2})', 'patron_fecha': r'(\d{2}/\d{2}/\d{4})', 'prioridad': 3 }, 'tienda_pintura': { 'identificadores': ['tinte', 'cromatique', 'oxidante', 'distribuidora', 'colorante', 'pintura', 'tono', 'decolorante', 'revelador'], 'patron_total': r'total\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'patron_productos': r'^(.+?)\s+(\d{1,3})\s+\$?\s*([\d,]+)', 'patron_tubos': r'(\d+)\s*(?:tubos?|pzas?|piezas?|unid)', 'prioridad': 0 # Highest priority for paint stores }, 'farmacia': { 'identificadores': ['farmacia', 'guadalajara', 'benavides', 'similares', 'ahorro'], 'patron_total': r'total\s*\$?\s*([\d,]+\.\d{2})', 'patron_fecha': r'(\d{2}/\d{2}/\d{2,4})', 'prioridad': 4 }, 'seven_eleven': { 'identificadores': ['7-eleven', '7eleven', '7 eleven', 'iconn'], 'patron_total': r'total\s*\$?\s*([\d,]+\.\d{2})', 'patron_fecha': r'(\d{2}/\d{2}/\d{4})', 'prioridad': 5 }, 'generico': { 'identificadores': [], # Fallback - matches everything 'patron_total': r'total\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'patron_fecha': r'(\d{2}[/-]\d{2}[/-]\d{2,4})', 'prioridad': 99 } } # Common patterns for amount extraction (in priority order) AMOUNT_PATTERNS = [ # Explicit total patterns (r'total\s*a\s*pagar\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'total_a_pagar', 1), (r'gran\s*total\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'gran_total', 2), (r'total\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'total', 3), # Payment related (r'a\s*cobrar\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'a_cobrar', 4), (r'importe\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'importe', 5), (r'monto\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'monto', 6), (r'suma\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'suma', 7), # Subtotal (lower priority) (r'subtotal\s*:?\s*\$?\s*([\d,]+[\s\.]?\d{0,2})', 'subtotal', 8), # Last resort - currency amounts at end of lines (r'\$\s*([\d,]+\.\d{2})\s*$', 'monto_final', 9), ] # Date patterns DATE_PATTERNS = [ r'(\d{2}/\d{2}/\d{4})', # DD/MM/YYYY r'(\d{2}-\d{2}-\d{4})', # DD-MM-YYYY r'(\d{4}-\d{2}-\d{2})', # YYYY-MM-DD r'(\d{2}/\d{2}/\d{2})', # DD/MM/YY r'(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})', # D de Mes de YYYY ] # Client name patterns CLIENT_PATTERNS = [ r'cliente\s*:?\s*(.+?)(?:\n|$)', r'nombre\s*:?\s*(.+?)(?:\n|$)', r'sr\.?\s*(.+?)(?:\n|$)', r'sra\.?\s*(.+?)(?:\n|$)', ] def detectar_formato_ticket(texto: str) -> str: """ Detecta el formato del ticket basado en identificadores. Args: texto: Texto extraído del ticket Returns: Nombre del formato detectado """ texto_lower = texto.lower() # Check formats by priority (lower number = higher priority) formatos_encontrados = [] for formato, config in TICKET_FORMATS.items(): if formato == 'generico': continue for identificador in config.get('identificadores', []): if identificador in texto_lower: formatos_encontrados.append((formato, config.get('prioridad', 99))) break if formatos_encontrados: # Sort by priority and return highest priority match formatos_encontrados.sort(key=lambda x: x[1]) return formatos_encontrados[0][0] return 'generico' def get_patron_total(formato: str) -> str: """ Obtiene el patrón de total para un formato específico. Args: formato: Nombre del formato Returns: Patrón regex para extraer el total """ config = TICKET_FORMATS.get(formato, TICKET_FORMATS['generico']) return config.get('patron_total', TICKET_FORMATS['generico']['patron_total']) def extraer_fecha_ticket(texto: str) -> Optional[str]: """ Extrae la fecha del ticket. Args: texto: Texto del ticket Returns: Fecha encontrada o None """ for patron in DATE_PATTERNS: match = re.search(patron, texto, re.IGNORECASE) if match: return match.group(1) return None def extraer_cliente_ticket(texto: str) -> Optional[str]: """ Extrae el nombre del cliente del ticket. Args: texto: Texto del ticket Returns: Nombre del cliente o None """ for patron in CLIENT_PATTERNS: match = re.search(patron, texto, re.IGNORECASE) if match: cliente = match.group(1).strip() # Clean up common artifacts cliente = re.sub(r'[^\w\s\-\.]', '', cliente) if len(cliente) > 2: # Valid name should have at least 3 chars return cliente return None def contar_tubos_texto(texto: str) -> int: """ Cuenta la cantidad de tubos mencionados en el ticket. Args: texto: Texto del ticket Returns: Cantidad de tubos detectados """ texto_lower = texto.lower() total_tubos = 0 # Pattern for explicit tube counts patrones_tubos = [ r'(\d+)\s*(?:tubos?|tbs?)', r'(\d+)\s*(?:pzas?|piezas?)\s*(?:de\s+)?(?:tinte|color)', r'(?:cantidad|qty|cant)\s*:?\s*(\d+)', r'x\s*(\d+)\s*(?:tubos?)?', ] for patron in patrones_tubos: matches = re.findall(patron, texto_lower) for match in matches: try: total_tubos += int(match) except ValueError: continue # If no explicit count found, estimate from line items if total_tubos == 0: # Count lines that look like product entries lineas = texto_lower.split('\n') for linea in lineas: if any(word in linea for word in ['tinte', 'color', 'tubo', 'cromatique']): # Check for quantity at start of line or after product name qty_match = re.search(r'^(\d+)\s+|x\s*(\d+)|(\d+)\s*pza', linea) if qty_match: qty = next((g for g in qty_match.groups() if g), '1') total_tubos += int(qty) else: total_tubos += 1 # Assume 1 if no explicit quantity return total_tubos