import re from typing import List from pydantic import BaseModel class ParsedItem(BaseModel): raw_name: str amount: float price: float sum: float # Регулярка для поиска чисел с плавающей точкой: 123.00, 123,00, 10.5 FLOAT_RE = r'\d+[.,]\d{2}' def clean_text(text: str) -> str: """Удаляет лишние символы из названия товара.""" # Оставляем буквы, цифры, пробелы и базовые знаки return re.sub(r'[^\w\s.,%/-]', '', text).strip() def parse_float(val: str) -> float: """Преобразует строку '123,45' или '123.45' в float.""" if not val: return 0.0 # Заменяем запятую на точку и убираем возможные пробелы return float(val.replace(',', '.').replace(' ', '')) def parse_receipt_text(text: str) -> List[ParsedItem]: """ Парсит текст чека построчно. Логика: накапливаем строки названия, пока не встретим строку с математикой (цена/сумма). """ lines = text.split('\n') items = [] name_buffer = [] for line in lines: line = line.strip() if not line: continue # Ищем все числа, похожие на цену (с двумя знаками после запятой/точки) # Пример: 129.99 floats = re.findall(FLOAT_RE, line) # Эвристика: строка считается "товарной позицией с ценой", если в ней есть минимум 2 числа # (обычно Цена и Сумма, или Кол-во и Сумма) # ИЛИ одно число, если это итоговая сумма, но мы ищем товары. is_price_line = False if len(floats) >= 2: is_price_line = True vals = [parse_float(f) for f in floats] # Попытка определить структуру: Цена x Кол-во = Сумма price = 0.0 amount = 1.0 total = 0.0 # Обычно последнее число - это сумма (итог по строке) total = vals[-1] if len(vals) == 2: # Скорее всего: Цена ... Сумма (кол-во = 1) # Или: Кол-во ... Сумма (если цена не распозналась) # Предположим amount=1, тогда первое число - цена price = vals[0] amount = 1.0 # Проверка на адекватность: сумма обычно >= цены # Если total < price, возможно порядок перепутан или это скидка if total < price and total != 0: # Если total сильно меньше, возможно это не сумма pass elif total > price and price > 0: # Пытаемся вычислить кол-во calc_amount = total / price # Если результат близок к целому (например 1.999 -> 2), то ок if abs(round(calc_amount) - calc_amount) < 0.05: amount = float(round(calc_amount)) elif len(vals) >= 3: # Варианты: [Цена, Кол-во, Сумма] или [Кол-во, Цена, Сумма] # Проверяем математику: A * B = C v1, v2 = vals[-3], vals[-2] if abs(v1 * v2 - total) < 0.5: # Допуск 0.5 руб price = v1 amount = v2 elif abs(v2 * v1 - total) < 0.5: price = v2 amount = v1 else: # Если математика не сходится, берем предпоследнее как цену price = vals[-2] amount = 1.0 # Сборка названия full_name = " ".join(name_buffer).strip() # Если буфер пуст, возможно название в этой же строке слева if not full_name: # Удаляем найденные числа из строки, остаток считаем названием text_without_floats = re.sub(FLOAT_RE, '', line) full_name = clean_text(text_without_floats) # Фильтрация мусора (слишком короткие названия или нулевые суммы) if len(full_name) > 2 and total > 0: items.append(ParsedItem( raw_name=full_name, amount=amount, price=price, sum=total )) # Очищаем буфер, так как позиция закрыта name_buffer = [] else: # Строка не похожа на цену. # Проверяем на стоп-слова (конец чека) upper_line = line.upper() if "ИТОГ" in upper_line or "СУММА" in upper_line or "ПРИХОД" in upper_line: # Считаем, что товары закончились name_buffer = [] # Можно здесь сделать break, если уверены, что ниже товаров нет continue # Добавляем в буфер названия name_buffer.append(line) return items