mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
2612-есть ок OCR, нужно допиливать бота под новый flow для операторов
This commit is contained in:
165
ocr-service/app/services/qr.py
Normal file
165
ocr-service/app/services/qr.py
Normal 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 []
|
||||
Reference in New Issue
Block a user