Загрузка данных


import os
import requests
import psycopg2

from psycopg2.extras import execute_values
from dotenv import load_dotenv
from datetime import datetime, timedelta

load_dotenv()

API_KEY = os.getenv("VP_API_KEY")

DB_CONFIG = {
    "host": os.getenv("DB_HOST"),
    "port": os.getenv("DB_PORT"),
    "dbname": os.getenv("DB_NAME"),
    "user": os.getenv("DB_USER"),
    "password": os.getenv("DB_PASSWORD"),
}

BASE_URL = "https://tl.myvirtualpos.ru/api/receipt"

KRASNOYARSK_WAREHOUSE_IDS = {
    229,
    226,
    224,
    225,
    228,
    230,
    232,
    234
}


def safe_float(value):
    if value is None or value == "":
        return 0.0

    try:
        return float(value)
    except:
        return 0.0


def fetch_sales(date_from, date_to):

    params = {
        "apikey": API_KEY,
        "format": "json",
        "datefrom": date_from.strftime("%Y%m%d"),
        "dateto": date_to.strftime("%Y%m%d"),
        "fields": "itemname:article:user_fio:manufacturer_name"
    }

    response = requests.get(
        BASE_URL,
        params=params,
        timeout=120
    )

    response.raise_for_status()

    data = response.json()

    return data.get("receipts", [])


def transform_data(receipts):

    rows = []

    for receipt_wrapper in receipts:

        receipt = receipt_wrapper.get("receipt", {})

        receipt_id = receipt.get("receipt_id")
        sale_type = receipt.get("doc_type")
        sale_datetime_raw = receipt.get("date")
        warehouse_id = receipt.get("warehouseid")
        seller_name = receipt.get("user_fio")

        if warehouse_id in KRASNOYARSK_WAREHOUSE_IDS:
            continue

        if not receipt_id:
            continue

        if not sale_datetime_raw:
            continue

        sale_datetime = datetime.strptime(
            sale_datetime_raw,
            "%d.%m.%Y %H:%M:%S"
        )

        items = receipt.get("items", [])

        for item_wrapper in items:

            item = item_wrapper.get("item", {})

            item_id = item.get("itemid")

            if item_id is None:
                continue

            row = (
                receipt_id,
                sale_type,
                sale_datetime,
                warehouse_id,
                item.get("manufacturer_name"),
                item_id,
                item.get("article"),
                item.get("itemname"),
                safe_float(item.get("cogs")),
                safe_float(item.get("quantity")),
                safe_float(item.get("amount")),
                safe_float(item.get("discount")),
                seller_name
            )

            rows.append(row)

    return rows


def load_to_postgres(rows):

    if not rows:
        return

    conn = psycopg2.connect(**DB_CONFIG)

    try:

        with conn:

            with conn.cursor() as cur:

                query = """
                    INSERT INTO sales_raw (
                        receipt_id,
                        sale_type,
                        sale_datetime,
                        warehouse_id,
                        manufacturer,
                        item_id,
                        sku,
                        product_name,
                        unit_cost,
                        quantity,
                        line_amount,
                        discount_amount,
                        seller_name
                    )
                    VALUES %s
                    ON CONFLICT (receipt_id, item_id)
                    DO NOTHING
                """

                execute_values(cur, query, rows)

        print(f"Загружено строк: {len(rows)}")

    finally:

        conn.close()


def print_uncategorized_items():

    conn = psycopg2.connect(**DB_CONFIG)

    try:

        with conn.cursor() as cur:

            cur.execute("""
                SELECT DISTINCT
                    s.item_id,
                    s.sku,
                    s.product_name,
                    s.manufacturer
                FROM sales_raw s
                LEFT JOIN product_categories c
                    ON s.item_id::text = c.item_id
                WHERE c.item_id IS NULL
                ORDER BY s.product_name
            """)

            rows = cur.fetchall()

            print("")
            print("Товары без категории")
            print("-" * 80)

            if not rows:
                print("Все товары категоризированы")
                return

            for row in rows:

                item_id, sku, product_name, manufacturer = row

                print(
                    f"item_id={item_id} | "
                    f"sku={sku} | "
                    f"manufacturer={manufacturer} | "
                    f"{product_name}"
                )

            print("-" * 80)
            print(f"Всего: {len(rows)}")

    finally:

        conn.close()


def main():

    start_date = datetime(2026, 1, 1)

    end_date = datetime.now()

    current = start_date

    while current < end_date:

        next_day = current + timedelta(days=1)

        print("")
        print(
            f"Загрузка продаж за "
            f"{current.strftime('%d.%m.%Y')}"
        )

        receipts = fetch_sales(
            current,
            next_day
        )

        print(f"Получено чеков: {len(receipts)}")

        rows = transform_data(receipts)

        print(
            f"Подготовлено строк: {len(rows)}"
        )

        load_to_postgres(rows)

        current = next_day

    print_uncategorized_items()

    print("")
    print("Готово")


if __name__ == "__main__":
    main()