2612-есть ок OCR, нужно допиливать бота под новый flow для операторов

This commit is contained in:
2026-01-27 00:17:10 +03:00
parent 7d2ffb54b5
commit 1843cb9c20
22 changed files with 1011 additions and 577 deletions

View File

@@ -0,0 +1,165 @@
import logging
import requests
import re
from typing import Optional, List
from pyzbar.pyzbar import decode
from PIL import Image
import numpy as np
from app.schemas.models import ParsedItem
API_TOKEN = "36590.yqtiephCvvkYUKM2W"
API_URL = "https://proverkacheka.com/api/v1/check/get"
logger = logging.getLogger(__name__)
def extract_fiscal_data(text: str) -> Optional[str]:
"""
Ищет в тексте:
- Дата и время (t): 19.12.25 12:16 -> 20251219T1216
- Сумма (s): ИТОГ: 770.00
- ФН (fn): 16 цифр, начинается на 73
- ФД (i): до 8 цифр (ищем после Д: или ФД:)
- ФП (fp): 10 цифр (ищем после П: или ФП:)
Возвращает строку формата: t=20251219T1216&s=770.00&fn=7384440800514469&i=11194&fp=3334166168&n=1
"""
# 1. Поиск даты и времени
# Ищем форматы DD.MM.YY HH:MM или DD.MM.YYYY HH:MM
date_match = re.search(r'(\d{2})\.(\d{2})\.(\d{2,4})\s+(\d{2}):(\d{2})', text)
t_param = ""
if date_match:
d, m, y, hh, mm = date_match.groups()
if len(y) == 2: y = "20" + y
t_param = f"{y}{m}{d}T{hh}{mm}"
# 2. Поиск суммы (Итог)
# Ищем слово ИТОГ и число после него
sum_match = re.search(r'(?:ИТОГ|СУММА|СУММА:)\s*[:]*\s*(\d+[.,]\d{2})', text, re.IGNORECASE)
s_param = ""
if sum_match:
s_param = sum_match.group(1).replace(',', '.')
# 3. Поиск ФН (16 цифр, начинается с 73)
fn_match = re.search(r'\b(73\d{14})\b', text)
fn_param = fn_match.group(1) if fn_match else ""
# 4. Поиск ФД (i) - ищем после маркеров Д: или ФД:
# Берем набор цифр до 8 знаков
fd_match = re.search(r'(?:ФД|Д)[:\s]+(\d{1,8})\b', text)
i_param = fd_match.group(1) if fd_match else ""
# 5. Поиск ФП (fp) - ищем после маркеров П: или ФП:
# Строго 10 цифр
fp_match = re.search(r'(?:ФП|П)[:\s]+(\d{10})\b', text)
fp_param = fp_match.group(1) if fp_match else ""
# Валидация: для формирования запроса к API нам критически важны все параметры
if all([t_param, s_param, fn_param, i_param, fp_param]):
return f"t={t_param}&s={s_param}&fn={fn_param}&i={i_param}&fp={fp_param}&n=1"
return None
def is_valid_fiscal_qr(qr_string: str) -> bool:
"""
Проверяет, соответствует ли строка формату фискального чека ФНС.
Ожидаемый формат: t=...&s=...&fn=...&i=...&fp=...&n=...
Мы проверяем наличие хотя бы 3-х ключевых параметров.
"""
if not qr_string:
return False
# Ключевые параметры, которые обязаны быть в строке чека
required_keys = ["t=", "s=", "fn="]
# Проверяем, что все ключевые параметры присутствуют
# (порядок может отличаться, поэтому проверяем вхождение каждого)
matches = [key in qr_string for key in required_keys]
return all(matches)
def detect_and_decode_qr(image: np.ndarray) -> Optional[str]:
"""
Ищет ВСЕ QR-коды на изображении и возвращает только тот,
который похож на фискальный чек.
"""
try:
pil_img = Image.fromarray(image)
# Декодируем все коды на картинке
decoded_objects = decode(pil_img)
if not decoded_objects:
logger.info("No QR codes detected on the image.")
return None
logger.info(f"Detected {len(decoded_objects)} code(s). Scanning for fiscal data...")
for obj in decoded_objects:
if obj.type == 'QRCODE':
qr_data = obj.data.decode("utf-8")
# Логируем найденное (для отладки, если вдруг формат хитрый)
# Обрезаем длинные строки, чтобы не засорять лог
log_preview = (qr_data[:75] + '..') if len(qr_data) > 75 else qr_data
logger.info(f"Checking QR content: {log_preview}")
if is_valid_fiscal_qr(qr_data):
logger.info("Valid fiscal QR found!")
return qr_data
else:
logger.info("QR skipped (not a fiscal receipt pattern).")
logger.warning("QR codes were found, but none matched the fiscal receipt format.")
return None
except Exception as e:
logger.error(f"Error during QR detection: {e}")
return None
def fetch_data_from_api(qr_raw: str) -> List[ParsedItem]:
"""
Отправляет данные QR-кода в API proverkacheka.com и парсит JSON-ответ.
"""
try:
payload = {
'qrraw': qr_raw,
'token': API_TOKEN
}
logger.info("Sending request to Check API...")
response = requests.post(API_URL, data=payload, timeout=10)
if response.status_code != 200:
logger.error(f"API Error: Status {response.status_code}")
return []
data = response.json()
if data.get('code') != 1:
logger.warning(f"API returned non-success code: {data.get('code')}")
return []
json_data = data.get('data', {}).get('json', {})
items_data = json_data.get('items', [])
parsed_items = []
for item in items_data:
price = float(item.get('price', 0)) / 100.0
total_sum = float(item.get('sum', 0)) / 100.0
quantity = float(item.get('quantity', 0))
name = item.get('name', 'Unknown')
parsed_items.append(ParsedItem(
raw_name=name,
amount=quantity,
price=price,
sum=total_sum
))
return parsed_items
except Exception as e:
logger.error(f"Error fetching/parsing API data: {e}")
return []