import io from datetime import datetime from typing import Tuple, List, Dict, Optional import numpy as np import requests from PIL import Image from loguru import logger as log from qreader import QReader SLOVAKIA = "Slovakia" EUR = "EUR" qreader = QReader(model_size='m') # =============================== Main Methods =============================== def get_receipt_by_qr(image_path: str) -> Tuple[Optional[Dict], Optional[Dict]]: """ Return converted receipt recognized from QR code Args: image_path (str): path to input image Returns: Tuple[Dict, Dict]: Tuple of original JSON from API and receipt in appropriate format, Tuple[None, None]: In case if QR code is not recognized """ image = get_png_image(image_path) decoded_text = qreader.detect_and_decode(image=image) if decoded_text[0]: original_json, receipt = get_receipt_by_uid(decoded_text[0]) return original_json, receipt else: log.error("QR code not found") return None, None def get_receipt_by_uid(receipt_uid: str) -> Tuple[Dict, Dict]: """ Return receipt gotten from API and converted Args: receipt_uid (str): uid of the receipt Returns: Tuple[Dict, Dict]: Tuple of original JSON from API and receipt in appropriate format """ original_json = get_receipt_from_api(receipt_uid) receipt = convert_receipt(original_json) return original_json, receipt # =============================== Utils =============================== def get_receipt_from_api(receipt_uid: str) -> Dict: """ Sends request to Slovenia financial API Args: receipt_uid (str): uid of the receipt Returns: Dict: JSON from API, None: In case of errors """ url = "https://ekasa.financnasprava.sk/mdu/api/v1/opd/receipt/find" headers = { "Content-Type": "application/json", "Accept": "application/json" } data = { "receiptId": receipt_uid } try: response = requests.post(url, json=data, headers=headers) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as http_err: print(f"HTTP error occurred: {http_err}") except Exception as err: print(f"Other error occurred: {err}") def convert_receipt(response: Dict) -> Dict: """ Converts API response to appropriate receipt format Args: response (str): response from API Returns: Dict: JSON from API, None: In case of errors """ rec_receipt = response.get("receipt") rec_items = rec_receipt.get("items") items = convert_items(rec_items) taxes = get_taxes(rec_receipt) unit = rec_receipt.get("unit") subtotal = calculate_subtotal(items) total_price = rec_receipt.get("totalPrice") receipt = { "store_name": rec_receipt.get("organization").get("name"), "country": SLOVAKIA, "receipt_type": None, "store_address": get_address(unit), "date_time": change_date_format(rec_receipt.get("createDate")), "currency": EUR, "sub_total_amount": subtotal, "total_amount": total_price, "total_discount": total_price - subtotal, "all_items_price_with_tax": True, "payment_method": None, "rounding": None, "tax": calculate_tax(rec_receipt), "taxes_not_included_sum": 0.0, # Field only for American\Canadian receipts "tips": None, "items": items, "taxs_items": taxes } return receipt def convert_items(recognized_items: Dict) -> List[Dict]: """ Converts items from API to appropriate format Args: recognized_items (Dict): Dictionary that represents items gotten from API Returns: List[Dict]: Array with items in appropriate format """ items = [] for rec_item in recognized_items: price = round(float(rec_item.get("price", 0)), 2) quantity = round(float(rec_item.get("quantity", 0)), 2) item = { "name": rec_item.get("name"), "unit_price": round(price / quantity, 2), "quantity": quantity, "unit_of_measurement": None, "total_price": price, "discount": None, "category": None, "item_price_with_tax": True } items.append(item) return items def get_taxes(recognized_receipt: Dict) -> List[Dict]: """ Returns list of taxes (Base and Reduced. Both or single by existence) Args: recognized_receipt (Dict): Dictionary that represents receipt gotten from API Returns: List[Dict]: Array with Base or/and Reduced taxes """ receipt = recognized_receipt tax_types = ["Basic", "Reduced"] taxes = [] for tax_type in tax_types: vat_rate = receipt.get(f"vatRate{tax_type}") if vat_rate: tax_base = receipt.get(f"taxBase{tax_type}") vat_amount = receipt.get(f"vatAmount{tax_type}") tax_base = round(tax_base, 2) if tax_base is not None else 0.0 vat_amount = round(vat_amount, 2) if vat_amount is not None else 0.0 tax_info = { "tax_name": f'${vat_rate}%', "percentage": float(vat_rate), "tax_from_amount": tax_base, "tax": vat_amount, "total": round(tax_base + vat_amount, 2), "tax_included": True, } taxes.append(tax_info) return taxes def calculate_tax(recognized_receipt: Dict) -> float: """ Returns sum of Base and Reduced tax amount Args: recognized_receipt (Dict): Dictionary that represents receipt gotten from API Returns: float: Summary tax """ receipt = recognized_receipt tax_types = ["Basic", "Reduced"] tax = .0 for tax_type in tax_types: vat_amount = receipt.get(f"vatAmount{tax_type}") if vat_amount: tax += vat_amount return tax def get_address(unit: Dict) -> str: """ Returns full store address Args: unit (Dict): Dictionary that represents cash register Returns: str: Address in format '{city}, {street_name} {property_number}, {postal_code}' """ return f'{unit.get("municipality")}, {unit.get("streetName")} {unit.get("propertyRegistrationNumber")}, {unit.get("postalCode")}' def calculate_subtotal(items: List[Dict]) -> float: """ Calculate the subtotal of items' total prices. Args: items (List[Dict]): List of items, where each item is a dictionary with a 'total_price' key. Returns: float: The subtotal of all item total prices. """ subtotal = .0 for item in items: subtotal += float(item.get("total_price")) return subtotal def get_png_image(img_path: str) -> np.ndarray: image = Image.open(img_path) png_buffer = io.BytesIO() image.save(png_buffer, format='PNG') png_buffer.seek(0) png_image = Image.open(png_buffer) return np.array(png_image) def change_date_format(date_str: str) -> str: dt = datetime.strptime(date_str, '%d.%m.%Y %H:%M:%S') return dt.strftime('%Y.%m.%d %H:%M:%S')